@immediately-run/sdk 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalog.cjs +6 -3
- package/dist/catalog.cjs.map +1 -1
- package/dist/catalog.d.cts +3 -2
- package/dist/catalog.d.ts +3 -2
- package/dist/catalog.js +6 -3
- package/dist/catalog.js.map +1 -1
- package/dist/llm.cjs +6 -1
- package/dist/llm.cjs.map +1 -1
- package/dist/llm.d.cts +6 -0
- package/dist/llm.d.ts +6 -0
- package/dist/llm.js +6 -1
- package/dist/llm.js.map +1 -1
- package/dist/protocolStream.cjs +20 -4
- package/dist/protocolStream.cjs.map +1 -1
- package/dist/protocolStream.d.cts +15 -2
- package/dist/protocolStream.d.ts +15 -2
- package/dist/protocolStream.js +20 -4
- package/dist/protocolStream.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/catalog.cjs
CHANGED
|
@@ -45,11 +45,14 @@ const invoke = async (name, params = {}) => {
|
|
|
45
45
|
};
|
|
46
46
|
const streamTransport = {
|
|
47
47
|
send: (msg) => (0, import_sandboxUtils.sendMessage)(msg.type, msg),
|
|
48
|
-
subscribe: (type, handler) => (0, import_sandboxUtils.addListener)(type, (msg) => handler(msg))
|
|
48
|
+
subscribe: (type, handler) => (0, import_sandboxUtils.addListener)(type, (msg) => handler(msg)),
|
|
49
|
+
// Early-cancel: route a `{type, msgId, cancel:true}` frame back to the host so it
|
|
50
|
+
// aborts the in-flight generation (and, for `llm:chat`, stops billing) — §3.3.
|
|
51
|
+
cancel: (msg) => (0, import_sandboxUtils.sendMessage)(msg.type, msg)
|
|
49
52
|
};
|
|
50
|
-
function invokeStream(name, params = {}) {
|
|
53
|
+
function invokeStream(name, params = {}, signal) {
|
|
51
54
|
const [scheme, method] = split(name);
|
|
52
|
-
return (0, import_protocolStream.consumeStream)(streamTransport, `protocol-${scheme}`, method, [params]);
|
|
55
|
+
return (0, import_protocolStream.consumeStream)(streamTransport, `protocol-${scheme}`, method, [params], void 0, signal);
|
|
53
56
|
}
|
|
54
57
|
const channel = (0, import_pushChannel.createPushChannel)({
|
|
55
58
|
pushType: "api-catalog",
|
package/dist/catalog.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["// The method catalog (UI_AS_APPS_SPEC §5.5) — the app's own grant-filtered RPC\n// surface, and a generic way to call it. The host advertises exactly the methods\n// this app may invoke (MCP-tool-shaped); `invoke()` calls one by its catalog name.\n// Handing the catalog to an embedded agent as its tool list confines the agent to\n// the app's authority (agent sandboxing falls out of the capability model, §5.9).\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport type { StreamFrame, StreamTransport } from './protocolStream';\nimport { consumeStream } from './protocolStream';\nimport { createPushChannel } from './pushChannel';\n\n/** One advertised method, as the host generated it from its gate table. */\nexport interface ApiMethod {\n /** Catalog name, `protocol-` stripped — e.g. `spaces:share`, `contribute:run`. */\n name: string;\n /** The capability this method requires (already held — it's in your catalog). */\n capability: string;\n /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */\n stream?: boolean;\n /**\n * JSON Schema for the method's single object argument, when the host declares one.\n * Self-describes the call so a catalog-as-tools bridge (an embedded agent) can\n * advertise the real param shape — e.g. that `authoring:typecheck` takes a nested\n * `{ files: [{ path, content }] }` array — instead of a permissive \"any object\".\n * Absent for methods the host advertises without a schema.\n */\n paramsSchema?: Record<string, unknown>;\n}\n\n// `scheme:method` → ['scheme', 'method'] (the wire protocol is `protocol-scheme`).\nconst split = (name: string): [string, string] => {\n const i = name.indexOf(':');\n if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);\n return [name.slice(0, i), name.slice(i + 1)];\n};\n\n/**\n * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.\n * A thin generic over the host protocol: the host validates params and gates the\n * call (an un-granted method → `forbidden`, even if you name it directly). For a\n * STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.\n */\nexport const invoke = async <T = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): Promise<T> => {\n const [scheme, method] = split(name);\n // The host replies with an `{ ok, data } | { ok:false, code }` envelope; unwrap\n // it and THROW on refusal (a `.code` like `forbidden` for an off-catalog call)\n // so callers — and any agent driving `invoke` — see the gate's verdict.\n const res = (await protocolRequest(scheme, method, [params])) as\n | { ok: true; data: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `${name} failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Stream transport over the resolver (SDK_PACKAGING_SPEC §4) — sendMessage /\n// addListener route through `transport()` (injected bundler messageBus or the §4\n// global), never `bundler.messageBus` directly.\nconst streamTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n};\n\n/** Call a STREAMING catalog method by name, yielding its events. */\nexport function invokeStream<T = unknown, R = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): AsyncGenerator<T, R, void> {\n const [scheme, method] = split(name);\n return consumeStream<T, R>(streamTransport, `protocol-${scheme}`, method, [params]);\n}\n\n// The catalog list is read over the transport (§4): the host pushes `api-catalog`\n// and answers `request-api-catalog` with this app's grant-filtered methods (wire\n// format: site-main channelBridge.ts).\nconst channel = createPushChannel<ApiMethod[]>({\n pushType: 'api-catalog',\n requestType: 'request-api-catalog',\n initial: [],\n parse: (msg) => (Array.isArray(msg.methods) ? (msg.methods as ApiMethod[]) : undefined),\n});\n\n/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;\n * use {@link onCatalogChange} / {@link useCatalog} to react. */\nexport const getCatalog = (): ApiMethod[] => channel.get();\n\n/** Subscribe to catalog changes (e.g. a grant added/revoked). Invoked immediately\n * with the current catalog, then on every change. Returns an unsubscribe fn. */\nexport const onCatalogChange = (listener: (catalog: ApiMethod[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning this app's method catalog, re-rendering on change. Hand\n * it to an embedded agent as its tool list to confine the agent to the app's\n * authority (§5.9). */\nexport const useCatalog = (): ApiMethod[] => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,0BAA0D;AAE1D,4BAA8B;AAC9B,yBAAkC;AAqBlC,MAAM,QAAQ,CAAC,SAAmC;AAChD,QAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,MAAI,KAAK,EAAG,OAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAClE,SAAO,CAAC,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAC7C;AAQO,MAAM,SAAS,OACpB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AAInC,QAAM,MAAO,UAAM,qCAAgB,QAAQ,QAAQ,CAAC,MAAM,CAAC;AAI3D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,GAAG,IAAI,SAAS;AACtD,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,kBAAmC;AAAA,EACvC,MAAM,CAAC,YAAQ,iCAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,gBAChB,iCAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;
|
|
1
|
+
{"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["// The method catalog (UI_AS_APPS_SPEC §5.5) — the app's own grant-filtered RPC\n// surface, and a generic way to call it. The host advertises exactly the methods\n// this app may invoke (MCP-tool-shaped); `invoke()` calls one by its catalog name.\n// Handing the catalog to an embedded agent as its tool list confines the agent to\n// the app's authority (agent sandboxing falls out of the capability model, §5.9).\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport type { StreamFrame, StreamTransport } from './protocolStream';\nimport { consumeStream } from './protocolStream';\nimport { createPushChannel } from './pushChannel';\n\n/** One advertised method, as the host generated it from its gate table. */\nexport interface ApiMethod {\n /** Catalog name, `protocol-` stripped — e.g. `spaces:share`, `contribute:run`. */\n name: string;\n /** The capability this method requires (already held — it's in your catalog). */\n capability: string;\n /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */\n stream?: boolean;\n /**\n * JSON Schema for the method's single object argument, when the host declares one.\n * Self-describes the call so a catalog-as-tools bridge (an embedded agent) can\n * advertise the real param shape — e.g. that `authoring:typecheck` takes a nested\n * `{ files: [{ path, content }] }` array — instead of a permissive \"any object\".\n * Absent for methods the host advertises without a schema.\n */\n paramsSchema?: Record<string, unknown>;\n}\n\n// `scheme:method` → ['scheme', 'method'] (the wire protocol is `protocol-scheme`).\nconst split = (name: string): [string, string] => {\n const i = name.indexOf(':');\n if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);\n return [name.slice(0, i), name.slice(i + 1)];\n};\n\n/**\n * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.\n * A thin generic over the host protocol: the host validates params and gates the\n * call (an un-granted method → `forbidden`, even if you name it directly). For a\n * STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.\n */\nexport const invoke = async <T = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): Promise<T> => {\n const [scheme, method] = split(name);\n // The host replies with an `{ ok, data } | { ok:false, code }` envelope; unwrap\n // it and THROW on refusal (a `.code` like `forbidden` for an off-catalog call)\n // so callers — and any agent driving `invoke` — see the gate's verdict.\n const res = (await protocolRequest(scheme, method, [params])) as\n | { ok: true; data: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `${name} failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Stream transport over the resolver (SDK_PACKAGING_SPEC §4) — sendMessage /\n// addListener route through `transport()` (injected bundler messageBus or the §4\n// global), never `bundler.messageBus` directly.\nconst streamTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n // Early-cancel: route a `{type, msgId, cancel:true}` frame back to the host so it\n // aborts the in-flight generation (and, for `llm:chat`, stops billing) — §3.3.\n cancel: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n};\n\n/** Call a STREAMING catalog method by name, yielding its events. Pass `signal` to\n * abort mid-stream: the host stops generating and (for `llm:chat`) stops billing. */\nexport function invokeStream<T = unknown, R = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n signal?: AbortSignal,\n): AsyncGenerator<T, R, void> {\n const [scheme, method] = split(name);\n return consumeStream<T, R>(streamTransport, `protocol-${scheme}`, method, [params], undefined, signal);\n}\n\n// The catalog list is read over the transport (§4): the host pushes `api-catalog`\n// and answers `request-api-catalog` with this app's grant-filtered methods (wire\n// format: site-main channelBridge.ts).\nconst channel = createPushChannel<ApiMethod[]>({\n pushType: 'api-catalog',\n requestType: 'request-api-catalog',\n initial: [],\n parse: (msg) => (Array.isArray(msg.methods) ? (msg.methods as ApiMethod[]) : undefined),\n});\n\n/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;\n * use {@link onCatalogChange} / {@link useCatalog} to react. */\nexport const getCatalog = (): ApiMethod[] => channel.get();\n\n/** Subscribe to catalog changes (e.g. a grant added/revoked). Invoked immediately\n * with the current catalog, then on every change. Returns an unsubscribe fn. */\nexport const onCatalogChange = (listener: (catalog: ApiMethod[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning this app's method catalog, re-rendering on change. Hand\n * it to an embedded agent as its tool list to confine the agent to the app's\n * authority (§5.9). */\nexport const useCatalog = (): ApiMethod[] => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,0BAA0D;AAE1D,4BAA8B;AAC9B,yBAAkC;AAqBlC,MAAM,QAAQ,CAAC,SAAmC;AAChD,QAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,MAAI,KAAK,EAAG,OAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAClE,SAAO,CAAC,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAC7C;AAQO,MAAM,SAAS,OACpB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AAInC,QAAM,MAAO,UAAM,qCAAgB,QAAQ,QAAQ,CAAC,MAAM,CAAC;AAI3D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,GAAG,IAAI,SAAS;AACtD,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,kBAAmC;AAAA,EACvC,MAAM,CAAC,YAAQ,iCAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,gBAChB,iCAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AAAA;AAAA;AAAA,EAGrF,QAAQ,CAAC,YAAQ,iCAAY,IAAI,MAAM,GAAyC;AAClF;AAIO,SAAS,aACd,MACA,SAAkC,CAAC,GACnC,QAC4B;AAC5B,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AACnC,aAAO,qCAAoB,iBAAiB,YAAY,MAAM,IAAI,QAAQ,CAAC,MAAM,GAAG,QAAW,MAAM;AACvG;AAKA,MAAM,cAAU,sCAA+B;AAAA,EAC7C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,OAAO,IAAK,IAAI,UAA0B;AAC/E,CAAC;AAIM,MAAM,aAAa,MAAmB,QAAQ,IAAI;AAIlD,MAAM,kBAAkB,CAAC,aAC9B,QAAQ,SAAS,QAAQ;AAKpB,MAAM,aAAa,MAAmB,QAAQ,IAAI;","names":[]}
|
package/dist/catalog.d.cts
CHANGED
|
@@ -22,8 +22,9 @@ interface ApiMethod {
|
|
|
22
22
|
* STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.
|
|
23
23
|
*/
|
|
24
24
|
declare const invoke: <T = unknown>(name: string, params?: Record<string, unknown>) => Promise<T>;
|
|
25
|
-
/** Call a STREAMING catalog method by name, yielding its events.
|
|
26
|
-
|
|
25
|
+
/** Call a STREAMING catalog method by name, yielding its events. Pass `signal` to
|
|
26
|
+
* abort mid-stream: the host stops generating and (for `llm:chat`) stops billing. */
|
|
27
|
+
declare function invokeStream<T = unknown, R = unknown>(name: string, params?: Record<string, unknown>, signal?: AbortSignal): AsyncGenerator<T, R, void>;
|
|
27
28
|
/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;
|
|
28
29
|
* use {@link onCatalogChange} / {@link useCatalog} to react. */
|
|
29
30
|
declare const getCatalog: () => ApiMethod[];
|
package/dist/catalog.d.ts
CHANGED
|
@@ -22,8 +22,9 @@ interface ApiMethod {
|
|
|
22
22
|
* STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.
|
|
23
23
|
*/
|
|
24
24
|
declare const invoke: <T = unknown>(name: string, params?: Record<string, unknown>) => Promise<T>;
|
|
25
|
-
/** Call a STREAMING catalog method by name, yielding its events.
|
|
26
|
-
|
|
25
|
+
/** Call a STREAMING catalog method by name, yielding its events. Pass `signal` to
|
|
26
|
+
* abort mid-stream: the host stops generating and (for `llm:chat`) stops billing. */
|
|
27
|
+
declare function invokeStream<T = unknown, R = unknown>(name: string, params?: Record<string, unknown>, signal?: AbortSignal): AsyncGenerator<T, R, void>;
|
|
27
28
|
/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;
|
|
28
29
|
* use {@link onCatalogChange} / {@link useCatalog} to react. */
|
|
29
30
|
declare const getCatalog: () => ApiMethod[];
|
package/dist/catalog.js
CHANGED
|
@@ -18,11 +18,14 @@ const invoke = async (name, params = {}) => {
|
|
|
18
18
|
};
|
|
19
19
|
const streamTransport = {
|
|
20
20
|
send: (msg) => sendMessage(msg.type, msg),
|
|
21
|
-
subscribe: (type, handler) => addListener(type, (msg) => handler(msg))
|
|
21
|
+
subscribe: (type, handler) => addListener(type, (msg) => handler(msg)),
|
|
22
|
+
// Early-cancel: route a `{type, msgId, cancel:true}` frame back to the host so it
|
|
23
|
+
// aborts the in-flight generation (and, for `llm:chat`, stops billing) — §3.3.
|
|
24
|
+
cancel: (msg) => sendMessage(msg.type, msg)
|
|
22
25
|
};
|
|
23
|
-
function invokeStream(name, params = {}) {
|
|
26
|
+
function invokeStream(name, params = {}, signal) {
|
|
24
27
|
const [scheme, method] = split(name);
|
|
25
|
-
return consumeStream(streamTransport, `protocol-${scheme}`, method, [params]);
|
|
28
|
+
return consumeStream(streamTransport, `protocol-${scheme}`, method, [params], void 0, signal);
|
|
26
29
|
}
|
|
27
30
|
const channel = createPushChannel({
|
|
28
31
|
pushType: "api-catalog",
|
package/dist/catalog.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["// The method catalog (UI_AS_APPS_SPEC §5.5) — the app's own grant-filtered RPC\n// surface, and a generic way to call it. The host advertises exactly the methods\n// this app may invoke (MCP-tool-shaped); `invoke()` calls one by its catalog name.\n// Handing the catalog to an embedded agent as its tool list confines the agent to\n// the app's authority (agent sandboxing falls out of the capability model, §5.9).\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport type { StreamFrame, StreamTransport } from './protocolStream';\nimport { consumeStream } from './protocolStream';\nimport { createPushChannel } from './pushChannel';\n\n/** One advertised method, as the host generated it from its gate table. */\nexport interface ApiMethod {\n /** Catalog name, `protocol-` stripped — e.g. `spaces:share`, `contribute:run`. */\n name: string;\n /** The capability this method requires (already held — it's in your catalog). */\n capability: string;\n /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */\n stream?: boolean;\n /**\n * JSON Schema for the method's single object argument, when the host declares one.\n * Self-describes the call so a catalog-as-tools bridge (an embedded agent) can\n * advertise the real param shape — e.g. that `authoring:typecheck` takes a nested\n * `{ files: [{ path, content }] }` array — instead of a permissive \"any object\".\n * Absent for methods the host advertises without a schema.\n */\n paramsSchema?: Record<string, unknown>;\n}\n\n// `scheme:method` → ['scheme', 'method'] (the wire protocol is `protocol-scheme`).\nconst split = (name: string): [string, string] => {\n const i = name.indexOf(':');\n if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);\n return [name.slice(0, i), name.slice(i + 1)];\n};\n\n/**\n * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.\n * A thin generic over the host protocol: the host validates params and gates the\n * call (an un-granted method → `forbidden`, even if you name it directly). For a\n * STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.\n */\nexport const invoke = async <T = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): Promise<T> => {\n const [scheme, method] = split(name);\n // The host replies with an `{ ok, data } | { ok:false, code }` envelope; unwrap\n // it and THROW on refusal (a `.code` like `forbidden` for an off-catalog call)\n // so callers — and any agent driving `invoke` — see the gate's verdict.\n const res = (await protocolRequest(scheme, method, [params])) as\n | { ok: true; data: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `${name} failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Stream transport over the resolver (SDK_PACKAGING_SPEC §4) — sendMessage /\n// addListener route through `transport()` (injected bundler messageBus or the §4\n// global), never `bundler.messageBus` directly.\nconst streamTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n};\n\n/** Call a STREAMING catalog method by name, yielding its events. */\nexport function invokeStream<T = unknown, R = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): AsyncGenerator<T, R, void> {\n const [scheme, method] = split(name);\n return consumeStream<T, R>(streamTransport, `protocol-${scheme}`, method, [params]);\n}\n\n// The catalog list is read over the transport (§4): the host pushes `api-catalog`\n// and answers `request-api-catalog` with this app's grant-filtered methods (wire\n// format: site-main channelBridge.ts).\nconst channel = createPushChannel<ApiMethod[]>({\n pushType: 'api-catalog',\n requestType: 'request-api-catalog',\n initial: [],\n parse: (msg) => (Array.isArray(msg.methods) ? (msg.methods as ApiMethod[]) : undefined),\n});\n\n/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;\n * use {@link onCatalogChange} / {@link useCatalog} to react. */\nexport const getCatalog = (): ApiMethod[] => channel.get();\n\n/** Subscribe to catalog changes (e.g. a grant added/revoked). Invoked immediately\n * with the current catalog, then on every change. Returns an unsubscribe fn. */\nexport const onCatalogChange = (listener: (catalog: ApiMethod[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning this app's method catalog, re-rendering on change. Hand\n * it to an embedded agent as its tool list to confine the agent to the app's\n * authority (§5.9). */\nexport const useCatalog = (): ApiMethod[] => channel.use();\n"],"mappings":"AAKA,SAAS,iBAAiB,aAAa,mBAAmB;AAE1D,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAqBlC,MAAM,QAAQ,CAAC,SAAmC;AAChD,QAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,MAAI,KAAK,EAAG,OAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAClE,SAAO,CAAC,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAC7C;AAQO,MAAM,SAAS,OACpB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AAInC,QAAM,MAAO,MAAM,gBAAgB,QAAQ,QAAQ,CAAC,MAAM,CAAC;AAI3D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,GAAG,IAAI,SAAS;AACtD,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,kBAAmC;AAAA,EACvC,MAAM,CAAC,QAAQ,YAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,YAChB,YAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;
|
|
1
|
+
{"version":3,"sources":["../src/catalog.ts"],"sourcesContent":["// The method catalog (UI_AS_APPS_SPEC §5.5) — the app's own grant-filtered RPC\n// surface, and a generic way to call it. The host advertises exactly the methods\n// this app may invoke (MCP-tool-shaped); `invoke()` calls one by its catalog name.\n// Handing the catalog to an embedded agent as its tool list confines the agent to\n// the app's authority (agent sandboxing falls out of the capability model, §5.9).\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport type { StreamFrame, StreamTransport } from './protocolStream';\nimport { consumeStream } from './protocolStream';\nimport { createPushChannel } from './pushChannel';\n\n/** One advertised method, as the host generated it from its gate table. */\nexport interface ApiMethod {\n /** Catalog name, `protocol-` stripped — e.g. `spaces:share`, `contribute:run`. */\n name: string;\n /** The capability this method requires (already held — it's in your catalog). */\n capability: string;\n /** True when the method STREAMS (use {@link invokeStream}) vs. single-reply. */\n stream?: boolean;\n /**\n * JSON Schema for the method's single object argument, when the host declares one.\n * Self-describes the call so a catalog-as-tools bridge (an embedded agent) can\n * advertise the real param shape — e.g. that `authoring:typecheck` takes a nested\n * `{ files: [{ path, content }] }` array — instead of a permissive \"any object\".\n * Absent for methods the host advertises without a schema.\n */\n paramsSchema?: Record<string, unknown>;\n}\n\n// `scheme:method` → ['scheme', 'method'] (the wire protocol is `protocol-scheme`).\nconst split = (name: string): [string, string] => {\n const i = name.indexOf(':');\n if (i <= 0) throw new Error(`invalid catalog method name: ${name}`);\n return [name.slice(0, i), name.slice(i + 1)];\n};\n\n/**\n * Call a catalog method by name — `invoke('spaces:share', { spaceId, login, role })`.\n * A thin generic over the host protocol: the host validates params and gates the\n * call (an un-granted method → `forbidden`, even if you name it directly). For a\n * STREAMING method (`ApiMethod.stream`), use {@link invokeStream}.\n */\nexport const invoke = async <T = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n): Promise<T> => {\n const [scheme, method] = split(name);\n // The host replies with an `{ ok, data } | { ok:false, code }` envelope; unwrap\n // it and THROW on refusal (a `.code` like `forbidden` for an off-catalog call)\n // so callers — and any agent driving `invoke` — see the gate's verdict.\n const res = (await protocolRequest(scheme, method, [params])) as\n | { ok: true; data: unknown }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `${name} failed`) as Error & { code?: string };\n err.code = res?.code ?? 'unknown';\n throw err;\n }\n return res.data as T;\n};\n\n// Stream transport over the resolver (SDK_PACKAGING_SPEC §4) — sendMessage /\n// addListener route through `transport()` (injected bundler messageBus or the §4\n// global), never `bundler.messageBus` directly.\nconst streamTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n // Early-cancel: route a `{type, msgId, cancel:true}` frame back to the host so it\n // aborts the in-flight generation (and, for `llm:chat`, stops billing) — §3.3.\n cancel: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n};\n\n/** Call a STREAMING catalog method by name, yielding its events. Pass `signal` to\n * abort mid-stream: the host stops generating and (for `llm:chat`) stops billing. */\nexport function invokeStream<T = unknown, R = unknown>(\n name: string,\n params: Record<string, unknown> = {},\n signal?: AbortSignal,\n): AsyncGenerator<T, R, void> {\n const [scheme, method] = split(name);\n return consumeStream<T, R>(streamTransport, `protocol-${scheme}`, method, [params], undefined, signal);\n}\n\n// The catalog list is read over the transport (§4): the host pushes `api-catalog`\n// and answers `request-api-catalog` with this app's grant-filtered methods (wire\n// format: site-main channelBridge.ts).\nconst channel = createPushChannel<ApiMethod[]>({\n pushType: 'api-catalog',\n requestType: 'request-api-catalog',\n initial: [],\n parse: (msg) => (Array.isArray(msg.methods) ? (msg.methods as ApiMethod[]) : undefined),\n});\n\n/** The methods this app may call (grant-filtered, §5.5). Poll for a one-off read;\n * use {@link onCatalogChange} / {@link useCatalog} to react. */\nexport const getCatalog = (): ApiMethod[] => channel.get();\n\n/** Subscribe to catalog changes (e.g. a grant added/revoked). Invoked immediately\n * with the current catalog, then on every change. Returns an unsubscribe fn. */\nexport const onCatalogChange = (listener: (catalog: ApiMethod[]) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning this app's method catalog, re-rendering on change. Hand\n * it to an embedded agent as its tool list to confine the agent to the app's\n * authority (§5.9). */\nexport const useCatalog = (): ApiMethod[] => channel.use();\n"],"mappings":"AAKA,SAAS,iBAAiB,aAAa,mBAAmB;AAE1D,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAqBlC,MAAM,QAAQ,CAAC,SAAmC;AAChD,QAAM,IAAI,KAAK,QAAQ,GAAG;AAC1B,MAAI,KAAK,EAAG,OAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAClE,SAAO,CAAC,KAAK,MAAM,GAAG,CAAC,GAAG,KAAK,MAAM,IAAI,CAAC,CAAC;AAC7C;AAQO,MAAM,SAAS,OACpB,MACA,SAAkC,CAAC,MACpB;AACf,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AAInC,QAAM,MAAO,MAAM,gBAAgB,QAAQ,QAAQ,CAAC,MAAM,CAAC;AAI3D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,GAAG,IAAI,SAAS;AACtD,QAAI,OAAO,KAAK,QAAQ;AACxB,UAAM;AAAA,EACR;AACA,SAAO,IAAI;AACb;AAKA,MAAM,kBAAmC;AAAA,EACvC,MAAM,CAAC,QAAQ,YAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,YAChB,YAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AAAA;AAAA;AAAA,EAGrF,QAAQ,CAAC,QAAQ,YAAY,IAAI,MAAM,GAAyC;AAClF;AAIO,SAAS,aACd,MACA,SAAkC,CAAC,GACnC,QAC4B;AAC5B,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI;AACnC,SAAO,cAAoB,iBAAiB,YAAY,MAAM,IAAI,QAAQ,CAAC,MAAM,GAAG,QAAW,MAAM;AACvG;AAKA,MAAM,UAAU,kBAA+B;AAAA,EAC7C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,CAAC;AAAA,EACV,OAAO,CAAC,QAAS,MAAM,QAAQ,IAAI,OAAO,IAAK,IAAI,UAA0B;AAC/E,CAAC;AAIM,MAAM,aAAa,MAAmB,QAAQ,IAAI;AAIlD,MAAM,kBAAkB,CAAC,aAC9B,QAAQ,SAAS,QAAQ;AAKpB,MAAM,aAAa,MAAmB,QAAQ,IAAI;","names":[]}
|
package/dist/llm.cjs
CHANGED
|
@@ -27,7 +27,12 @@ module.exports = __toCommonJS(llm_exports);
|
|
|
27
27
|
var import_catalog = require("./catalog");
|
|
28
28
|
var import_pushChannel = require("./pushChannel");
|
|
29
29
|
function chat(req) {
|
|
30
|
-
|
|
30
|
+
const { signal, ...params } = req;
|
|
31
|
+
return (0, import_catalog.invokeStream)(
|
|
32
|
+
"llm:chat",
|
|
33
|
+
params,
|
|
34
|
+
signal
|
|
35
|
+
);
|
|
31
36
|
}
|
|
32
37
|
const channel = (0, import_pushChannel.createPushChannel)({
|
|
33
38
|
pushType: "llm-provider",
|
package/dist/llm.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n | { type: 'usage'; inputTokens: number; outputTokens: number };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound the host fails the\n * stream into the SP-7 connect-me prompt (the user adds a key) — the generator\n * throws with `code: 'auth-required'`; an un-granted call throws `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n return invokeStream<ChatDelta, ChatResult>('llm:chat'
|
|
1
|
+
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n | { type: 'usage'; inputTokens: number; outputTokens: number };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound the host fails the\n * stream into the SP-7 connect-me prompt (the user adds a key) — the generator\n * throws with `code: 'auth-required'`; an un-granted call throws `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n // Peel `signal` out of the request before it becomes wire params — an AbortSignal\n // can't cross the postMessage boundary as data; it drives the SDK-side cancel frame.\n const { signal, ...params } = req;\n return invokeStream<ChatDelta, ChatResult>(\n 'llm:chat',\n params as unknown as Record<string, unknown>,\n signal,\n );\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n maxContextTokens: number;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n}\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\" (distinct from \"not yet answered\", which keeps the `initial` null).\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: 'llm-provider',\n requestType: 'request-llm-provider',\n initial: null,\n parse: (msg) =>\n 'provider' in msg ? (msg.provider as ChatProviderInfo | null) : undefined,\n});\n\n/** The provider the host resolved for this app (or `null` if none bound). Poll for a\n * one-off read; use {@link onChatProviderChange}/{@link useChatProvider} to react. */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (\n listener: (provider: ChatProviderInfo | null) => void,\n): (() => void) => channel.onChange(listener);\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,qBAA6B;AAC7B,yBAAkC;AAiF3B,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,aAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AA0BA,MAAM,cAAU,sCAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QACN,cAAc,MAAO,IAAI,WAAuC;AACpE,CAAC;AAIM,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAIhE,MAAM,uBAAuB,CAClC,aACiB,QAAQ,SAAS,QAAQ;AAIrC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;","names":[]}
|
package/dist/llm.d.cts
CHANGED
|
@@ -45,6 +45,12 @@ interface ChatRequest {
|
|
|
45
45
|
/** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete
|
|
46
46
|
* model on the resolved provider. Omit to take the provider's default. */
|
|
47
47
|
modelHint?: 'fast' | 'smart';
|
|
48
|
+
/** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel
|
|
49
|
+
* frame so the host aborts the upstream provider request and STOPS BILLING the
|
|
50
|
+
* user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3
|
|
51
|
+
* "abort the in-flight LLM request", R3-224). Not sent over the wire (an
|
|
52
|
+
* `AbortSignal` isn't serializable); handled SDK-side. */
|
|
53
|
+
signal?: AbortSignal;
|
|
48
54
|
}
|
|
49
55
|
/** One streamed chunk. Consumers typically accumulate `text-delta`s. */
|
|
50
56
|
type ChatDelta = {
|
package/dist/llm.d.ts
CHANGED
|
@@ -45,6 +45,12 @@ interface ChatRequest {
|
|
|
45
45
|
/** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete
|
|
46
46
|
* model on the resolved provider. Omit to take the provider's default. */
|
|
47
47
|
modelHint?: 'fast' | 'smart';
|
|
48
|
+
/** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel
|
|
49
|
+
* frame so the host aborts the upstream provider request and STOPS BILLING the
|
|
50
|
+
* user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3
|
|
51
|
+
* "abort the in-flight LLM request", R3-224). Not sent over the wire (an
|
|
52
|
+
* `AbortSignal` isn't serializable); handled SDK-side. */
|
|
53
|
+
signal?: AbortSignal;
|
|
48
54
|
}
|
|
49
55
|
/** One streamed chunk. Consumers typically accumulate `text-delta`s. */
|
|
50
56
|
type ChatDelta = {
|
package/dist/llm.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { invokeStream } from "./catalog";
|
|
2
2
|
import { createPushChannel } from "./pushChannel";
|
|
3
3
|
function chat(req) {
|
|
4
|
-
|
|
4
|
+
const { signal, ...params } = req;
|
|
5
|
+
return invokeStream(
|
|
6
|
+
"llm:chat",
|
|
7
|
+
params,
|
|
8
|
+
signal
|
|
9
|
+
);
|
|
5
10
|
}
|
|
6
11
|
const channel = createPushChannel({
|
|
7
12
|
pushType: "llm-provider",
|
package/dist/llm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n | { type: 'usage'; inputTokens: number; outputTokens: number };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound the host fails the\n * stream into the SP-7 connect-me prompt (the user adds a key) — the generator\n * throws with `code: 'auth-required'`; an un-granted call throws `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n return invokeStream<ChatDelta, ChatResult>('llm:chat'
|
|
1
|
+
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n | { type: 'usage'; inputTokens: number; outputTokens: number };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound the host fails the\n * stream into the SP-7 connect-me prompt (the user adds a key) — the generator\n * throws with `code: 'auth-required'`; an un-granted call throws `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n // Peel `signal` out of the request before it becomes wire params — an AbortSignal\n // can't cross the postMessage boundary as data; it drives the SDK-side cancel frame.\n const { signal, ...params } = req;\n return invokeStream<ChatDelta, ChatResult>(\n 'llm:chat',\n params as unknown as Record<string, unknown>,\n signal,\n );\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n maxContextTokens: number;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n}\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\" (distinct from \"not yet answered\", which keeps the `initial` null).\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: 'llm-provider',\n requestType: 'request-llm-provider',\n initial: null,\n parse: (msg) =>\n 'provider' in msg ? (msg.provider as ChatProviderInfo | null) : undefined,\n});\n\n/** The provider the host resolved for this app (or `null` if none bound). Poll for a\n * one-off read; use {@link onChatProviderChange}/{@link useChatProvider} to react. */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (\n listener: (provider: ChatProviderInfo | null) => void,\n): (() => void) => channel.onChange(listener);\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n"],"mappings":"AAeA,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAiF3B,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AA0BA,MAAM,UAAU,kBAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QACN,cAAc,MAAO,IAAI,WAAuC;AACpE,CAAC;AAIM,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAIhE,MAAM,uBAAuB,CAClC,aACiB,QAAQ,SAAS,QAAQ;AAIrC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;","names":[]}
|
package/dist/protocolStream.cjs
CHANGED
|
@@ -36,9 +36,11 @@ const nextMsgId = () => {
|
|
|
36
36
|
streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
37
37
|
return streamCounter;
|
|
38
38
|
};
|
|
39
|
-
async function* consumeStream(transport, type, method, params, msgId = nextMsgId()) {
|
|
39
|
+
async function* consumeStream(transport, type, method, params, msgId = nextMsgId(), signal) {
|
|
40
40
|
const queue = [];
|
|
41
41
|
let wake = null;
|
|
42
|
+
let settled = false;
|
|
43
|
+
let started = false;
|
|
42
44
|
const push = (frame) => {
|
|
43
45
|
queue.push(frame);
|
|
44
46
|
const w = wake;
|
|
@@ -49,9 +51,18 @@ async function* consumeStream(transport, type, method, params, msgId = nextMsgId
|
|
|
49
51
|
if (msg.msgId !== msgId || !msg.stream) return;
|
|
50
52
|
push(msg.stream);
|
|
51
53
|
});
|
|
54
|
+
const onAbort = () => {
|
|
55
|
+
const w = wake;
|
|
56
|
+
wake = null;
|
|
57
|
+
w?.();
|
|
58
|
+
};
|
|
59
|
+
if (signal) signal.addEventListener("abort", onAbort);
|
|
52
60
|
try {
|
|
61
|
+
if (signal?.aborted) throw new StreamError("aborted", "stream aborted before start");
|
|
53
62
|
transport.send({ type, method, params, msgId, stream: true });
|
|
63
|
+
started = true;
|
|
54
64
|
while (true) {
|
|
65
|
+
if (signal?.aborted) throw new StreamError("aborted", "stream aborted");
|
|
55
66
|
if (queue.length === 0) {
|
|
56
67
|
await new Promise((resolve) => {
|
|
57
68
|
wake = resolve;
|
|
@@ -62,21 +73,26 @@ async function* consumeStream(transport, type, method, params, msgId = nextMsgId
|
|
|
62
73
|
if (frame.kind === "event") {
|
|
63
74
|
yield frame.value;
|
|
64
75
|
} else if (frame.kind === "done") {
|
|
76
|
+
settled = true;
|
|
65
77
|
return frame.value;
|
|
66
78
|
} else {
|
|
79
|
+
settled = true;
|
|
67
80
|
throw new StreamError(frame.code, frame.message);
|
|
68
81
|
}
|
|
69
82
|
}
|
|
70
83
|
} finally {
|
|
71
84
|
unsubscribe();
|
|
85
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
86
|
+
if (started && !settled) transport.cancel?.({ type, msgId, cancel: true });
|
|
72
87
|
}
|
|
73
88
|
}
|
|
74
89
|
const bundlerTransport = {
|
|
75
90
|
send: (msg) => (0, import_sandboxUtils.sendMessage)(msg.type, msg),
|
|
76
|
-
subscribe: (type, handler) => (0, import_sandboxUtils.addListener)(type, (msg) => handler(msg))
|
|
91
|
+
subscribe: (type, handler) => (0, import_sandboxUtils.addListener)(type, (msg) => handler(msg)),
|
|
92
|
+
cancel: (msg) => (0, import_sandboxUtils.sendMessage)(msg.type, msg)
|
|
77
93
|
};
|
|
78
|
-
function protocolStream(protocolName, method, params) {
|
|
79
|
-
return consumeStream(bundlerTransport, protocolName, method, params);
|
|
94
|
+
function protocolStream(protocolName, method, params, signal) {
|
|
95
|
+
return consumeStream(bundlerTransport, protocolName, method, params, void 0, signal);
|
|
80
96
|
}
|
|
81
97
|
// Annotate the CommonJS export names for ESM import in node:
|
|
82
98
|
0 && (module.exports = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\n\n/** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\n/** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */\nexport interface StreamTransport {\n // Fire the request that starts the stream. The host replies with frames tagged\n // by the same `msgId`.\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n // Subscribe to inbound frames for `type`; returns an unsubscribe.\n subscribe: (\n type: string,\n handler: (msg: { msgId?: number; stream?: StreamFrame }) => void\n ) => () => void;\n}\n\n/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */\nexport class StreamError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = 'StreamError';\n this.code = code;\n }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n // reply (which has `result`, not `stream`) can't be misread.\n streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n transport: StreamTransport,\n type: string,\n method: string,\n params: unknown[],\n msgId: number = nextMsgId()\n): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\n const push = (frame: StreamFrame) => {\n queue.push(frame);\n const w = wake;\n wake = null;\n w?.();\n };\n\n const unsubscribe = transport.subscribe(type, (msg) => {\n if (msg.msgId !== msgId || !msg.stream) return;\n push(msg.stream);\n });\n\n try {\n transport.send({ type, method, params, msgId, stream: true });\n while (true) {\n if (queue.length === 0) {\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n continue;\n }\n const frame = queue.shift() as StreamFrame;\n if (frame.kind === 'event') {\n yield frame.value as T;\n } else if (frame.kind === 'done') {\n return frame.value as R;\n } else {\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers.\nconst bundlerTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n};\n\n/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[]\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(bundlerTransport, protocolName, method, params);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,0BAAyC;
|
|
1
|
+
{"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\n\n/** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\n/** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */\nexport interface StreamTransport {\n // Fire the request that starts the stream. The host replies with frames tagged\n // by the same `msgId`.\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n // Subscribe to inbound frames for `type`; returns an unsubscribe.\n subscribe: (\n type: string,\n handler: (msg: { msgId?: number; stream?: StreamFrame }) => void\n ) => () => void;\n // Tell the host to STOP the stream early — abort the in-flight generation (and,\n // for `llm:chat`, the upstream provider fetch so it stops BILLING). Sent when the\n // consumer bails before a terminal frame: an early `break`/`return` out of the\n // `for await`, or an `AbortSignal` firing. Carries the same `(type, msgId)` the\n // host tagged its frames with, so the host aborts the matching generator\n // (LLM_AND_AGENTS_SPEC §3.3 \"abort the in-flight LLM request\"). Optional — a\n // transport predating the cancel frame simply omits it and the old behavior\n // (host runs to completion) stands.\n cancel?: (msg: { type: string; msgId: number; cancel: true }) => void;\n}\n\n/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */\nexport class StreamError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = 'StreamError';\n this.code = code;\n }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n // reply (which has `result`, not `stream`) can't be misread.\n streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n *\n * `signal` wires a caller {@link AbortSignal} to mid-stream cancellation: when it\n * fires (or the consumer `break`s before a terminal frame), the `finally` sends a\n * `cancel` frame back over `transport.cancel` so the HOST stops generating — without\n * it, aborting only stops the app-side iterator while the upstream provider keeps\n * streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n transport: StreamTransport,\n type: string,\n method: string,\n params: unknown[],\n msgId: number = nextMsgId(),\n signal?: AbortSignal\n): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\n // True once a terminal (`done`/`error`) frame arrived — so the `finally` knows the\n // host already stopped and must NOT send a redundant cancel. Left false when the\n // consumer bails early or the signal aborts (the two cases that DO need a cancel).\n let settled = false;\n // True once the request frame went out. A cancel is only meaningful for a stream\n // the host actually started — an abort BEFORE `send` sends nothing to cancel.\n let started = false;\n const push = (frame: StreamFrame) => {\n queue.push(frame);\n const w = wake;\n wake = null;\n w?.();\n };\n\n const unsubscribe = transport.subscribe(type, (msg) => {\n if (msg.msgId !== msgId || !msg.stream) return;\n push(msg.stream);\n });\n\n // An abort wakes the pull loop out of its idle `await`; the loop then throws.\n const onAbort = () => {\n const w = wake;\n wake = null;\n w?.();\n };\n if (signal) signal.addEventListener('abort', onAbort);\n\n try {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted before start');\n transport.send({ type, method, params, msgId, stream: true });\n started = true;\n while (true) {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted');\n if (queue.length === 0) {\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n continue;\n }\n const frame = queue.shift() as StreamFrame;\n if (frame.kind === 'event') {\n yield frame.value as T;\n } else if (frame.kind === 'done') {\n settled = true;\n return frame.value as R;\n } else {\n settled = true;\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n if (signal) signal.removeEventListener('abort', onAbort);\n // Consumer stopped pulling before a terminal frame (early break/return, or the\n // signal aborted): tell the host to stop the in-flight generation + billing.\n if (started && !settled) transport.cancel?.({ type, msgId, cancel: true });\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers. `cancel`\n// rides the same messageBus as `send` — a `{type, msgId, cancel:true}` frame the\n// host dispatcher routes to the in-flight generator's AbortController.\nconst bundlerTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n cancel: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n};\n\n/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n *\n * Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.\n */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[],\n signal?: AbortSignal\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(bundlerTransport, protocolName, method, params, undefined, signal);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,0BAAyC;AA8BlC,MAAM,oBAAoB,MAAM;AAAA,EAErC,YAAY,MAAc,SAAiB;AACzC,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAI,gBAAgB;AACpB,MAAM,YAAY,MAAc;AAI9B,mBAAiB,gBAAgB,KAAK,OAAO;AAC7C,SAAO;AACT;AAeA,gBAAuB,cACrB,WACA,MACA,QACA,QACA,QAAgB,UAAU,GAC1B,QAC4B;AAC5B,QAAM,QAAuB,CAAC;AAC9B,MAAI,OAA4B;AAIhC,MAAI,UAAU;AAGd,MAAI,UAAU;AACd,QAAM,OAAO,CAAC,UAAuB;AACnC,UAAM,KAAK,KAAK;AAChB,UAAM,IAAI;AACV,WAAO;AACP,QAAI;AAAA,EACN;AAEA,QAAM,cAAc,UAAU,UAAU,MAAM,CAAC,QAAQ;AACrD,QAAI,IAAI,UAAU,SAAS,CAAC,IAAI,OAAQ;AACxC,SAAK,IAAI,MAAM;AAAA,EACjB,CAAC;AAGD,QAAM,UAAU,MAAM;AACpB,UAAM,IAAI;AACV,WAAO;AACP,QAAI;AAAA,EACN;AACA,MAAI,OAAQ,QAAO,iBAAiB,SAAS,OAAO;AAEpD,MAAI;AACF,QAAI,QAAQ,QAAS,OAAM,IAAI,YAAY,WAAW,6BAA6B;AACnF,cAAU,KAAK,EAAE,MAAM,QAAQ,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAC5D,cAAU;AACV,WAAO,MAAM;AACX,UAAI,QAAQ,QAAS,OAAM,IAAI,YAAY,WAAW,gBAAgB;AACtE,UAAI,MAAM,WAAW,GAAG;AACtB,cAAM,IAAI,QAAc,CAAC,YAAY;AACnC,iBAAO;AAAA,QACT,CAAC;AACD;AAAA,MACF;AACA,YAAM,QAAQ,MAAM,MAAM;AAC1B,UAAI,MAAM,SAAS,SAAS;AAC1B,cAAM,MAAM;AAAA,MACd,WAAW,MAAM,SAAS,QAAQ;AAChC,kBAAU;AACV,eAAO,MAAM;AAAA,MACf,OAAO;AACL,kBAAU;AACV,cAAM,IAAI,YAAY,MAAM,MAAM,MAAM,OAAO;AAAA,MACjD;AAAA,IACF;AAAA,EACF,UAAE;AACA,gBAAY;AACZ,QAAI,OAAQ,QAAO,oBAAoB,SAAS,OAAO;AAGvD,QAAI,WAAW,CAAC,QAAS,WAAU,SAAS,EAAE,MAAM,OAAO,QAAQ,KAAK,CAAC;AAAA,EAC3E;AACF;AAKA,MAAM,mBAAoC;AAAA,EACxC,MAAM,CAAC,YAAQ,iCAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,gBAChB,iCAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AAAA,EACrF,QAAQ,CAAC,YAAQ,iCAAY,IAAI,MAAM,GAAyC;AAClF;AASO,SAAS,eACd,cACA,QACA,QACA,QAC4B;AAC5B,SAAO,cAAoB,kBAAkB,cAAc,QAAQ,QAAQ,QAAW,MAAM;AAC9F;","names":[]}
|
|
@@ -23,6 +23,11 @@ interface StreamTransport {
|
|
|
23
23
|
msgId?: number;
|
|
24
24
|
stream?: StreamFrame;
|
|
25
25
|
}) => void) => () => void;
|
|
26
|
+
cancel?: (msg: {
|
|
27
|
+
type: string;
|
|
28
|
+
msgId: number;
|
|
29
|
+
cancel: true;
|
|
30
|
+
}) => void;
|
|
26
31
|
}
|
|
27
32
|
/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */
|
|
28
33
|
declare class StreamError extends Error {
|
|
@@ -35,13 +40,21 @@ declare class StreamError extends Error {
|
|
|
35
40
|
* Yields each event value; returns the `done` value; throws `StreamError` on an
|
|
36
41
|
* error frame. Always unsubscribes (via the generator's `finally`) so an early
|
|
37
42
|
* `break` in the consumer doesn't leak the listener.
|
|
43
|
+
*
|
|
44
|
+
* `signal` wires a caller {@link AbortSignal} to mid-stream cancellation: when it
|
|
45
|
+
* fires (or the consumer `break`s before a terminal frame), the `finally` sends a
|
|
46
|
+
* `cancel` frame back over `transport.cancel` so the HOST stops generating — without
|
|
47
|
+
* it, aborting only stops the app-side iterator while the upstream provider keeps
|
|
48
|
+
* streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).
|
|
38
49
|
*/
|
|
39
|
-
declare function consumeStream<T = unknown, R = unknown>(transport: StreamTransport, type: string, method: string, params: unknown[], msgId?: number): AsyncGenerator<T, R, void>;
|
|
50
|
+
declare function consumeStream<T = unknown, R = unknown>(transport: StreamTransport, type: string, method: string, params: unknown[], msgId?: number, signal?: AbortSignal): AsyncGenerator<T, R, void>;
|
|
40
51
|
/**
|
|
41
52
|
* Consume an elevated streaming protocol method from app code.
|
|
42
53
|
*
|
|
43
54
|
* `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`
|
|
55
|
+
*
|
|
56
|
+
* Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.
|
|
44
57
|
*/
|
|
45
|
-
declare function protocolStream<T = unknown, R = unknown>(protocolName: string, method: string, params: unknown[]): AsyncGenerator<T, R, void>;
|
|
58
|
+
declare function protocolStream<T = unknown, R = unknown>(protocolName: string, method: string, params: unknown[], signal?: AbortSignal): AsyncGenerator<T, R, void>;
|
|
46
59
|
|
|
47
60
|
export { StreamError, type StreamFrame, type StreamTransport, consumeStream, protocolStream };
|
package/dist/protocolStream.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ interface StreamTransport {
|
|
|
23
23
|
msgId?: number;
|
|
24
24
|
stream?: StreamFrame;
|
|
25
25
|
}) => void) => () => void;
|
|
26
|
+
cancel?: (msg: {
|
|
27
|
+
type: string;
|
|
28
|
+
msgId: number;
|
|
29
|
+
cancel: true;
|
|
30
|
+
}) => void;
|
|
26
31
|
}
|
|
27
32
|
/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */
|
|
28
33
|
declare class StreamError extends Error {
|
|
@@ -35,13 +40,21 @@ declare class StreamError extends Error {
|
|
|
35
40
|
* Yields each event value; returns the `done` value; throws `StreamError` on an
|
|
36
41
|
* error frame. Always unsubscribes (via the generator's `finally`) so an early
|
|
37
42
|
* `break` in the consumer doesn't leak the listener.
|
|
43
|
+
*
|
|
44
|
+
* `signal` wires a caller {@link AbortSignal} to mid-stream cancellation: when it
|
|
45
|
+
* fires (or the consumer `break`s before a terminal frame), the `finally` sends a
|
|
46
|
+
* `cancel` frame back over `transport.cancel` so the HOST stops generating — without
|
|
47
|
+
* it, aborting only stops the app-side iterator while the upstream provider keeps
|
|
48
|
+
* streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).
|
|
38
49
|
*/
|
|
39
|
-
declare function consumeStream<T = unknown, R = unknown>(transport: StreamTransport, type: string, method: string, params: unknown[], msgId?: number): AsyncGenerator<T, R, void>;
|
|
50
|
+
declare function consumeStream<T = unknown, R = unknown>(transport: StreamTransport, type: string, method: string, params: unknown[], msgId?: number, signal?: AbortSignal): AsyncGenerator<T, R, void>;
|
|
40
51
|
/**
|
|
41
52
|
* Consume an elevated streaming protocol method from app code.
|
|
42
53
|
*
|
|
43
54
|
* `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`
|
|
55
|
+
*
|
|
56
|
+
* Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.
|
|
44
57
|
*/
|
|
45
|
-
declare function protocolStream<T = unknown, R = unknown>(protocolName: string, method: string, params: unknown[]): AsyncGenerator<T, R, void>;
|
|
58
|
+
declare function protocolStream<T = unknown, R = unknown>(protocolName: string, method: string, params: unknown[], signal?: AbortSignal): AsyncGenerator<T, R, void>;
|
|
46
59
|
|
|
47
60
|
export { StreamError, type StreamFrame, type StreamTransport, consumeStream, protocolStream };
|
package/dist/protocolStream.js
CHANGED
|
@@ -11,9 +11,11 @@ const nextMsgId = () => {
|
|
|
11
11
|
streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
12
12
|
return streamCounter;
|
|
13
13
|
};
|
|
14
|
-
async function* consumeStream(transport, type, method, params, msgId = nextMsgId()) {
|
|
14
|
+
async function* consumeStream(transport, type, method, params, msgId = nextMsgId(), signal) {
|
|
15
15
|
const queue = [];
|
|
16
16
|
let wake = null;
|
|
17
|
+
let settled = false;
|
|
18
|
+
let started = false;
|
|
17
19
|
const push = (frame) => {
|
|
18
20
|
queue.push(frame);
|
|
19
21
|
const w = wake;
|
|
@@ -24,9 +26,18 @@ async function* consumeStream(transport, type, method, params, msgId = nextMsgId
|
|
|
24
26
|
if (msg.msgId !== msgId || !msg.stream) return;
|
|
25
27
|
push(msg.stream);
|
|
26
28
|
});
|
|
29
|
+
const onAbort = () => {
|
|
30
|
+
const w = wake;
|
|
31
|
+
wake = null;
|
|
32
|
+
w?.();
|
|
33
|
+
};
|
|
34
|
+
if (signal) signal.addEventListener("abort", onAbort);
|
|
27
35
|
try {
|
|
36
|
+
if (signal?.aborted) throw new StreamError("aborted", "stream aborted before start");
|
|
28
37
|
transport.send({ type, method, params, msgId, stream: true });
|
|
38
|
+
started = true;
|
|
29
39
|
while (true) {
|
|
40
|
+
if (signal?.aborted) throw new StreamError("aborted", "stream aborted");
|
|
30
41
|
if (queue.length === 0) {
|
|
31
42
|
await new Promise((resolve) => {
|
|
32
43
|
wake = resolve;
|
|
@@ -37,21 +48,26 @@ async function* consumeStream(transport, type, method, params, msgId = nextMsgId
|
|
|
37
48
|
if (frame.kind === "event") {
|
|
38
49
|
yield frame.value;
|
|
39
50
|
} else if (frame.kind === "done") {
|
|
51
|
+
settled = true;
|
|
40
52
|
return frame.value;
|
|
41
53
|
} else {
|
|
54
|
+
settled = true;
|
|
42
55
|
throw new StreamError(frame.code, frame.message);
|
|
43
56
|
}
|
|
44
57
|
}
|
|
45
58
|
} finally {
|
|
46
59
|
unsubscribe();
|
|
60
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
61
|
+
if (started && !settled) transport.cancel?.({ type, msgId, cancel: true });
|
|
47
62
|
}
|
|
48
63
|
}
|
|
49
64
|
const bundlerTransport = {
|
|
50
65
|
send: (msg) => sendMessage(msg.type, msg),
|
|
51
|
-
subscribe: (type, handler) => addListener(type, (msg) => handler(msg))
|
|
66
|
+
subscribe: (type, handler) => addListener(type, (msg) => handler(msg)),
|
|
67
|
+
cancel: (msg) => sendMessage(msg.type, msg)
|
|
52
68
|
};
|
|
53
|
-
function protocolStream(protocolName, method, params) {
|
|
54
|
-
return consumeStream(bundlerTransport, protocolName, method, params);
|
|
69
|
+
function protocolStream(protocolName, method, params, signal) {
|
|
70
|
+
return consumeStream(bundlerTransport, protocolName, method, params, void 0, signal);
|
|
55
71
|
}
|
|
56
72
|
export {
|
|
57
73
|
StreamError,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\n\n/** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\n/** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */\nexport interface StreamTransport {\n // Fire the request that starts the stream. The host replies with frames tagged\n // by the same `msgId`.\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n // Subscribe to inbound frames for `type`; returns an unsubscribe.\n subscribe: (\n type: string,\n handler: (msg: { msgId?: number; stream?: StreamFrame }) => void\n ) => () => void;\n}\n\n/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */\nexport class StreamError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = 'StreamError';\n this.code = code;\n }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n // reply (which has `result`, not `stream`) can't be misread.\n streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n transport: StreamTransport,\n type: string,\n method: string,\n params: unknown[],\n msgId: number = nextMsgId()\n): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\n const push = (frame: StreamFrame) => {\n queue.push(frame);\n const w = wake;\n wake = null;\n w?.();\n };\n\n const unsubscribe = transport.subscribe(type, (msg) => {\n if (msg.msgId !== msgId || !msg.stream) return;\n push(msg.stream);\n });\n\n try {\n transport.send({ type, method, params, msgId, stream: true });\n while (true) {\n if (queue.length === 0) {\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n continue;\n }\n const frame = queue.shift() as StreamFrame;\n if (frame.kind === 'event') {\n yield frame.value as T;\n } else if (frame.kind === 'done') {\n return frame.value as R;\n } else {\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers.\nconst bundlerTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n};\n\n/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[]\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(bundlerTransport, protocolName, method, params);\n}\n"],"mappings":"AAUA,SAAS,aAAa,mBAAmB;
|
|
1
|
+
{"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\n\n/** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\n/** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */\nexport interface StreamTransport {\n // Fire the request that starts the stream. The host replies with frames tagged\n // by the same `msgId`.\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n // Subscribe to inbound frames for `type`; returns an unsubscribe.\n subscribe: (\n type: string,\n handler: (msg: { msgId?: number; stream?: StreamFrame }) => void\n ) => () => void;\n // Tell the host to STOP the stream early — abort the in-flight generation (and,\n // for `llm:chat`, the upstream provider fetch so it stops BILLING). Sent when the\n // consumer bails before a terminal frame: an early `break`/`return` out of the\n // `for await`, or an `AbortSignal` firing. Carries the same `(type, msgId)` the\n // host tagged its frames with, so the host aborts the matching generator\n // (LLM_AND_AGENTS_SPEC §3.3 \"abort the in-flight LLM request\"). Optional — a\n // transport predating the cancel frame simply omits it and the old behavior\n // (host runs to completion) stands.\n cancel?: (msg: { type: string; msgId: number; cancel: true }) => void;\n}\n\n/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */\nexport class StreamError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = 'StreamError';\n this.code = code;\n }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n // reply (which has `result`, not `stream`) can't be misread.\n streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n *\n * `signal` wires a caller {@link AbortSignal} to mid-stream cancellation: when it\n * fires (or the consumer `break`s before a terminal frame), the `finally` sends a\n * `cancel` frame back over `transport.cancel` so the HOST stops generating — without\n * it, aborting only stops the app-side iterator while the upstream provider keeps\n * streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n transport: StreamTransport,\n type: string,\n method: string,\n params: unknown[],\n msgId: number = nextMsgId(),\n signal?: AbortSignal\n): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\n // True once a terminal (`done`/`error`) frame arrived — so the `finally` knows the\n // host already stopped and must NOT send a redundant cancel. Left false when the\n // consumer bails early or the signal aborts (the two cases that DO need a cancel).\n let settled = false;\n // True once the request frame went out. A cancel is only meaningful for a stream\n // the host actually started — an abort BEFORE `send` sends nothing to cancel.\n let started = false;\n const push = (frame: StreamFrame) => {\n queue.push(frame);\n const w = wake;\n wake = null;\n w?.();\n };\n\n const unsubscribe = transport.subscribe(type, (msg) => {\n if (msg.msgId !== msgId || !msg.stream) return;\n push(msg.stream);\n });\n\n // An abort wakes the pull loop out of its idle `await`; the loop then throws.\n const onAbort = () => {\n const w = wake;\n wake = null;\n w?.();\n };\n if (signal) signal.addEventListener('abort', onAbort);\n\n try {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted before start');\n transport.send({ type, method, params, msgId, stream: true });\n started = true;\n while (true) {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted');\n if (queue.length === 0) {\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n continue;\n }\n const frame = queue.shift() as StreamFrame;\n if (frame.kind === 'event') {\n yield frame.value as T;\n } else if (frame.kind === 'done') {\n settled = true;\n return frame.value as R;\n } else {\n settled = true;\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n if (signal) signal.removeEventListener('abort', onAbort);\n // Consumer stopped pulling before a terminal frame (early break/return, or the\n // signal aborted): tell the host to stop the in-flight generation + billing.\n if (started && !settled) transport.cancel?.({ type, msgId, cancel: true });\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers. `cancel`\n// rides the same messageBus as `send` — a `{type, msgId, cancel:true}` frame the\n// host dispatcher routes to the in-flight generator's AbortController.\nconst bundlerTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n cancel: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n};\n\n/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n *\n * Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.\n */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[],\n signal?: AbortSignal\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(bundlerTransport, protocolName, method, params, undefined, signal);\n}\n"],"mappings":"AAUA,SAAS,aAAa,mBAAmB;AA8BlC,MAAM,oBAAoB,MAAM;AAAA,EAErC,YAAY,MAAc,SAAiB;AACzC,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAI,gBAAgB;AACpB,MAAM,YAAY,MAAc;AAI9B,mBAAiB,gBAAgB,KAAK,OAAO;AAC7C,SAAO;AACT;AAeA,gBAAuB,cACrB,WACA,MACA,QACA,QACA,QAAgB,UAAU,GAC1B,QAC4B;AAC5B,QAAM,QAAuB,CAAC;AAC9B,MAAI,OAA4B;AAIhC,MAAI,UAAU;AAGd,MAAI,UAAU;AACd,QAAM,OAAO,CAAC,UAAuB;AACnC,UAAM,KAAK,KAAK;AAChB,UAAM,IAAI;AACV,WAAO;AACP,QAAI;AAAA,EACN;AAEA,QAAM,cAAc,UAAU,UAAU,MAAM,CAAC,QAAQ;AACrD,QAAI,IAAI,UAAU,SAAS,CAAC,IAAI,OAAQ;AACxC,SAAK,IAAI,MAAM;AAAA,EACjB,CAAC;AAGD,QAAM,UAAU,MAAM;AACpB,UAAM,IAAI;AACV,WAAO;AACP,QAAI;AAAA,EACN;AACA,MAAI,OAAQ,QAAO,iBAAiB,SAAS,OAAO;AAEpD,MAAI;AACF,QAAI,QAAQ,QAAS,OAAM,IAAI,YAAY,WAAW,6BAA6B;AACnF,cAAU,KAAK,EAAE,MAAM,QAAQ,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAC5D,cAAU;AACV,WAAO,MAAM;AACX,UAAI,QAAQ,QAAS,OAAM,IAAI,YAAY,WAAW,gBAAgB;AACtE,UAAI,MAAM,WAAW,GAAG;AACtB,cAAM,IAAI,QAAc,CAAC,YAAY;AACnC,iBAAO;AAAA,QACT,CAAC;AACD;AAAA,MACF;AACA,YAAM,QAAQ,MAAM,MAAM;AAC1B,UAAI,MAAM,SAAS,SAAS;AAC1B,cAAM,MAAM;AAAA,MACd,WAAW,MAAM,SAAS,QAAQ;AAChC,kBAAU;AACV,eAAO,MAAM;AAAA,MACf,OAAO;AACL,kBAAU;AACV,cAAM,IAAI,YAAY,MAAM,MAAM,MAAM,OAAO;AAAA,MACjD;AAAA,IACF;AAAA,EACF,UAAE;AACA,gBAAY;AACZ,QAAI,OAAQ,QAAO,oBAAoB,SAAS,OAAO;AAGvD,QAAI,WAAW,CAAC,QAAS,WAAU,SAAS,EAAE,MAAM,OAAO,QAAQ,KAAK,CAAC;AAAA,EAC3E;AACF;AAKA,MAAM,mBAAoC;AAAA,EACxC,MAAM,CAAC,QAAQ,YAAY,IAAI,MAAM,GAAyC;AAAA,EAC9E,WAAW,CAAC,MAAM,YAChB,YAAY,MAAM,CAAC,QAAQ,QAAQ,GAA+C,CAAC;AAAA,EACrF,QAAQ,CAAC,QAAQ,YAAY,IAAI,MAAM,GAAyC;AAClF;AASO,SAAS,eACd,cACA,QACA,QACA,QAC4B;AAC5B,SAAO,cAAoB,kBAAkB,cAAc,QAAQ,QAAQ,QAAW,MAAM;AAC9F;","names":[]}
|
package/dist/version.cjs
CHANGED
|
@@ -21,7 +21,7 @@ __export(version_exports, {
|
|
|
21
21
|
SDK_VERSION: () => SDK_VERSION
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(version_exports);
|
|
24
|
-
const SDK_VERSION = "0.
|
|
24
|
+
const SDK_VERSION = "0.32.0";
|
|
25
25
|
// Annotate the CommonJS export names for ESM import in node:
|
|
26
26
|
0 && (module.exports = {
|
|
27
27
|
SDK_VERSION
|
package/dist/version.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.32.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
|
package/dist/version.d.cts
CHANGED
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.
|
|
1
|
+
{"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.32.0';\n"],"mappings":"AAIO,MAAM,cAAc;","names":[]}
|
package/package.json
CHANGED