From facf257ff4ca961adedb51b2385f52e6e1f2bc52 Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Fri, 22 May 2026 01:27:16 -0700 Subject: [PATCH 1/4] =?UTF-8?q?feat(ai):=20finalize=20IBackendsTransport?= =?UTF-8?q?=20=E2=80=94=20add=20list/uninstall,=20widen=20opts,=20tighten?= =?UTF-8?q?=20JSDoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds list() and uninstall(backend) to IBackendsTransport so the libs interface matches what builder's MessagePortBackendsTransport already implements and the renderer UI requires. Widens IEnsureRunningRequest.opts from { ctx: number } to Readonly> so non-llamacpp backends (sd-cpp, future MLX/whisper) don't have to lie with ctx: 0. Updates StableDiffusionCppProvider to pass opts: {} accordingly. Tightens JSDoc on release() (resolves on post, not broker-ack) and subscribeStatus() (idempotent unsubscribe, dedup of double-subscribe). --- .../src/provider-utils/IBackendsTransport.ts | 37 ++++++++++--- .../IBackendsTransport.types.test.ts | 55 +++++++++++++++++++ .../src/ai/StableDiffusionCppProvider.ts | 2 +- 3 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 packages/ai/tests/provider-utils/IBackendsTransport.types.test.ts diff --git a/packages/ai/src/provider-utils/IBackendsTransport.ts b/packages/ai/src/provider-utils/IBackendsTransport.ts index 26dd437d4..2c012ad94 100644 --- a/packages/ai/src/provider-utils/IBackendsTransport.ts +++ b/packages/ai/src/provider-utils/IBackendsTransport.ts @@ -23,8 +23,12 @@ export interface IEnsureRunningRequest { readonly backend: string; /** Absolute path to the model file. */ readonly modelPath: string; - /** Runtime options forwarded to the backend process. */ - readonly opts: { readonly ctx: number }; + /** + * Backend-specific runtime options forwarded to the broker as opaque JSON. + * llamacpp uses `{ ctx: number }`; sd-cpp omits it; future backends define + * their own schema. + */ + readonly opts: Readonly>; } /** @@ -40,7 +44,10 @@ export interface IRunningHandle { /** * Decrements the broker's refcount for this handle. The backend may shut * down after the broker's idle timeout if refcount reaches zero. - * Fire-and-forget — no ack is awaited from the broker. + * + * The returned promise resolves once the release message has been posted + * to the port; the broker does not acknowledge. Errors posting (e.g. port + * closed) reject. */ readonly release: () => Promise; } @@ -70,7 +77,7 @@ export interface IBackendsTransport { /** * Acquire (or share) a running backend. Resolves once the backend is healthy. * - * Multiple callers requesting the same `(backend, modelPath, opts.ctx)` triple + * Multiple callers requesting the same `(backend, modelPath, opts)` triple * will share one process via the broker's refcounting. `release()` on the * returned handle decrements the refcount. */ @@ -79,9 +86,11 @@ export interface IBackendsTransport { /** * Subscribe to status updates for a backend. * - * The callback is called immediately (on the next port message) and on every - * subsequent status change. Subscriptions persist across port reconnects - * (utility crash + restart). + * The callback fires on every subsequent broker `status` event; callers + * wanting an initial snapshot must invoke `list()`. Subscriptions persist + * across port reconnects. Implementations MUST be idempotent: calling the + * returned unsubscribe twice is a no-op; subscribing the same callback + * twice is allowed and de-duplicated. * * @returns An unsubscribe function. Call it to stop receiving updates. */ @@ -95,4 +104,18 @@ export interface IBackendsTransport { * `total` may be 0 if the content-length is unknown. */ install(backend: string, onProgress?: (bytes: number, total: number) => void): Promise; + + /** + * Fire-and-forget request for the broker to broadcast a `status` + * event for every backend in its registry. Resolves once the request + * has been posted (the broker does not send a discrete reply). + */ + list(): Promise; + + /** + * Removes the backend's installed binary. In v1 the broker rejects + * this with an error; callers should handle the rejection. Future + * versions may implement teardown semantics. + */ + uninstall(backend: string): Promise; } diff --git a/packages/ai/tests/provider-utils/IBackendsTransport.types.test.ts b/packages/ai/tests/provider-utils/IBackendsTransport.types.test.ts new file mode 100644 index 000000000..ce77ade83 --- /dev/null +++ b/packages/ai/tests/provider-utils/IBackendsTransport.types.test.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2026 Steven Roussey + * SPDX-License-Identifier: Apache-2.0 + */ + +// ──────────────────────────────────────────────────────────────────────────── +// Compile-time conformance tests for IBackendsTransport. +// +// These tests run via the test runner but their value is in `tsc` accepting +// (or rejecting) the declarations below. No runtime assertions: if the file +// compiles, the contract holds. +// ──────────────────────────────────────────────────────────────────────────── + +import type { IBackendsTransport, IEnsureRunningRequest } from "../../src/provider-utils"; + +// opts is now open — accepts the historic llamacpp shape … +const _checkOptsWithCtx: IEnsureRunningRequest["opts"] = { ctx: 4096 }; +// … the empty shape for backends like sd-cpp that have no per-run opts … +const _checkOptsEmpty: IEnsureRunningRequest["opts"] = {}; +// … and arbitrary shapes for future backends (MLX, whisper, etc.). +const _checkOptsArbitrary: IEnsureRunningRequest["opts"] = { foo: "bar", n: 42 }; + +// Interface exposes `list` and `uninstall` alongside the existing methods. +type _Methods = keyof IBackendsTransport; +const _hasList: _Methods = "list"; +const _hasUninstall: _Methods = "uninstall"; + +// Structural-conformance dummy — any breaking change to the interface will +// fail typecheck on this assignment. +const _conforms: IBackendsTransport = { + ensureRunning: async () => ({ url: "http://localhost", release: async () => {} }), + subscribeStatus: () => () => {}, + install: async () => {}, + list: async () => {}, + uninstall: async () => {}, +}; + +// Reference each binding so eslint's `no-unused-vars` and `noUnusedLocals` +// don't trip even though these assertions are purely compile-time. +void _checkOptsWithCtx; +void _checkOptsEmpty; +void _checkOptsArbitrary; +void _hasList; +void _hasUninstall; +void _conforms; + +// Provide a single trivial runtime assertion so test runners that require at +// least one `test`/`it` block don't choke on this file. +import { describe, it, expect } from "vitest"; +describe("IBackendsTransport (type-only conformance)", () => { + it("compiles", () => { + expect(true).toBe(true); + }); +}); diff --git a/providers/stable-diffusion-cpp/src/ai/StableDiffusionCppProvider.ts b/providers/stable-diffusion-cpp/src/ai/StableDiffusionCppProvider.ts index 56e3d8722..bd79f141c 100644 --- a/providers/stable-diffusion-cpp/src/ai/StableDiffusionCppProvider.ts +++ b/providers/stable-diffusion-cpp/src/ai/StableDiffusionCppProvider.ts @@ -109,7 +109,7 @@ function createStableDiffusionCppImageGenerateRunFn( handle = await options.transport.ensureRunning({ backend: "stable-diffusion-cpp", modelPath: model.model_id, - opts: { ctx: 0 }, + opts: {}, }); baseUrl = handle.url.replace(/\/$/, ""); } From 9612cff7a2cf367f715fe5575cb6d9f7a6255f1f Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Fri, 22 May 2026 08:25:36 -0700 Subject: [PATCH 2/4] docs(IBackendsTransport): clarify opts JSDoc for sd-cpp --- packages/ai/src/provider-utils/IBackendsTransport.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/provider-utils/IBackendsTransport.ts b/packages/ai/src/provider-utils/IBackendsTransport.ts index 2c012ad94..67bf0d8dd 100644 --- a/packages/ai/src/provider-utils/IBackendsTransport.ts +++ b/packages/ai/src/provider-utils/IBackendsTransport.ts @@ -25,8 +25,8 @@ export interface IEnsureRunningRequest { readonly modelPath: string; /** * Backend-specific runtime options forwarded to the broker as opaque JSON. - * llamacpp uses `{ ctx: number }`; sd-cpp omits it; future backends define - * their own schema. + * llamacpp uses `{ ctx: number }`; sd-cpp passes an empty object `{}`; + * future backends define their own schema. */ readonly opts: Readonly>; } From a12aff9ec0cd79b8d83fb46ab25709c817c7c894 Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Fri, 22 May 2026 08:25:57 -0700 Subject: [PATCH 3/4] test(IBackendsTransport): move type test under packages/test for CI discovery The repo's test runner (scripts/test.ts) only collects Vitest files from packages/test/src/test, so this file was never executed in CI. Removing the old location; the file is recreated under packages/test/src/test/ai in the following commit (also strengthens the structural conformance assertion with explicit parameter signatures). --- .../IBackendsTransport.types.test.ts | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 packages/ai/tests/provider-utils/IBackendsTransport.types.test.ts diff --git a/packages/ai/tests/provider-utils/IBackendsTransport.types.test.ts b/packages/ai/tests/provider-utils/IBackendsTransport.types.test.ts deleted file mode 100644 index ce77ade83..000000000 --- a/packages/ai/tests/provider-utils/IBackendsTransport.types.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @license - * Copyright 2026 Steven Roussey - * SPDX-License-Identifier: Apache-2.0 - */ - -// ──────────────────────────────────────────────────────────────────────────── -// Compile-time conformance tests for IBackendsTransport. -// -// These tests run via the test runner but their value is in `tsc` accepting -// (or rejecting) the declarations below. No runtime assertions: if the file -// compiles, the contract holds. -// ──────────────────────────────────────────────────────────────────────────── - -import type { IBackendsTransport, IEnsureRunningRequest } from "../../src/provider-utils"; - -// opts is now open — accepts the historic llamacpp shape … -const _checkOptsWithCtx: IEnsureRunningRequest["opts"] = { ctx: 4096 }; -// … the empty shape for backends like sd-cpp that have no per-run opts … -const _checkOptsEmpty: IEnsureRunningRequest["opts"] = {}; -// … and arbitrary shapes for future backends (MLX, whisper, etc.). -const _checkOptsArbitrary: IEnsureRunningRequest["opts"] = { foo: "bar", n: 42 }; - -// Interface exposes `list` and `uninstall` alongside the existing methods. -type _Methods = keyof IBackendsTransport; -const _hasList: _Methods = "list"; -const _hasUninstall: _Methods = "uninstall"; - -// Structural-conformance dummy — any breaking change to the interface will -// fail typecheck on this assignment. -const _conforms: IBackendsTransport = { - ensureRunning: async () => ({ url: "http://localhost", release: async () => {} }), - subscribeStatus: () => () => {}, - install: async () => {}, - list: async () => {}, - uninstall: async () => {}, -}; - -// Reference each binding so eslint's `no-unused-vars` and `noUnusedLocals` -// don't trip even though these assertions are purely compile-time. -void _checkOptsWithCtx; -void _checkOptsEmpty; -void _checkOptsArbitrary; -void _hasList; -void _hasUninstall; -void _conforms; - -// Provide a single trivial runtime assertion so test runners that require at -// least one `test`/`it` block don't choke on this file. -import { describe, it, expect } from "vitest"; -describe("IBackendsTransport (type-only conformance)", () => { - it("compiles", () => { - expect(true).toBe(true); - }); -}); From 911c165c85ca9339a346078c948969118d2a1b2e Mon Sep 17 00:00:00 2001 From: Steven Roussey Date: Fri, 22 May 2026 08:26:19 -0700 Subject: [PATCH 4/4] test(IBackendsTransport): strengthen conformance with explicit signatures Recreates the type-conformance test at packages/test/src/test/ai/ (the location scripts/test.ts discovers) and replaces the zero-arg dummy implementations with explicit parameter signatures drawn from the interface. TypeScript happily assigns `() => X` to `(arg) => X`, so the previous form did not catch parameter-list drift; the new form fails typecheck on any rename, type change, or return-type change to a method of `IBackendsTransport`. Imports come from `@workglow/ai/provider-utils` (the public subpath export) instead of the previous relative path into packages/ai/src. --- .../test/ai/IBackendsTransport.types.test.ts | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 packages/test/src/test/ai/IBackendsTransport.types.test.ts diff --git a/packages/test/src/test/ai/IBackendsTransport.types.test.ts b/packages/test/src/test/ai/IBackendsTransport.types.test.ts new file mode 100644 index 000000000..d002ae162 --- /dev/null +++ b/packages/test/src/test/ai/IBackendsTransport.types.test.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2026 Steven Roussey + * SPDX-License-Identifier: Apache-2.0 + */ + +// ──────────────────────────────────────────────────────────────────────────── +// Compile-time conformance tests for IBackendsTransport. +// +// These tests run via the test runner but their value is in `tsc` accepting +// (or rejecting) the declarations below. No runtime assertions of substance: +// if the file compiles, the contract holds. +// +// Lives under packages/test/src/test/ai/ so that scripts/test.ts picks it up +// (the runner only scans packages/test/src/test). +// ──────────────────────────────────────────────────────────────────────────── + +import type { + IBackendsTransport, + IBackendStatus, + IEnsureRunningRequest, + IRunningHandle, +} from "@workglow/ai/provider-utils"; +import { expect, test } from "vitest"; + +// `opts` is open — accepts the historic llamacpp shape … +const _checkOptsWithCtx: IEnsureRunningRequest["opts"] = { ctx: 4096 }; +// … the empty shape that sd-cpp uses today (no per-run options) … +const _checkOptsEmpty: IEnsureRunningRequest["opts"] = {}; +// … and arbitrary shapes future backends may define. +const _checkOptsArbitrary: IEnsureRunningRequest["opts"] = { foo: "bar", n: 42 }; + +// Interface exposes `list` and `uninstall` alongside the existing methods. +type _Methods = keyof IBackendsTransport; +const _hasList: _Methods = "list"; +const _hasUninstall: _Methods = "uninstall"; + +// Structural conformance using explicit parameter signatures: TypeScript +// allows assigning `() => X` to `(arg: T) => X`, so a zero-arg dummy would +// silently accept a parameter-list change. Spelling each signature out +// forces a typecheck failure on any rename, type change, or return-type +// change to a method of `IBackendsTransport`. +const _conforms: IBackendsTransport = { + ensureRunning: (_req: IEnsureRunningRequest): Promise => { + return Promise.resolve({ + url: "http://127.0.0.1:0", + release: (): Promise => Promise.resolve(), + }); + }, + subscribeStatus: ( + _backend: string, + _callback: (status: IBackendStatus) => void + ): (() => void) => { + return (): void => undefined; + }, + install: ( + _backend: string, + _onProgress?: (bytes: number, total: number) => void + ): Promise => Promise.resolve(), + list: (): Promise => Promise.resolve(), + uninstall: (_backend: string): Promise => Promise.resolve(), +}; + +// Silence `no-unused-vars` / `noUnusedLocals` on the type-only assertions. +void _checkOptsWithCtx; +void _checkOptsEmpty; +void _checkOptsArbitrary; +void _hasList; +void _hasUninstall; +void _conforms; + +// Vitest requires at least one runtime test in the file. +test("IBackendsTransport conformance compiles", () => { + expect(true).toBe(true); +});