@immediately-run/sdk 0.46.0 → 0.48.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/MDXProvider.cjs +1 -5
- package/dist/MDXProvider.cjs.map +1 -1
- package/dist/MDXProvider.js +1 -5
- package/dist/MDXProvider.js.map +1 -1
- package/dist/RoutingSpec.cjs.map +1 -1
- package/dist/TinkerableContext.cjs.map +1 -1
- package/dist/TinkerableContext.js.map +1 -1
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/boot.cjs +2 -9
- package/dist/boot.cjs.map +1 -1
- package/dist/boot.d.cts +2 -2
- package/dist/boot.d.ts +2 -2
- package/dist/boot.js +2 -9
- package/dist/boot.js.map +1 -1
- package/dist/catalog.cjs.map +1 -1
- package/dist/catalog.js.map +1 -1
- package/dist/components/Admonition.cjs +4 -13
- package/dist/components/Admonition.cjs.map +1 -1
- package/dist/components/Admonition.js +4 -13
- package/dist/components/Admonition.js.map +1 -1
- package/dist/components/FileRouter.cjs +11 -3
- package/dist/components/FileRouter.cjs.map +1 -1
- package/dist/components/FileRouter.js +11 -3
- package/dist/components/FileRouter.js.map +1 -1
- package/dist/components/HeadingAnchor.cjs.map +1 -1
- package/dist/components/HeadingAnchor.js.map +1 -1
- package/dist/components/Link.cjs.map +1 -1
- package/dist/components/Link.js.map +1 -1
- package/dist/components/MainContent.cjs +11 -1
- package/dist/components/MainContent.cjs.map +1 -1
- package/dist/components/MainContent.js +11 -1
- package/dist/components/MainContent.js.map +1 -1
- package/dist/components/MountImage.cjs +1 -9
- package/dist/components/MountImage.cjs.map +1 -1
- package/dist/components/MountImage.js +1 -9
- package/dist/components/MountImage.js.map +1 -1
- package/dist/components/Routes.cjs +1 -4
- package/dist/components/Routes.cjs.map +1 -1
- package/dist/components/Routes.d.cts +1 -1
- package/dist/components/Routes.d.ts +1 -1
- package/dist/components/Routes.js +1 -4
- package/dist/components/Routes.js.map +1 -1
- package/dist/components/SafeInclude.cjs +1 -4
- package/dist/components/SafeInclude.cjs.map +1 -1
- package/dist/components/SafeInclude.js +1 -4
- package/dist/components/SafeInclude.js.map +1 -1
- package/dist/components/WikiLink.cjs +1 -10
- package/dist/components/WikiLink.cjs.map +1 -1
- package/dist/components/WikiLink.js +1 -10
- package/dist/components/WikiLink.js.map +1 -1
- package/dist/components/defaults.cjs.map +1 -1
- package/dist/components/defaults.d.cts +1 -1
- package/dist/components/defaults.d.ts +1 -1
- package/dist/components/defaults.js.map +1 -1
- package/dist/components/errors.cjs +3 -1
- package/dist/components/errors.cjs.map +1 -1
- package/dist/components/errors.js +3 -1
- package/dist/components/errors.js.map +1 -1
- package/dist/contextUtils.cjs.map +1 -1
- package/dist/contextUtils.js.map +1 -1
- package/dist/contribute.cjs.map +1 -1
- package/dist/contribute.js.map +1 -1
- package/dist/debug.cjs +2 -1
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.js +3 -8
- package/dist/debug.js.map +1 -1
- package/dist/diagnostics.cjs.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js.map +1 -1
- package/dist/editorContext.cjs.map +1 -1
- package/dist/editorContext.js.map +1 -1
- package/dist/fs.cjs.map +1 -1
- package/dist/fs.js.map +1 -1
- package/dist/hostTransport.cjs.map +1 -1
- package/dist/hostTransport.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/injectedBundler.cjs.map +1 -1
- package/dist/injectedBundler.js.map +1 -1
- package/dist/ipc.cjs +1 -4
- package/dist/ipc.cjs.map +1 -1
- package/dist/ipc.js +1 -4
- package/dist/ipc.js.map +1 -1
- package/dist/irMarkers.cjs.map +1 -1
- package/dist/irMarkers.d.cts +11 -11
- package/dist/irMarkers.d.ts +11 -11
- package/dist/irMarkers.js.map +1 -1
- package/dist/launch.cjs.map +1 -1
- package/dist/launch.js.map +1 -1
- package/dist/llm.cjs +9 -6
- package/dist/llm.cjs.map +1 -1
- package/dist/llm.d.cts +40 -1
- package/dist/llm.d.ts +40 -1
- package/dist/llm.js +8 -6
- package/dist/llm.js.map +1 -1
- package/dist/loading.cjs +18 -14
- package/dist/loading.cjs.map +1 -1
- package/dist/loading.d.cts +2 -2
- package/dist/loading.d.ts +2 -2
- package/dist/loading.js +19 -19
- package/dist/loading.js.map +1 -1
- package/dist/markers.cjs.map +1 -1
- package/dist/markers.js.map +1 -1
- package/dist/metadataSource.cjs +1 -5
- package/dist/metadataSource.cjs.map +1 -1
- package/dist/metadataSource.d.cts +1 -1
- package/dist/metadataSource.d.ts +1 -1
- package/dist/metadataSource.js +1 -5
- package/dist/metadataSource.js.map +1 -1
- package/dist/moduleCache.cjs +4 -1
- package/dist/moduleCache.cjs.map +1 -1
- package/dist/moduleCache.d.cts +1 -1
- package/dist/moduleCache.d.ts +1 -1
- package/dist/moduleCache.js +4 -1
- package/dist/moduleCache.js.map +1 -1
- package/dist/mountMatch.cjs.map +1 -1
- package/dist/mountMatch.js.map +1 -1
- package/dist/mounts.cjs +6 -1
- package/dist/mounts.cjs.map +1 -1
- package/dist/mounts.d.cts +2 -2
- package/dist/mounts.d.ts +2 -2
- package/dist/mounts.js +6 -1
- package/dist/mounts.js.map +1 -1
- package/dist/netFetch.cjs +3 -5
- package/dist/netFetch.cjs.map +1 -1
- package/dist/netFetch.js +3 -5
- package/dist/netFetch.js.map +1 -1
- package/dist/onFsChange.cjs.map +1 -1
- package/dist/onFsChange.js.map +1 -1
- package/dist/pathUtils.cjs +9 -12
- package/dist/pathUtils.cjs.map +1 -1
- package/dist/pathUtils.js +9 -12
- package/dist/pathUtils.js.map +1 -1
- package/dist/protocolDeadline.cjs.map +1 -1
- package/dist/protocolDeadline.js.map +1 -1
- package/dist/protocolStream.cjs +2 -13
- package/dist/protocolStream.cjs.map +1 -1
- package/dist/protocolStream.js +2 -13
- package/dist/protocolStream.js.map +1 -1
- package/dist/ready.cjs.map +1 -1
- package/dist/ready.js.map +1 -1
- package/dist/routing.cjs +3 -1
- package/dist/routing.cjs.map +1 -1
- package/dist/routing.js +3 -1
- package/dist/routing.js.map +1 -1
- package/dist/safeContent/index.cjs.map +1 -1
- package/dist/safeContent/index.js.map +1 -1
- package/dist/safeContent/parseSafeMdast.cjs.map +1 -1
- package/dist/safeContent/parseSafeMdast.js.map +1 -1
- package/dist/safeContent/renderMdast.cjs.map +1 -1
- package/dist/safeContent/renderMdast.js.map +1 -1
- package/dist/sandboxTypes.cjs.map +1 -1
- package/dist/sandboxUtils.cjs +1 -6
- package/dist/sandboxUtils.cjs.map +1 -1
- package/dist/sandboxUtils.js +1 -6
- package/dist/sandboxUtils.js.map +1 -1
- package/dist/scrollToId.cjs.map +1 -1
- package/dist/scrollToId.js.map +1 -1
- package/dist/secrets.cjs.map +1 -1
- package/dist/secrets.js.map +1 -1
- package/dist/tasks.cjs +12 -2
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +12 -2
- package/dist/tasks.js.map +1 -1
- package/dist/testing.cjs +1 -3
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.js +1 -3
- package/dist/testing.js.map +1 -1
- package/dist/theme.cjs.map +1 -1
- package/dist/theme.js.map +1 -1
- package/dist/urlUtils.cjs +7 -18
- package/dist/urlUtils.cjs.map +1 -1
- package/dist/urlUtils.js +7 -18
- package/dist/urlUtils.js.map +1 -1
- package/dist/vcs.cjs +1 -3
- package/dist/vcs.cjs.map +1 -1
- package/dist/vcs.js +1 -3
- package/dist/vcs.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 +10 -4
package/dist/irMarkers.d.ts
CHANGED
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
* array means "no attributes" (a bare mark).
|
|
6
6
|
*/
|
|
7
7
|
declare const IR_MARKERS: {
|
|
8
|
-
readonly
|
|
9
|
-
readonly
|
|
10
|
-
readonly
|
|
11
|
-
readonly
|
|
12
|
-
readonly
|
|
13
|
-
readonly
|
|
14
|
-
readonly
|
|
15
|
-
readonly
|
|
16
|
-
readonly
|
|
17
|
-
readonly
|
|
18
|
-
readonly
|
|
8
|
+
readonly 'ir.open': readonly ["url", "provider", "ns", "repo", "ref", "refKind"];
|
|
9
|
+
readonly 'ir.fetch': readonly ["source", "bytes", "requestCount", "cacheHit", "httpStatus"];
|
|
10
|
+
readonly 'ir.mount': readonly ["phantomCount", "writablePrimed"];
|
|
11
|
+
readonly 'ir.sandbox.boot': readonly [];
|
|
12
|
+
readonly 'ir.transpile': readonly ["moduleCount", "cacheHit", "bytesIn", "bytesOut"];
|
|
13
|
+
readonly 'ir.deps': readonly ["depCount", "bytes", "requestCount", "cacheHit", "cdn"];
|
|
14
|
+
readonly 'ir.eval': readonly ["moduleCount"];
|
|
15
|
+
readonly 'ir.fmp': readonly [];
|
|
16
|
+
readonly 'ir.interactive': readonly ["cold"];
|
|
17
|
+
readonly 'ir.verify': readonly ["result", "blocking"];
|
|
18
|
+
readonly 'ir.refresh': readonly ["bytes"];
|
|
19
19
|
};
|
|
20
20
|
/** A canonical `ir.*` load-profiling marker name (a key of {@link IR_MARKERS}). */
|
|
21
21
|
type IrMarkerName = keyof typeof IR_MARKERS;
|
package/dist/irMarkers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/irMarkers.ts"],"sourcesContent":["// The `ir.*` load-profiling marker vocabulary + the host-side allowlist + the\n// `ir.interactive` precedence rule (LOAD_PROFILING_SPEC §3/§3.1, R3-46).\n//\n// This module is PURE and dependency-free on purpose: the spec (§3.1) requires the\n// host's vocabulary allowlist to be MIRRORED from this same definition (\"additions\n// to the vocabulary are spec amendments, mirrored in the host allowlist in the same\n// change\"). Keeping the table + validator here, with no transport/React imports,\n// lets the sandbox side (this SDK) and the host (site-main, its own mirrored copy)\n// share an identical contract that is unit-tested in both places.\n\n/**\n * Each `ir.*` marker name → the attribute keys its payload may carry (the §3\n * table). A forwarded marker is accepted only if BOTH its name is defined here AND\n * every attribute key it carries is in that marker's allowed set (LP-5). An empty\n * array means \"no attributes\" (a bare mark).\n */\nexport const IR_MARKERS = {\n
|
|
1
|
+
{"version":3,"sources":["../src/irMarkers.ts"],"sourcesContent":["// The `ir.*` load-profiling marker vocabulary + the host-side allowlist + the\n// `ir.interactive` precedence rule (LOAD_PROFILING_SPEC §3/§3.1, R3-46).\n//\n// This module is PURE and dependency-free on purpose: the spec (§3.1) requires the\n// host's vocabulary allowlist to be MIRRORED from this same definition (\"additions\n// to the vocabulary are spec amendments, mirrored in the host allowlist in the same\n// change\"). Keeping the table + validator here, with no transport/React imports,\n// lets the sandbox side (this SDK) and the host (site-main, its own mirrored copy)\n// share an identical contract that is unit-tested in both places.\n\n/**\n * Each `ir.*` marker name → the attribute keys its payload may carry (the §3\n * table). A forwarded marker is accepted only if BOTH its name is defined here AND\n * every attribute key it carries is in that marker's allowed set (LP-5). An empty\n * array means \"no attributes\" (a bare mark).\n */\nexport const IR_MARKERS = {\n 'ir.open': ['url', 'provider', 'ns', 'repo', 'ref', 'refKind'],\n 'ir.fetch': ['source', 'bytes', 'requestCount', 'cacheHit', 'httpStatus'],\n 'ir.mount': ['phantomCount', 'writablePrimed'],\n 'ir.sandbox.boot': [],\n 'ir.transpile': ['moduleCount', 'cacheHit', 'bytesIn', 'bytesOut'],\n 'ir.deps': ['depCount', 'bytes', 'requestCount', 'cacheHit', 'cdn'],\n 'ir.eval': ['moduleCount'],\n 'ir.fmp': [],\n 'ir.interactive': ['cold'],\n 'ir.verify': ['result', 'blocking'],\n 'ir.refresh': ['bytes'],\n} as const;\n\n/** A canonical `ir.*` load-profiling marker name (a key of {@link IR_MARKERS}). */\nexport type IrMarkerName = keyof typeof IR_MARKERS;\n\n// Per-module / per-dep sub-marks (§3): a defined aggregate plus a `[…]` selector,\n// e.g. `ir.transpile.mod[/src/App.tsx]` or `ir.deps.pkg[react@18.2.0]`. A sub-mark\n// inherits its aggregate's attribute schema.\nconst SUBMARK_RE = /^(ir\\.transpile\\.mod|ir\\.deps\\.pkg)\\[[^\\]]+\\]$/;\nconst submarkAggregate = (name: string): IrMarkerName | null =>\n name.startsWith('ir.transpile.mod') ? 'ir.transpile' : name.startsWith('ir.deps.pkg') ? 'ir.deps' : null;\n\n/** Is `name` a defined top-level `ir.*` marker (not a sub-mark)? */\nexport const isIrMarkerName = (name: string): name is IrMarkerName =>\n Object.prototype.hasOwnProperty.call(IR_MARKERS, name);\n\n/** Is `name` an accepted marker name — a defined top-level marker OR a recognized\n * per-module/per-dep sub-mark? */\nexport const isAllowedMarkerName = (name: string): boolean => isIrMarkerName(name) || SUBMARK_RE.test(name);\n\n/** A marker forwarded across the origin boundary (§3.2): a name, the sandbox-side\n * `performance.now()` timestamp, and the optional attribute payload. */\nexport interface ForwardedMarker {\n name: string;\n /** Sandbox-relative timestamp (`performance.now()`) at emission (§3.2). */\n at: number;\n attrs?: Record<string, unknown>;\n}\n\n/**\n * The LP-5 vocabulary allowlist (pure). Accept a forwarded marker ONLY if its name\n * is in the vocabulary AND every attribute key is in that marker's schema. An\n * unknown name — or a defined name carrying an out-of-schema attribute key — is\n * DROPPED (returns `null`), never recorded. This is the gate against an untrusted\n * sandbox minting arbitrary names/values into the host timeline (an injection\n * surface for dashboards / the deferred RUM endpoint). Sub-marks inherit their\n * aggregate's schema.\n */\nexport function validateMarker(m: ForwardedMarker | null | undefined): ForwardedMarker | null {\n if (!m || typeof m.name !== 'string') return null;\n if (typeof m.at !== 'number' || !Number.isFinite(m.at)) return null;\n if (!isAllowedMarkerName(m.name)) return null;\n const base: IrMarkerName = isIrMarkerName(m.name) ? m.name : submarkAggregate(m.name)!;\n const allowed: readonly string[] = IR_MARKERS[base];\n const attrs = m.attrs;\n if (attrs !== undefined) {\n if (typeof attrs !== 'object' || attrs === null) return null;\n for (const key of Object.keys(attrs)) {\n if (!allowed.includes(key)) return null; // out-of-schema attribute → drop the whole marker\n }\n }\n return { name: m.name, at: m.at, ...(attrs !== undefined ? { attrs } : {}) };\n}\n\n/**\n * `ir.interactive = max(rootRenderCommit, reportReady)` (LP2-3). An app-called\n * `reportReady()` may only DELAY interactive — never advance it before the root\n * render commits. A `reportReady()` that fires early is recorded but takes effect\n * at the commit; if the app never reports, the commit alone stands. Budgets bind to\n * the later of the two, so an app can't game its budget by declaring itself ready\n * before it has rendered anything. PURE.\n */\nexport function resolveInteractive(rootRenderCommitAt: number, reportReadyAt?: number): number {\n if (reportReadyAt === undefined) return rootRenderCommitAt;\n return Math.max(rootRenderCommitAt, reportReadyAt);\n}\n"],"mappings":";AAgBO,MAAM,aAAa;AAAA,EACxB,WAAW,CAAC,OAAO,YAAY,MAAM,QAAQ,OAAO,SAAS;AAAA,EAC7D,YAAY,CAAC,UAAU,SAAS,gBAAgB,YAAY,YAAY;AAAA,EACxE,YAAY,CAAC,gBAAgB,gBAAgB;AAAA,EAC7C,mBAAmB,CAAC;AAAA,EACpB,gBAAgB,CAAC,eAAe,YAAY,WAAW,UAAU;AAAA,EACjE,WAAW,CAAC,YAAY,SAAS,gBAAgB,YAAY,KAAK;AAAA,EAClE,WAAW,CAAC,aAAa;AAAA,EACzB,UAAU,CAAC;AAAA,EACX,kBAAkB,CAAC,MAAM;AAAA,EACzB,aAAa,CAAC,UAAU,UAAU;AAAA,EAClC,cAAc,CAAC,OAAO;AACxB;AAQA,MAAM,aAAa;AACnB,MAAM,mBAAmB,CAAC,SACxB,KAAK,WAAW,kBAAkB,IAAI,iBAAiB,KAAK,WAAW,aAAa,IAAI,YAAY;AAG/F,MAAM,iBAAiB,CAAC,SAC7B,OAAO,UAAU,eAAe,KAAK,YAAY,IAAI;AAIhD,MAAM,sBAAsB,CAAC,SAA0B,eAAe,IAAI,KAAK,WAAW,KAAK,IAAI;AAoBnG,SAAS,eAAe,GAA+D;AAC5F,MAAI,CAAC,KAAK,OAAO,EAAE,SAAS,SAAU,QAAO;AAC7C,MAAI,OAAO,EAAE,OAAO,YAAY,CAAC,OAAO,SAAS,EAAE,EAAE,EAAG,QAAO;AAC/D,MAAI,CAAC,oBAAoB,EAAE,IAAI,EAAG,QAAO;AACzC,QAAM,OAAqB,eAAe,EAAE,IAAI,IAAI,EAAE,OAAO,iBAAiB,EAAE,IAAI;AACpF,QAAM,UAA6B,WAAW,IAAI;AAClD,QAAM,QAAQ,EAAE;AAChB,MAAI,UAAU,QAAW;AACvB,QAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,eAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,UAAI,CAAC,QAAQ,SAAS,GAAG,EAAG,QAAO;AAAA,IACrC;AAAA,EACF;AACA,SAAO,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE,IAAI,GAAI,UAAU,SAAY,EAAE,MAAM,IAAI,CAAC,EAAG;AAC7E;AAUO,SAAS,mBAAmB,oBAA4B,eAAgC;AAC7F,MAAI,kBAAkB,OAAW,QAAO;AACxC,SAAO,KAAK,IAAI,oBAAoB,aAAa;AACnD;","names":[]}
|
package/dist/launch.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/launch.ts"],"sourcesContent":["// Launch — the TO-RUN twin of `invokeTask` (STANDING_APP_LIFECYCLE_SPEC §1–§6).\n// Where `invokeTask` is FOR-RESULT (await one value, callee exits), `launch` is\n// TO-RUN (non-blocking, standing): it starts a bound program RUNNING in a region\n// and hands back a HANDLE, never a typed value (R-SAL-1). The launched app earns\n// its OWN grants — the only launcher→launched data path is the explicitly\n// delegated `capDir`/`capFile` in `input` (§5), attenuated + tainted + gated host-\n// side. The host owns the whole lifecycle (visibility, budget, revocation,\n// teardown); the launcher only OBSERVES via the handle.\n//\n// A caller that wants a typed value back uses `invokeTask` instead — the two are\n// siblings, not a replacement.\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { LAUNCH_DISMISS, LAUNCH_ENDED, PROTOCOL_LAUNCH } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** Where a launched program runs (§6). `overlay` covers the caller's own region\n * with opaque host chrome; `stage` replaces the focal app (the elevated into-\n * stage surface, §7 — refused above the stage-principal ceiling). */\nexport type LaunchRegion = 'overlay' | 'stage';\n\n/**\n * What to launch (§3) — binding-resolved, NEVER a caller-named app. Exactly one of:\n * - `entryPoint`: a sibling entry point of the caller's OWN repo (the mini-app\n * case, `AGENT_AUTHORING §5`) — **rejected `forbidden` until program-identity\n * `appKey` lands** (R-SAL-2a);\n * - `task`: a task contract (`open-project`, …), resolved through the user-\n * overridable `task.<name>` binding to whichever app the user bound (§3 kind 2).\n */\nexport interface LaunchTarget {\n /** Kind 1 — a sibling entry point of the caller's own repo (mini-app overlay). */\n entryPoint?: string;\n /** Kind 2 — a task contract name (the host resolves the bound provider). */\n task?: string;\n /** Optional accepted contract version for a `task` target (semver, e.g. `^1`). */\n version?: string;\n}\n\nexport interface LaunchOptions {\n /** Where it runs (§6). */\n region: LaunchRegion;\n /**\n * Delegations + plain data handed to the launched app. `$cap:'dir'|'file'`\n * markers (`capDir`/`capFile`, exported from `./tasks`) are resolved against the\n * launcher's OWN grants and minted as attenuated, `ro`-by-default chroots (§5) —\n * you can only delegate what you already hold. Everything else is plain data.\n */\n input?: Record<string, unknown>;\n}\n\n/** The live state of a launch (§2). Terminal states (`dismissed`/`revoked`/\n * `failed`) are reached identically for self-exit, user dismiss, and host revoke —\n * the host debounces so the launcher gets no timing oracle (R-SAL-1 / §6.4). */\nexport type LaunchStatus = 'running' | 'dismissed' | 'revoked' | 'failed';\n\n/** Machine codes a refused `launch` resolves with (§8). */\nexport type LaunchErrorCode =\n | 'forbidden' // undeclared target; sibling (pre-AA-01); self-resolving target; stage cap absent\n | 'unsupported' // no provider bound / entry point absent / unknown contract version\n | 'budget' // over an R-SAL-8 concurrency/rate limit\n | 'revoked' // a delegated mount was revoked during the create-and-bind window\n | 'cancelled' // the user dismissed the host launch affordance (into-stage only)\n | 'invalid-params' // a malformed `capDir`/`capFile` (`..`/absolute/mode-escalation)\n | 'unknown';\n\n/**\n * The control channel back to a launch — the ONLY thing a launcher gets (§2).\n * There is no typed return value (R-SAL-1); `status`/`onDismiss` are debounced so\n * they cannot time-distinguish a self-exit from a user dismiss.\n */\nexport interface LaunchHandle {\n /** Host-assigned id for this launch. */\n readonly launchId: string;\n /** The current lifecycle state (§2). */\n readonly status: LaunchStatus;\n /** Ask the host to tear this launch down. Idempotent (double-dismiss is a no-op). */\n dismiss(): void;\n /**\n * Observe the launch ending (self-exit / user dismiss / host revoke — fired\n * identically, R-SAL-1). Returns an unsubscribe fn. Fires at most once; if the\n * launch has already ended it fires on the next tick.\n */\n onDismiss(cb: () => void): () => void;\n}\n\ninterface LaunchEndedMessage {\n launchId: string;\n status: Exclude<LaunchStatus, 'running'>;\n}\n\n/** Live handles awaiting their terminal `launch-ended` message, keyed by launchId. */\nconst liveHandles = new Map<string, LaunchHandleImpl>();\n\n// The host delivers ONE `launch-ended` message per launch when it tears down —\n// the SAME message shape for self-exit, dismiss, and revoke (the host debounces\n// so the timing is not an oracle, §6.4). We fan it out to the matching handle.\naddListener(LAUNCH_ENDED, (m: LaunchEndedMessage) => {\n const h = liveHandles.get(m.launchId);\n if (h) h._end(m.status);\n});\n\nclass LaunchHandleImpl implements LaunchHandle {\n #status: LaunchStatus = 'running';\n #dismissListeners = new Set<() => void>();\n #ended = false;\n\n constructor(readonly launchId: string) {\n liveHandles.set(launchId, this);\n }\n\n get status(): LaunchStatus {\n return this.#status;\n }\n\n dismiss(): void {\n if (this.#ended) return;\n // Fire-and-forget: the host owns teardown and answers with `launch-ended`,\n // which drives `_end` (so status/onDismiss are host-authoritative, never\n // optimistically local — a dismiss the host refuses would otherwise desync).\n sendMessage(LAUNCH_DISMISS, { launchId: this.launchId });\n }\n\n onDismiss(cb: () => void): () => void {\n if (this.#ended) {\n // Already ended: fire on the next tick so the contract (\"returns an\n // unsubscribe\") holds and the callback never runs synchronously mid-register.\n queueMicrotask(cb);\n return () => {};\n }\n this.#dismissListeners.add(cb);\n return () => {\n this.#dismissListeners.delete(cb);\n };\n }\n\n /** Host-driven terminal transition — the only writer of `status`. Idempotent. */\n _end(status: Exclude<LaunchStatus, 'running'>): void {\n if (this.#ended) return;\n this.#ended = true;\n this.#status = status;\n liveHandles.delete(this.launchId);\n const listeners = [...this.#dismissListeners];\n this.#dismissListeners.clear();\n for (const l of listeners) {\n try {\n l();\n } catch {\n /* a launcher's own callback must never wedge teardown */\n }\n }\n }\n}\n\n/**\n * Launch a bound program to RUN in a region (§2). Non-blocking: resolves once the\n * frame is created and bound, with a {@link LaunchHandle} — or a typed\n * `{ ok:false, code }` on refusal (§8), NEVER a throw for an ordinary refusal (so\n * a launcher branches on `code` without a try/catch). The launched app runs under\n * its OWN grants; the launcher's authority does not flow to it (R-SAL-4).\n *\n * const h = await launch({ task: 'open-project' }, {\n * region: 'stage',\n * input: { dir: capDir({ mountId: 'space:abc', relPath: 'proj' }, { mode: 'ro' }) },\n * });\n * if ('ok' in h && h.ok === false) { ...handle h.code... }\n * else { h.onDismiss(() => ...); }\n */\nexport const launch = async (\n target: LaunchTarget,\n opts: LaunchOptions,\n): Promise<LaunchHandle | { ok: false; code: LaunchErrorCode }> => {\n // The host wraps a successful handler return as `{ ok:true, data }` (the same\n // Recipe-B framing `invokeTask` uses); a refusal is `{ ok:false, code }`.\n const res = (await protocolRequest(SCHEMES[PROTOCOL_LAUNCH], 'create', [{ target, opts }])) as\n | { ok: true; data: { launchId: string } }\n | { ok: false; code?: LaunchErrorCode }\n | undefined;\n if (!res || res.ok !== true || !res.data?.launchId) {\n const code = res && res.ok === false ?
|
|
1
|
+
{"version":3,"sources":["../src/launch.ts"],"sourcesContent":["// Launch — the TO-RUN twin of `invokeTask` (STANDING_APP_LIFECYCLE_SPEC §1–§6).\n// Where `invokeTask` is FOR-RESULT (await one value, callee exits), `launch` is\n// TO-RUN (non-blocking, standing): it starts a bound program RUNNING in a region\n// and hands back a HANDLE, never a typed value (R-SAL-1). The launched app earns\n// its OWN grants — the only launcher→launched data path is the explicitly\n// delegated `capDir`/`capFile` in `input` (§5), attenuated + tainted + gated host-\n// side. The host owns the whole lifecycle (visibility, budget, revocation,\n// teardown); the launcher only OBSERVES via the handle.\n//\n// A caller that wants a typed value back uses `invokeTask` instead — the two are\n// siblings, not a replacement.\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { LAUNCH_DISMISS, LAUNCH_ENDED, PROTOCOL_LAUNCH } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** Where a launched program runs (§6). `overlay` covers the caller's own region\n * with opaque host chrome; `stage` replaces the focal app (the elevated into-\n * stage surface, §7 — refused above the stage-principal ceiling). */\nexport type LaunchRegion = 'overlay' | 'stage';\n\n/**\n * What to launch (§3) — binding-resolved, NEVER a caller-named app. Exactly one of:\n * - `entryPoint`: a sibling entry point of the caller's OWN repo (the mini-app\n * case, `AGENT_AUTHORING §5`) — **rejected `forbidden` until program-identity\n * `appKey` lands** (R-SAL-2a);\n * - `task`: a task contract (`open-project`, …), resolved through the user-\n * overridable `task.<name>` binding to whichever app the user bound (§3 kind 2).\n */\nexport interface LaunchTarget {\n /** Kind 1 — a sibling entry point of the caller's own repo (mini-app overlay). */\n entryPoint?: string;\n /** Kind 2 — a task contract name (the host resolves the bound provider). */\n task?: string;\n /** Optional accepted contract version for a `task` target (semver, e.g. `^1`). */\n version?: string;\n}\n\nexport interface LaunchOptions {\n /** Where it runs (§6). */\n region: LaunchRegion;\n /**\n * Delegations + plain data handed to the launched app. `$cap:'dir'|'file'`\n * markers (`capDir`/`capFile`, exported from `./tasks`) are resolved against the\n * launcher's OWN grants and minted as attenuated, `ro`-by-default chroots (§5) —\n * you can only delegate what you already hold. Everything else is plain data.\n */\n input?: Record<string, unknown>;\n}\n\n/** The live state of a launch (§2). Terminal states (`dismissed`/`revoked`/\n * `failed`) are reached identically for self-exit, user dismiss, and host revoke —\n * the host debounces so the launcher gets no timing oracle (R-SAL-1 / §6.4). */\nexport type LaunchStatus = 'running' | 'dismissed' | 'revoked' | 'failed';\n\n/** Machine codes a refused `launch` resolves with (§8). */\nexport type LaunchErrorCode =\n | 'forbidden' // undeclared target; sibling (pre-AA-01); self-resolving target; stage cap absent\n | 'unsupported' // no provider bound / entry point absent / unknown contract version\n | 'budget' // over an R-SAL-8 concurrency/rate limit\n | 'revoked' // a delegated mount was revoked during the create-and-bind window\n | 'cancelled' // the user dismissed the host launch affordance (into-stage only)\n | 'invalid-params' // a malformed `capDir`/`capFile` (`..`/absolute/mode-escalation)\n | 'unknown';\n\n/**\n * The control channel back to a launch — the ONLY thing a launcher gets (§2).\n * There is no typed return value (R-SAL-1); `status`/`onDismiss` are debounced so\n * they cannot time-distinguish a self-exit from a user dismiss.\n */\nexport interface LaunchHandle {\n /** Host-assigned id for this launch. */\n readonly launchId: string;\n /** The current lifecycle state (§2). */\n readonly status: LaunchStatus;\n /** Ask the host to tear this launch down. Idempotent (double-dismiss is a no-op). */\n dismiss(): void;\n /**\n * Observe the launch ending (self-exit / user dismiss / host revoke — fired\n * identically, R-SAL-1). Returns an unsubscribe fn. Fires at most once; if the\n * launch has already ended it fires on the next tick.\n */\n onDismiss(cb: () => void): () => void;\n}\n\ninterface LaunchEndedMessage {\n launchId: string;\n status: Exclude<LaunchStatus, 'running'>;\n}\n\n/** Live handles awaiting their terminal `launch-ended` message, keyed by launchId. */\nconst liveHandles = new Map<string, LaunchHandleImpl>();\n\n// The host delivers ONE `launch-ended` message per launch when it tears down —\n// the SAME message shape for self-exit, dismiss, and revoke (the host debounces\n// so the timing is not an oracle, §6.4). We fan it out to the matching handle.\naddListener(LAUNCH_ENDED, (m: LaunchEndedMessage) => {\n const h = liveHandles.get(m.launchId);\n if (h) h._end(m.status);\n});\n\nclass LaunchHandleImpl implements LaunchHandle {\n #status: LaunchStatus = 'running';\n #dismissListeners = new Set<() => void>();\n #ended = false;\n\n constructor(readonly launchId: string) {\n liveHandles.set(launchId, this);\n }\n\n get status(): LaunchStatus {\n return this.#status;\n }\n\n dismiss(): void {\n if (this.#ended) return;\n // Fire-and-forget: the host owns teardown and answers with `launch-ended`,\n // which drives `_end` (so status/onDismiss are host-authoritative, never\n // optimistically local — a dismiss the host refuses would otherwise desync).\n sendMessage(LAUNCH_DISMISS, { launchId: this.launchId });\n }\n\n onDismiss(cb: () => void): () => void {\n if (this.#ended) {\n // Already ended: fire on the next tick so the contract (\"returns an\n // unsubscribe\") holds and the callback never runs synchronously mid-register.\n queueMicrotask(cb);\n return () => {};\n }\n this.#dismissListeners.add(cb);\n return () => {\n this.#dismissListeners.delete(cb);\n };\n }\n\n /** Host-driven terminal transition — the only writer of `status`. Idempotent. */\n _end(status: Exclude<LaunchStatus, 'running'>): void {\n if (this.#ended) return;\n this.#ended = true;\n this.#status = status;\n liveHandles.delete(this.launchId);\n const listeners = [...this.#dismissListeners];\n this.#dismissListeners.clear();\n for (const l of listeners) {\n try {\n l();\n } catch {\n /* a launcher's own callback must never wedge teardown */\n }\n }\n }\n}\n\n/**\n * Launch a bound program to RUN in a region (§2). Non-blocking: resolves once the\n * frame is created and bound, with a {@link LaunchHandle} — or a typed\n * `{ ok:false, code }` on refusal (§8), NEVER a throw for an ordinary refusal (so\n * a launcher branches on `code` without a try/catch). The launched app runs under\n * its OWN grants; the launcher's authority does not flow to it (R-SAL-4).\n *\n * const h = await launch({ task: 'open-project' }, {\n * region: 'stage',\n * input: { dir: capDir({ mountId: 'space:abc', relPath: 'proj' }, { mode: 'ro' }) },\n * });\n * if ('ok' in h && h.ok === false) { ...handle h.code... }\n * else { h.onDismiss(() => ...); }\n */\nexport const launch = async (\n target: LaunchTarget,\n opts: LaunchOptions,\n): Promise<LaunchHandle | { ok: false; code: LaunchErrorCode }> => {\n // The host wraps a successful handler return as `{ ok:true, data }` (the same\n // Recipe-B framing `invokeTask` uses); a refusal is `{ ok:false, code }`.\n const res = (await protocolRequest(SCHEMES[PROTOCOL_LAUNCH], 'create', [{ target, opts }])) as\n | { ok: true; data: { launchId: string } }\n | { ok: false; code?: LaunchErrorCode }\n | undefined;\n if (!res || res.ok !== true || !res.data?.launchId) {\n const code = res && res.ok === false ? res.code ?? 'unknown' : 'unknown';\n return { ok: false, code };\n }\n return new LaunchHandleImpl(res.data.launchId);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,0BAA0D;AAC1D,sBAA8D;AAC9D,6BAAwB;AAbxB;AA0FA,MAAM,cAAc,oBAAI,IAA8B;AAAA,IAKtD,iCAAY,8BAAc,CAAC,MAA0B;AACnD,QAAM,IAAI,YAAY,IAAI,EAAE,QAAQ;AACpC,MAAI,EAAG,GAAE,KAAK,EAAE,MAAM;AACxB,CAAC;AAED,MAAM,iBAAyC;AAAA,EAK7C,YAAqB,UAAkB;AAAlB;AAJrB,gCAAwB;AACxB,0CAAoB,oBAAI,IAAgB;AACxC,+BAAS;AAGP,gBAAY,IAAI,UAAU,IAAI;AAAA,EAChC;AAAA,EAEA,IAAI,SAAuB;AACzB,WAAO,mBAAK;AAAA,EACd;AAAA,EAEA,UAAgB;AACd,QAAI,mBAAK,QAAQ;AAIjB,yCAAY,gCAAgB,EAAE,UAAU,KAAK,SAAS,CAAC;AAAA,EACzD;AAAA,EAEA,UAAU,IAA4B;AACpC,QAAI,mBAAK,SAAQ;AAGf,qBAAe,EAAE;AACjB,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AACA,uBAAK,mBAAkB,IAAI,EAAE;AAC7B,WAAO,MAAM;AACX,yBAAK,mBAAkB,OAAO,EAAE;AAAA,IAClC;AAAA,EACF;AAAA;AAAA,EAGA,KAAK,QAAgD;AACnD,QAAI,mBAAK,QAAQ;AACjB,uBAAK,QAAS;AACd,uBAAK,SAAU;AACf,gBAAY,OAAO,KAAK,QAAQ;AAChC,UAAM,YAAY,CAAC,GAAG,mBAAK,kBAAiB;AAC5C,uBAAK,mBAAkB,MAAM;AAC7B,eAAW,KAAK,WAAW;AACzB,UAAI;AACF,UAAE;AAAA,MACJ,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;AAjDE;AACA;AACA;AA+DK,MAAM,SAAS,OACpB,QACA,SACiE;AAGjE,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,+BAAe,GAAG,UAAU,CAAC,EAAE,QAAQ,KAAK,CAAC,CAAC;AAIzF,MAAI,CAAC,OAAO,IAAI,OAAO,QAAQ,CAAC,IAAI,MAAM,UAAU;AAClD,UAAM,OAAO,OAAO,IAAI,OAAO,QAAQ,IAAI,QAAQ,YAAY;AAC/D,WAAO,EAAE,IAAI,OAAO,KAAK;AAAA,EAC3B;AACA,SAAO,IAAI,iBAAiB,IAAI,KAAK,QAAQ;AAC/C;","names":[]}
|
package/dist/launch.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/launch.ts"],"sourcesContent":["// Launch — the TO-RUN twin of `invokeTask` (STANDING_APP_LIFECYCLE_SPEC §1–§6).\n// Where `invokeTask` is FOR-RESULT (await one value, callee exits), `launch` is\n// TO-RUN (non-blocking, standing): it starts a bound program RUNNING in a region\n// and hands back a HANDLE, never a typed value (R-SAL-1). The launched app earns\n// its OWN grants — the only launcher→launched data path is the explicitly\n// delegated `capDir`/`capFile` in `input` (§5), attenuated + tainted + gated host-\n// side. The host owns the whole lifecycle (visibility, budget, revocation,\n// teardown); the launcher only OBSERVES via the handle.\n//\n// A caller that wants a typed value back uses `invokeTask` instead — the two are\n// siblings, not a replacement.\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { LAUNCH_DISMISS, LAUNCH_ENDED, PROTOCOL_LAUNCH } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** Where a launched program runs (§6). `overlay` covers the caller's own region\n * with opaque host chrome; `stage` replaces the focal app (the elevated into-\n * stage surface, §7 — refused above the stage-principal ceiling). */\nexport type LaunchRegion = 'overlay' | 'stage';\n\n/**\n * What to launch (§3) — binding-resolved, NEVER a caller-named app. Exactly one of:\n * - `entryPoint`: a sibling entry point of the caller's OWN repo (the mini-app\n * case, `AGENT_AUTHORING §5`) — **rejected `forbidden` until program-identity\n * `appKey` lands** (R-SAL-2a);\n * - `task`: a task contract (`open-project`, …), resolved through the user-\n * overridable `task.<name>` binding to whichever app the user bound (§3 kind 2).\n */\nexport interface LaunchTarget {\n /** Kind 1 — a sibling entry point of the caller's own repo (mini-app overlay). */\n entryPoint?: string;\n /** Kind 2 — a task contract name (the host resolves the bound provider). */\n task?: string;\n /** Optional accepted contract version for a `task` target (semver, e.g. `^1`). */\n version?: string;\n}\n\nexport interface LaunchOptions {\n /** Where it runs (§6). */\n region: LaunchRegion;\n /**\n * Delegations + plain data handed to the launched app. `$cap:'dir'|'file'`\n * markers (`capDir`/`capFile`, exported from `./tasks`) are resolved against the\n * launcher's OWN grants and minted as attenuated, `ro`-by-default chroots (§5) —\n * you can only delegate what you already hold. Everything else is plain data.\n */\n input?: Record<string, unknown>;\n}\n\n/** The live state of a launch (§2). Terminal states (`dismissed`/`revoked`/\n * `failed`) are reached identically for self-exit, user dismiss, and host revoke —\n * the host debounces so the launcher gets no timing oracle (R-SAL-1 / §6.4). */\nexport type LaunchStatus = 'running' | 'dismissed' | 'revoked' | 'failed';\n\n/** Machine codes a refused `launch` resolves with (§8). */\nexport type LaunchErrorCode =\n | 'forbidden' // undeclared target; sibling (pre-AA-01); self-resolving target; stage cap absent\n | 'unsupported' // no provider bound / entry point absent / unknown contract version\n | 'budget' // over an R-SAL-8 concurrency/rate limit\n | 'revoked' // a delegated mount was revoked during the create-and-bind window\n | 'cancelled' // the user dismissed the host launch affordance (into-stage only)\n | 'invalid-params' // a malformed `capDir`/`capFile` (`..`/absolute/mode-escalation)\n | 'unknown';\n\n/**\n * The control channel back to a launch — the ONLY thing a launcher gets (§2).\n * There is no typed return value (R-SAL-1); `status`/`onDismiss` are debounced so\n * they cannot time-distinguish a self-exit from a user dismiss.\n */\nexport interface LaunchHandle {\n /** Host-assigned id for this launch. */\n readonly launchId: string;\n /** The current lifecycle state (§2). */\n readonly status: LaunchStatus;\n /** Ask the host to tear this launch down. Idempotent (double-dismiss is a no-op). */\n dismiss(): void;\n /**\n * Observe the launch ending (self-exit / user dismiss / host revoke — fired\n * identically, R-SAL-1). Returns an unsubscribe fn. Fires at most once; if the\n * launch has already ended it fires on the next tick.\n */\n onDismiss(cb: () => void): () => void;\n}\n\ninterface LaunchEndedMessage {\n launchId: string;\n status: Exclude<LaunchStatus, 'running'>;\n}\n\n/** Live handles awaiting their terminal `launch-ended` message, keyed by launchId. */\nconst liveHandles = new Map<string, LaunchHandleImpl>();\n\n// The host delivers ONE `launch-ended` message per launch when it tears down —\n// the SAME message shape for self-exit, dismiss, and revoke (the host debounces\n// so the timing is not an oracle, §6.4). We fan it out to the matching handle.\naddListener(LAUNCH_ENDED, (m: LaunchEndedMessage) => {\n const h = liveHandles.get(m.launchId);\n if (h) h._end(m.status);\n});\n\nclass LaunchHandleImpl implements LaunchHandle {\n #status: LaunchStatus = 'running';\n #dismissListeners = new Set<() => void>();\n #ended = false;\n\n constructor(readonly launchId: string) {\n liveHandles.set(launchId, this);\n }\n\n get status(): LaunchStatus {\n return this.#status;\n }\n\n dismiss(): void {\n if (this.#ended) return;\n // Fire-and-forget: the host owns teardown and answers with `launch-ended`,\n // which drives `_end` (so status/onDismiss are host-authoritative, never\n // optimistically local — a dismiss the host refuses would otherwise desync).\n sendMessage(LAUNCH_DISMISS, { launchId: this.launchId });\n }\n\n onDismiss(cb: () => void): () => void {\n if (this.#ended) {\n // Already ended: fire on the next tick so the contract (\"returns an\n // unsubscribe\") holds and the callback never runs synchronously mid-register.\n queueMicrotask(cb);\n return () => {};\n }\n this.#dismissListeners.add(cb);\n return () => {\n this.#dismissListeners.delete(cb);\n };\n }\n\n /** Host-driven terminal transition — the only writer of `status`. Idempotent. */\n _end(status: Exclude<LaunchStatus, 'running'>): void {\n if (this.#ended) return;\n this.#ended = true;\n this.#status = status;\n liveHandles.delete(this.launchId);\n const listeners = [...this.#dismissListeners];\n this.#dismissListeners.clear();\n for (const l of listeners) {\n try {\n l();\n } catch {\n /* a launcher's own callback must never wedge teardown */\n }\n }\n }\n}\n\n/**\n * Launch a bound program to RUN in a region (§2). Non-blocking: resolves once the\n * frame is created and bound, with a {@link LaunchHandle} — or a typed\n * `{ ok:false, code }` on refusal (§8), NEVER a throw for an ordinary refusal (so\n * a launcher branches on `code` without a try/catch). The launched app runs under\n * its OWN grants; the launcher's authority does not flow to it (R-SAL-4).\n *\n * const h = await launch({ task: 'open-project' }, {\n * region: 'stage',\n * input: { dir: capDir({ mountId: 'space:abc', relPath: 'proj' }, { mode: 'ro' }) },\n * });\n * if ('ok' in h && h.ok === false) { ...handle h.code... }\n * else { h.onDismiss(() => ...); }\n */\nexport const launch = async (\n target: LaunchTarget,\n opts: LaunchOptions,\n): Promise<LaunchHandle | { ok: false; code: LaunchErrorCode }> => {\n // The host wraps a successful handler return as `{ ok:true, data }` (the same\n // Recipe-B framing `invokeTask` uses); a refusal is `{ ok:false, code }`.\n const res = (await protocolRequest(SCHEMES[PROTOCOL_LAUNCH], 'create', [{ target, opts }])) as\n | { ok: true; data: { launchId: string } }\n | { ok: false; code?: LaunchErrorCode }\n | undefined;\n if (!res || res.ok !== true || !res.data?.launchId) {\n const code = res && res.ok === false ?
|
|
1
|
+
{"version":3,"sources":["../src/launch.ts"],"sourcesContent":["// Launch — the TO-RUN twin of `invokeTask` (STANDING_APP_LIFECYCLE_SPEC §1–§6).\n// Where `invokeTask` is FOR-RESULT (await one value, callee exits), `launch` is\n// TO-RUN (non-blocking, standing): it starts a bound program RUNNING in a region\n// and hands back a HANDLE, never a typed value (R-SAL-1). The launched app earns\n// its OWN grants — the only launcher→launched data path is the explicitly\n// delegated `capDir`/`capFile` in `input` (§5), attenuated + tainted + gated host-\n// side. The host owns the whole lifecycle (visibility, budget, revocation,\n// teardown); the launcher only OBSERVES via the handle.\n//\n// A caller that wants a typed value back uses `invokeTask` instead — the two are\n// siblings, not a replacement.\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { LAUNCH_DISMISS, LAUNCH_ENDED, PROTOCOL_LAUNCH } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** Where a launched program runs (§6). `overlay` covers the caller's own region\n * with opaque host chrome; `stage` replaces the focal app (the elevated into-\n * stage surface, §7 — refused above the stage-principal ceiling). */\nexport type LaunchRegion = 'overlay' | 'stage';\n\n/**\n * What to launch (§3) — binding-resolved, NEVER a caller-named app. Exactly one of:\n * - `entryPoint`: a sibling entry point of the caller's OWN repo (the mini-app\n * case, `AGENT_AUTHORING §5`) — **rejected `forbidden` until program-identity\n * `appKey` lands** (R-SAL-2a);\n * - `task`: a task contract (`open-project`, …), resolved through the user-\n * overridable `task.<name>` binding to whichever app the user bound (§3 kind 2).\n */\nexport interface LaunchTarget {\n /** Kind 1 — a sibling entry point of the caller's own repo (mini-app overlay). */\n entryPoint?: string;\n /** Kind 2 — a task contract name (the host resolves the bound provider). */\n task?: string;\n /** Optional accepted contract version for a `task` target (semver, e.g. `^1`). */\n version?: string;\n}\n\nexport interface LaunchOptions {\n /** Where it runs (§6). */\n region: LaunchRegion;\n /**\n * Delegations + plain data handed to the launched app. `$cap:'dir'|'file'`\n * markers (`capDir`/`capFile`, exported from `./tasks`) are resolved against the\n * launcher's OWN grants and minted as attenuated, `ro`-by-default chroots (§5) —\n * you can only delegate what you already hold. Everything else is plain data.\n */\n input?: Record<string, unknown>;\n}\n\n/** The live state of a launch (§2). Terminal states (`dismissed`/`revoked`/\n * `failed`) are reached identically for self-exit, user dismiss, and host revoke —\n * the host debounces so the launcher gets no timing oracle (R-SAL-1 / §6.4). */\nexport type LaunchStatus = 'running' | 'dismissed' | 'revoked' | 'failed';\n\n/** Machine codes a refused `launch` resolves with (§8). */\nexport type LaunchErrorCode =\n | 'forbidden' // undeclared target; sibling (pre-AA-01); self-resolving target; stage cap absent\n | 'unsupported' // no provider bound / entry point absent / unknown contract version\n | 'budget' // over an R-SAL-8 concurrency/rate limit\n | 'revoked' // a delegated mount was revoked during the create-and-bind window\n | 'cancelled' // the user dismissed the host launch affordance (into-stage only)\n | 'invalid-params' // a malformed `capDir`/`capFile` (`..`/absolute/mode-escalation)\n | 'unknown';\n\n/**\n * The control channel back to a launch — the ONLY thing a launcher gets (§2).\n * There is no typed return value (R-SAL-1); `status`/`onDismiss` are debounced so\n * they cannot time-distinguish a self-exit from a user dismiss.\n */\nexport interface LaunchHandle {\n /** Host-assigned id for this launch. */\n readonly launchId: string;\n /** The current lifecycle state (§2). */\n readonly status: LaunchStatus;\n /** Ask the host to tear this launch down. Idempotent (double-dismiss is a no-op). */\n dismiss(): void;\n /**\n * Observe the launch ending (self-exit / user dismiss / host revoke — fired\n * identically, R-SAL-1). Returns an unsubscribe fn. Fires at most once; if the\n * launch has already ended it fires on the next tick.\n */\n onDismiss(cb: () => void): () => void;\n}\n\ninterface LaunchEndedMessage {\n launchId: string;\n status: Exclude<LaunchStatus, 'running'>;\n}\n\n/** Live handles awaiting their terminal `launch-ended` message, keyed by launchId. */\nconst liveHandles = new Map<string, LaunchHandleImpl>();\n\n// The host delivers ONE `launch-ended` message per launch when it tears down —\n// the SAME message shape for self-exit, dismiss, and revoke (the host debounces\n// so the timing is not an oracle, §6.4). We fan it out to the matching handle.\naddListener(LAUNCH_ENDED, (m: LaunchEndedMessage) => {\n const h = liveHandles.get(m.launchId);\n if (h) h._end(m.status);\n});\n\nclass LaunchHandleImpl implements LaunchHandle {\n #status: LaunchStatus = 'running';\n #dismissListeners = new Set<() => void>();\n #ended = false;\n\n constructor(readonly launchId: string) {\n liveHandles.set(launchId, this);\n }\n\n get status(): LaunchStatus {\n return this.#status;\n }\n\n dismiss(): void {\n if (this.#ended) return;\n // Fire-and-forget: the host owns teardown and answers with `launch-ended`,\n // which drives `_end` (so status/onDismiss are host-authoritative, never\n // optimistically local — a dismiss the host refuses would otherwise desync).\n sendMessage(LAUNCH_DISMISS, { launchId: this.launchId });\n }\n\n onDismiss(cb: () => void): () => void {\n if (this.#ended) {\n // Already ended: fire on the next tick so the contract (\"returns an\n // unsubscribe\") holds and the callback never runs synchronously mid-register.\n queueMicrotask(cb);\n return () => {};\n }\n this.#dismissListeners.add(cb);\n return () => {\n this.#dismissListeners.delete(cb);\n };\n }\n\n /** Host-driven terminal transition — the only writer of `status`. Idempotent. */\n _end(status: Exclude<LaunchStatus, 'running'>): void {\n if (this.#ended) return;\n this.#ended = true;\n this.#status = status;\n liveHandles.delete(this.launchId);\n const listeners = [...this.#dismissListeners];\n this.#dismissListeners.clear();\n for (const l of listeners) {\n try {\n l();\n } catch {\n /* a launcher's own callback must never wedge teardown */\n }\n }\n }\n}\n\n/**\n * Launch a bound program to RUN in a region (§2). Non-blocking: resolves once the\n * frame is created and bound, with a {@link LaunchHandle} — or a typed\n * `{ ok:false, code }` on refusal (§8), NEVER a throw for an ordinary refusal (so\n * a launcher branches on `code` without a try/catch). The launched app runs under\n * its OWN grants; the launcher's authority does not flow to it (R-SAL-4).\n *\n * const h = await launch({ task: 'open-project' }, {\n * region: 'stage',\n * input: { dir: capDir({ mountId: 'space:abc', relPath: 'proj' }, { mode: 'ro' }) },\n * });\n * if ('ok' in h && h.ok === false) { ...handle h.code... }\n * else { h.onDismiss(() => ...); }\n */\nexport const launch = async (\n target: LaunchTarget,\n opts: LaunchOptions,\n): Promise<LaunchHandle | { ok: false; code: LaunchErrorCode }> => {\n // The host wraps a successful handler return as `{ ok:true, data }` (the same\n // Recipe-B framing `invokeTask` uses); a refusal is `{ ok:false, code }`.\n const res = (await protocolRequest(SCHEMES[PROTOCOL_LAUNCH], 'create', [{ target, opts }])) as\n | { ok: true; data: { launchId: string } }\n | { ok: false; code?: LaunchErrorCode }\n | undefined;\n if (!res || res.ok !== true || !res.data?.launchId) {\n const code = res && res.ok === false ? res.code ?? 'unknown' : 'unknown';\n return { ok: false, code };\n }\n return new LaunchHandleImpl(res.data.launchId);\n};\n"],"mappings":";;;;;AAAA;AAWA,SAAS,iBAAiB,aAAa,mBAAmB;AAC1D,SAAS,gBAAgB,cAAc,uBAAuB;AAC9D,SAAS,eAAe;AA6ExB,MAAM,cAAc,oBAAI,IAA8B;AAKtD,YAAY,cAAc,CAAC,MAA0B;AACnD,QAAM,IAAI,YAAY,IAAI,EAAE,QAAQ;AACpC,MAAI,EAAG,GAAE,KAAK,EAAE,MAAM;AACxB,CAAC;AAED,MAAM,iBAAyC;AAAA,EAK7C,YAAqB,UAAkB;AAAlB;AAJrB,gCAAwB;AACxB,0CAAoB,oBAAI,IAAgB;AACxC,+BAAS;AAGP,gBAAY,IAAI,UAAU,IAAI;AAAA,EAChC;AAAA,EAEA,IAAI,SAAuB;AACzB,WAAO,mBAAK;AAAA,EACd;AAAA,EAEA,UAAgB;AACd,QAAI,mBAAK,QAAQ;AAIjB,gBAAY,gBAAgB,EAAE,UAAU,KAAK,SAAS,CAAC;AAAA,EACzD;AAAA,EAEA,UAAU,IAA4B;AACpC,QAAI,mBAAK,SAAQ;AAGf,qBAAe,EAAE;AACjB,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AACA,uBAAK,mBAAkB,IAAI,EAAE;AAC7B,WAAO,MAAM;AACX,yBAAK,mBAAkB,OAAO,EAAE;AAAA,IAClC;AAAA,EACF;AAAA;AAAA,EAGA,KAAK,QAAgD;AACnD,QAAI,mBAAK,QAAQ;AACjB,uBAAK,QAAS;AACd,uBAAK,SAAU;AACf,gBAAY,OAAO,KAAK,QAAQ;AAChC,UAAM,YAAY,CAAC,GAAG,mBAAK,kBAAiB;AAC5C,uBAAK,mBAAkB,MAAM;AAC7B,eAAW,KAAK,WAAW;AACzB,UAAI;AACF,UAAE;AAAA,MACJ,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;AAjDE;AACA;AACA;AA+DK,MAAM,SAAS,OACpB,QACA,SACiE;AAGjE,QAAM,MAAO,MAAM,gBAAgB,QAAQ,eAAe,GAAG,UAAU,CAAC,EAAE,QAAQ,KAAK,CAAC,CAAC;AAIzF,MAAI,CAAC,OAAO,IAAI,OAAO,QAAQ,CAAC,IAAI,MAAM,UAAU;AAClD,UAAM,OAAO,OAAO,IAAI,OAAO,QAAQ,IAAI,QAAQ,YAAY;AAC/D,WAAO,EAAE,IAAI,OAAO,KAAK;AAAA,EAC3B;AACA,SAAO,IAAI,iBAAiB,IAAI,KAAK,QAAQ;AAC/C;","names":[]}
|
package/dist/llm.cjs
CHANGED
|
@@ -21,6 +21,7 @@ __export(llm_exports, {
|
|
|
21
21
|
chat: () => chat,
|
|
22
22
|
describeChat: () => describeChat,
|
|
23
23
|
describeChatState: () => describeChatState,
|
|
24
|
+
normalizeProviderInfo: () => normalizeProviderInfo,
|
|
24
25
|
onChatProviderChange: () => onChatProviderChange,
|
|
25
26
|
onChatProviderStateChange: () => onChatProviderStateChange,
|
|
26
27
|
useChatProvider: () => useChatProvider,
|
|
@@ -32,11 +33,12 @@ var import_pushChannel = require("./pushChannel");
|
|
|
32
33
|
var import_protocol = require("./generated/protocol");
|
|
33
34
|
function chat(req) {
|
|
34
35
|
const { signal, ...params } = req;
|
|
35
|
-
return (0, import_catalog.invokeStream)(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
return (0, import_catalog.invokeStream)("llm:chat", params, signal);
|
|
37
|
+
}
|
|
38
|
+
function normalizeProviderInfo(provider) {
|
|
39
|
+
if (!provider) return null;
|
|
40
|
+
const wire = provider.features;
|
|
41
|
+
return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } };
|
|
40
42
|
}
|
|
41
43
|
let answered = false;
|
|
42
44
|
const channel = (0, import_pushChannel.createPushChannel)({
|
|
@@ -46,7 +48,7 @@ const channel = (0, import_pushChannel.createPushChannel)({
|
|
|
46
48
|
parse: (msg) => {
|
|
47
49
|
if (!("provider" in msg)) return void 0;
|
|
48
50
|
answered = true;
|
|
49
|
-
return msg.provider ?? null;
|
|
51
|
+
return normalizeProviderInfo(msg.provider ?? null);
|
|
50
52
|
}
|
|
51
53
|
});
|
|
52
54
|
const stateOf = (provider) => !answered ? { status: "unknown" } : provider ? { status: "configured", provider } : { status: "not-configured" };
|
|
@@ -61,6 +63,7 @@ const useChatProviderState = () => stateOf(channel.use());
|
|
|
61
63
|
chat,
|
|
62
64
|
describeChat,
|
|
63
65
|
describeChatState,
|
|
66
|
+
normalizeProviderInfo,
|
|
64
67
|
onChatProviderChange,
|
|
65
68
|
onChatProviderStateChange,
|
|
66
69
|
useChatProvider,
|
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';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\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 // NOTE (R3-300): `displayName`, `executor` and the resolved per-tier `models` belong\n // here — an app rendering provider state wants all three. They are NOT added yet,\n // deliberately: this interface IS the `llm-provider` channel's declared value, so\n // adding a field is a WIRE change, and the wire is owned by\n // `@immediately-run/sandbox-protocol` (descriptor edit → publish → pin bump on both\n // sides). The protocol snapshot gate enforces exactly that, and it is right to. The\n // enrichment rides R3-307's publish, which already has to touch those descriptors —\n // one publish for two additions rather than two.\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'configured'; provider: ChatProviderInfo };\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\", which is now REPRESENTABLE as distinct from \"not yet answered\".\n// The channel's VALUE stays exactly what the wire carries — `ChatProviderInfo | null` —\n// because the wire did not change here and the protocol snapshot gate reads this type as\n// the channel's shape. The three-state lives BESIDE it: `answered` records whether the host\n// has ever spoken on this channel, which is the one bit `null` cannot carry. Deriving the\n// state rather than widening the channel keeps the wire contract byte-identical, which it\n// is (SDK_PACKAGING_SPEC §9: the wire is additive-only, and this is not a wire change).\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return (msg.provider as ChatProviderInfo | null) ?? null;\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(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/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (\n listener: (state: ChatProviderState) => void,\n): (() => void) => channel.onChange((p) => listener(stateOf(p)));\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\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,qBAA6B;AAC7B,yBAAkC;AAClC,sBAAmD;AAiF5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,aAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAsDA,IAAI,WAAW;AACf,MAAM,cAAU,sCAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAQ,IAAI,YAAwC;AAAA,EACtD;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAClC,aACiB,QAAQ,SAAS,QAAQ;AAGrC,MAAM,4BAA4B,CACvC,aACiB,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIxD,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}
|
|
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';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\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 // A block of the model's own REASONING from a prior `assistant` turn (R3-335).\n // Honored only when the resolved provider advertises `features.reasoning`.\n //\n // Echo these back. On some providers a reasoning block must be replayed — with its\n // `signature` intact and BEFORE the turn's text/tool-use — for the following turn to\n // be accepted at all; a loop that drops them is quietly lossy across turns in a way\n // that shows up as degraded output rather than an error. Pairs with the streamed\n // `reasoning` {@link ChatDelta}.\n | { type: 'reasoning'; text: string; signature?: string }\n // Reasoning the provider REDACTED: opaque bytes with no readable text, which still\n // have to be echoed back in place to keep the chain valid. Never render it.\n | { type: 'reasoning-redacted'; data: string }\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 // R3-335 — the model's reasoning as it streams. `reasoning-delta` carries the text\n // incrementally (render it live); the terminal `reasoning` carries the WHOLE block\n // plus the `signature` the provider may require on the echo, and is what a caller\n // should put back into the conversation. A provider without reasoning emits neither.\n | { type: 'reasoning-delta'; text: string }\n | { type: 'reasoning'; text: string; signature?: string }\n | { type: 'reasoning-redacted'; data: string }\n // Token accounting for the turn. `cacheReadTokens`/`cacheWriteTokens` are present\n // only on providers that report prompt caching (R3-336) — they are what makes a\n // caching claim verifiable rather than believed, and their ABSENCE is meaningful:\n // it says this provider reports nothing, not that nothing was cached.\n | {\n type: 'usage';\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n };\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>('llm:chat', params as unknown as Record<string, unknown>, signal);\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 /** R3-335: the provider emits reasoning blocks. Read it to decide whether to render\n * a thinking surface at all — an empty affordance on a provider that never thinks\n * is worse than none. Normalized to `false` by the channel when a host predating\n * R3-335 omits it, so this is never `undefined` in practice. */\n reasoning: 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 // NOTE (R3-300): `displayName`, `executor` and the resolved per-tier `models` belong\n // here — an app rendering provider state wants all three. They are NOT added yet,\n // deliberately: this interface IS the `llm-provider` channel's declared value, so\n // adding a field is a WIRE change, and the wire is owned by\n // `@immediately-run/sandbox-protocol` (descriptor edit → publish → pin bump on both\n // sides). The protocol snapshot gate enforces exactly that, and it is right to. The\n // enrichment rides R3-307's publish, which already has to touch those descriptors —\n // one publish for two additions rather than two.\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'configured'; provider: ChatProviderInfo };\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\", which is now REPRESENTABLE as distinct from \"not yet answered\".\n// The channel's VALUE stays exactly what the wire carries — `ChatProviderInfo | null` —\n// because the wire did not change here and the protocol snapshot gate reads this type as\n// the channel's shape. The three-state lives BESIDE it: `answered` records whether the host\n// has ever spoken on this channel, which is the one bit `null` cannot carry. Deriving the\n// state rather than widening the channel keeps the wire contract byte-identical, which it\n// is (SDK_PACKAGING_SPEC §9: the wire is additive-only, and this is not a wire change).\n/**\n * Fill in feature flags a host older than the field does not send (R3-335).\n *\n * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it\n * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in\n * features` check, which is exactly the kind of difference that produces one wrong\n * branch a year later — so it is normalized here, once, rather than left to every\n * caller. Absent means \"does not reason\": the fail-closed reading.\n *\n * Exported for its own test; not part of the public surface (`index.ts` re-exports\n * this module wholesale, so it is reachable — it is documented as internal rather\n * than hidden behind a lie).\n * @internal\n */\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The wire value is whatever the host sent, which may predate `reasoning` — so read\n // it as partial rather than trusting the declared type, and decide the flag explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures };\n}\n\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(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 = (listener: (provider: ChatProviderInfo | null) => void): (() => void) =>\n channel.onChange(listener);\n\n/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (listener: (state: ChatProviderState) => void): (() => void) =>\n channel.onChange((p) => listener(stateOf(p)));\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\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,qBAA6B;AAC7B,yBAAkC;AAClC,sBAAmD;AA8G5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,aAAO,6BAAoC,YAAY,QAA8C,MAAM;AAC7G;AAyEO,SAAS,sBAAsB,UAA4D;AAChG,MAAI,CAAC,SAAU,QAAO;AAGtB,QAAM,OAAO,SAAS;AACtB,SAAO,EAAE,GAAG,UAAU,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,cAAc,KAAK,EAAkB;AAClG;AAEA,IAAI,WAAW;AACf,MAAM,cAAU,sCAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAO,sBAAuB,IAAI,YAAwC,IAAI;AAAA,EAChF;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAAC,aACnC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,4BAA4B,CAAC,aACxC,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIvC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}
|
package/dist/llm.d.cts
CHANGED
|
@@ -15,6 +15,13 @@ type ContentPart = {
|
|
|
15
15
|
id: string;
|
|
16
16
|
name: string;
|
|
17
17
|
input: Record<string, unknown>;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'reasoning';
|
|
20
|
+
text: string;
|
|
21
|
+
signature?: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'reasoning-redacted';
|
|
24
|
+
data: string;
|
|
18
25
|
} | {
|
|
19
26
|
type: 'tool-result';
|
|
20
27
|
toolCallId: string;
|
|
@@ -61,10 +68,22 @@ type ChatDelta = {
|
|
|
61
68
|
id: string;
|
|
62
69
|
name: string;
|
|
63
70
|
input: unknown;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'reasoning-delta';
|
|
73
|
+
text: string;
|
|
74
|
+
} | {
|
|
75
|
+
type: 'reasoning';
|
|
76
|
+
text: string;
|
|
77
|
+
signature?: string;
|
|
78
|
+
} | {
|
|
79
|
+
type: 'reasoning-redacted';
|
|
80
|
+
data: string;
|
|
64
81
|
} | {
|
|
65
82
|
type: 'usage';
|
|
66
83
|
inputTokens: number;
|
|
67
84
|
outputTokens: number;
|
|
85
|
+
cacheReadTokens?: number;
|
|
86
|
+
cacheWriteTokens?: number;
|
|
68
87
|
};
|
|
69
88
|
/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */
|
|
70
89
|
type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';
|
|
@@ -93,6 +112,11 @@ interface ChatFeatures {
|
|
|
93
112
|
vision: boolean;
|
|
94
113
|
tools: boolean;
|
|
95
114
|
jsonMode: boolean;
|
|
115
|
+
/** R3-335: the provider emits reasoning blocks. Read it to decide whether to render
|
|
116
|
+
* a thinking surface at all — an empty affordance on a provider that never thinks
|
|
117
|
+
* is worse than none. Normalized to `false` by the channel when a host predating
|
|
118
|
+
* R3-335 omits it, so this is never `undefined` in practice. */
|
|
119
|
+
reasoning: boolean;
|
|
96
120
|
maxContextTokens: number;
|
|
97
121
|
}
|
|
98
122
|
/** Info about the provider the host resolved for this app. `null` when no provider
|
|
@@ -122,6 +146,21 @@ type ChatProviderState = {
|
|
|
122
146
|
status: 'configured';
|
|
123
147
|
provider: ChatProviderInfo;
|
|
124
148
|
};
|
|
149
|
+
/**
|
|
150
|
+
* Fill in feature flags a host older than the field does not send (R3-335).
|
|
151
|
+
*
|
|
152
|
+
* `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it
|
|
153
|
+
* omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in
|
|
154
|
+
* features` check, which is exactly the kind of difference that produces one wrong
|
|
155
|
+
* branch a year later — so it is normalized here, once, rather than left to every
|
|
156
|
+
* caller. Absent means "does not reason": the fail-closed reading.
|
|
157
|
+
*
|
|
158
|
+
* Exported for its own test; not part of the public surface (`index.ts` re-exports
|
|
159
|
+
* this module wholesale, so it is reachable — it is documented as internal rather
|
|
160
|
+
* than hidden behind a lie).
|
|
161
|
+
* @internal
|
|
162
|
+
*/
|
|
163
|
+
declare function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null;
|
|
125
164
|
/**
|
|
126
165
|
* The provider the host resolved for this app, or `null`.
|
|
127
166
|
*
|
|
@@ -150,4 +189,4 @@ declare const useChatProvider: () => ChatProviderInfo | null;
|
|
|
150
189
|
*/
|
|
151
190
|
declare const useChatProviderState: () => ChatProviderState;
|
|
152
191
|
|
|
153
|
-
export { type ChatDelta, type ChatFeatures, type ChatMessage, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ContentPart, type ToolDef, chat, describeChat, describeChatState, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
|
|
192
|
+
export { type ChatDelta, type ChatFeatures, type ChatMessage, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ContentPart, type ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
|
package/dist/llm.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ type ContentPart = {
|
|
|
15
15
|
id: string;
|
|
16
16
|
name: string;
|
|
17
17
|
input: Record<string, unknown>;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'reasoning';
|
|
20
|
+
text: string;
|
|
21
|
+
signature?: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'reasoning-redacted';
|
|
24
|
+
data: string;
|
|
18
25
|
} | {
|
|
19
26
|
type: 'tool-result';
|
|
20
27
|
toolCallId: string;
|
|
@@ -61,10 +68,22 @@ type ChatDelta = {
|
|
|
61
68
|
id: string;
|
|
62
69
|
name: string;
|
|
63
70
|
input: unknown;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'reasoning-delta';
|
|
73
|
+
text: string;
|
|
74
|
+
} | {
|
|
75
|
+
type: 'reasoning';
|
|
76
|
+
text: string;
|
|
77
|
+
signature?: string;
|
|
78
|
+
} | {
|
|
79
|
+
type: 'reasoning-redacted';
|
|
80
|
+
data: string;
|
|
64
81
|
} | {
|
|
65
82
|
type: 'usage';
|
|
66
83
|
inputTokens: number;
|
|
67
84
|
outputTokens: number;
|
|
85
|
+
cacheReadTokens?: number;
|
|
86
|
+
cacheWriteTokens?: number;
|
|
68
87
|
};
|
|
69
88
|
/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */
|
|
70
89
|
type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';
|
|
@@ -93,6 +112,11 @@ interface ChatFeatures {
|
|
|
93
112
|
vision: boolean;
|
|
94
113
|
tools: boolean;
|
|
95
114
|
jsonMode: boolean;
|
|
115
|
+
/** R3-335: the provider emits reasoning blocks. Read it to decide whether to render
|
|
116
|
+
* a thinking surface at all — an empty affordance on a provider that never thinks
|
|
117
|
+
* is worse than none. Normalized to `false` by the channel when a host predating
|
|
118
|
+
* R3-335 omits it, so this is never `undefined` in practice. */
|
|
119
|
+
reasoning: boolean;
|
|
96
120
|
maxContextTokens: number;
|
|
97
121
|
}
|
|
98
122
|
/** Info about the provider the host resolved for this app. `null` when no provider
|
|
@@ -122,6 +146,21 @@ type ChatProviderState = {
|
|
|
122
146
|
status: 'configured';
|
|
123
147
|
provider: ChatProviderInfo;
|
|
124
148
|
};
|
|
149
|
+
/**
|
|
150
|
+
* Fill in feature flags a host older than the field does not send (R3-335).
|
|
151
|
+
*
|
|
152
|
+
* `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it
|
|
153
|
+
* omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in
|
|
154
|
+
* features` check, which is exactly the kind of difference that produces one wrong
|
|
155
|
+
* branch a year later — so it is normalized here, once, rather than left to every
|
|
156
|
+
* caller. Absent means "does not reason": the fail-closed reading.
|
|
157
|
+
*
|
|
158
|
+
* Exported for its own test; not part of the public surface (`index.ts` re-exports
|
|
159
|
+
* this module wholesale, so it is reachable — it is documented as internal rather
|
|
160
|
+
* than hidden behind a lie).
|
|
161
|
+
* @internal
|
|
162
|
+
*/
|
|
163
|
+
declare function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null;
|
|
125
164
|
/**
|
|
126
165
|
* The provider the host resolved for this app, or `null`.
|
|
127
166
|
*
|
|
@@ -150,4 +189,4 @@ declare const useChatProvider: () => ChatProviderInfo | null;
|
|
|
150
189
|
*/
|
|
151
190
|
declare const useChatProviderState: () => ChatProviderState;
|
|
152
191
|
|
|
153
|
-
export { type ChatDelta, type ChatFeatures, type ChatMessage, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ContentPart, type ToolDef, chat, describeChat, describeChatState, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
|
|
192
|
+
export { type ChatDelta, type ChatFeatures, type ChatMessage, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ContentPart, type ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
|
package/dist/llm.js
CHANGED
|
@@ -4,11 +4,12 @@ import { createPushChannel } from "./pushChannel";
|
|
|
4
4
|
import { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from "./generated/protocol";
|
|
5
5
|
function chat(req) {
|
|
6
6
|
const { signal, ...params } = req;
|
|
7
|
-
return invokeStream(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
return invokeStream("llm:chat", params, signal);
|
|
8
|
+
}
|
|
9
|
+
function normalizeProviderInfo(provider) {
|
|
10
|
+
if (!provider) return null;
|
|
11
|
+
const wire = provider.features;
|
|
12
|
+
return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } };
|
|
12
13
|
}
|
|
13
14
|
let answered = false;
|
|
14
15
|
const channel = createPushChannel({
|
|
@@ -18,7 +19,7 @@ const channel = createPushChannel({
|
|
|
18
19
|
parse: (msg) => {
|
|
19
20
|
if (!("provider" in msg)) return void 0;
|
|
20
21
|
answered = true;
|
|
21
|
-
return msg.provider ?? null;
|
|
22
|
+
return normalizeProviderInfo(msg.provider ?? null);
|
|
22
23
|
}
|
|
23
24
|
});
|
|
24
25
|
const stateOf = (provider) => !answered ? { status: "unknown" } : provider ? { status: "configured", provider } : { status: "not-configured" };
|
|
@@ -32,6 +33,7 @@ export {
|
|
|
32
33
|
chat,
|
|
33
34
|
describeChat,
|
|
34
35
|
describeChatState,
|
|
36
|
+
normalizeProviderInfo,
|
|
35
37
|
onChatProviderChange,
|
|
36
38
|
onChatProviderStateChange,
|
|
37
39
|
useChatProvider,
|
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';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\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 // NOTE (R3-300): `displayName`, `executor` and the resolved per-tier `models` belong\n // here — an app rendering provider state wants all three. They are NOT added yet,\n // deliberately: this interface IS the `llm-provider` channel's declared value, so\n // adding a field is a WIRE change, and the wire is owned by\n // `@immediately-run/sandbox-protocol` (descriptor edit → publish → pin bump on both\n // sides). The protocol snapshot gate enforces exactly that, and it is right to. The\n // enrichment rides R3-307's publish, which already has to touch those descriptors —\n // one publish for two additions rather than two.\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'configured'; provider: ChatProviderInfo };\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\", which is now REPRESENTABLE as distinct from \"not yet answered\".\n// The channel's VALUE stays exactly what the wire carries — `ChatProviderInfo | null` —\n// because the wire did not change here and the protocol snapshot gate reads this type as\n// the channel's shape. The three-state lives BESIDE it: `answered` records whether the host\n// has ever spoken on this channel, which is the one bit `null` cannot carry. Deriving the\n// state rather than widening the channel keeps the wire contract byte-identical, which it\n// is (SDK_PACKAGING_SPEC §9: the wire is additive-only, and this is not a wire change).\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return (msg.provider as ChatProviderInfo | null) ?? null;\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(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/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (\n listener: (state: ChatProviderState) => void,\n): (() => void) => channel.onChange((p) => listener(stateOf(p)));\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\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";AAeA,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,cAAc,4BAA4B;AAiF5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAsDA,IAAI,WAAW;AACf,MAAM,UAAU,kBAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAQ,IAAI,YAAwC;AAAA,EACtD;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAClC,aACiB,QAAQ,SAAS,QAAQ;AAGrC,MAAM,4BAA4B,CACvC,aACiB,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIxD,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}
|
|
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';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\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 // A block of the model's own REASONING from a prior `assistant` turn (R3-335).\n // Honored only when the resolved provider advertises `features.reasoning`.\n //\n // Echo these back. On some providers a reasoning block must be replayed — with its\n // `signature` intact and BEFORE the turn's text/tool-use — for the following turn to\n // be accepted at all; a loop that drops them is quietly lossy across turns in a way\n // that shows up as degraded output rather than an error. Pairs with the streamed\n // `reasoning` {@link ChatDelta}.\n | { type: 'reasoning'; text: string; signature?: string }\n // Reasoning the provider REDACTED: opaque bytes with no readable text, which still\n // have to be echoed back in place to keep the chain valid. Never render it.\n | { type: 'reasoning-redacted'; data: string }\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 // R3-335 — the model's reasoning as it streams. `reasoning-delta` carries the text\n // incrementally (render it live); the terminal `reasoning` carries the WHOLE block\n // plus the `signature` the provider may require on the echo, and is what a caller\n // should put back into the conversation. A provider without reasoning emits neither.\n | { type: 'reasoning-delta'; text: string }\n | { type: 'reasoning'; text: string; signature?: string }\n | { type: 'reasoning-redacted'; data: string }\n // Token accounting for the turn. `cacheReadTokens`/`cacheWriteTokens` are present\n // only on providers that report prompt caching (R3-336) — they are what makes a\n // caching claim verifiable rather than believed, and their ABSENCE is meaningful:\n // it says this provider reports nothing, not that nothing was cached.\n | {\n type: 'usage';\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n };\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>('llm:chat', params as unknown as Record<string, unknown>, signal);\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 /** R3-335: the provider emits reasoning blocks. Read it to decide whether to render\n * a thinking surface at all — an empty affordance on a provider that never thinks\n * is worse than none. Normalized to `false` by the channel when a host predating\n * R3-335 omits it, so this is never `undefined` in practice. */\n reasoning: 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 // NOTE (R3-300): `displayName`, `executor` and the resolved per-tier `models` belong\n // here — an app rendering provider state wants all three. They are NOT added yet,\n // deliberately: this interface IS the `llm-provider` channel's declared value, so\n // adding a field is a WIRE change, and the wire is owned by\n // `@immediately-run/sandbox-protocol` (descriptor edit → publish → pin bump on both\n // sides). The protocol snapshot gate enforces exactly that, and it is right to. The\n // enrichment rides R3-307's publish, which already has to touch those descriptors —\n // one publish for two additions rather than two.\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'configured'; provider: ChatProviderInfo };\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\", which is now REPRESENTABLE as distinct from \"not yet answered\".\n// The channel's VALUE stays exactly what the wire carries — `ChatProviderInfo | null` —\n// because the wire did not change here and the protocol snapshot gate reads this type as\n// the channel's shape. The three-state lives BESIDE it: `answered` records whether the host\n// has ever spoken on this channel, which is the one bit `null` cannot carry. Deriving the\n// state rather than widening the channel keeps the wire contract byte-identical, which it\n// is (SDK_PACKAGING_SPEC §9: the wire is additive-only, and this is not a wire change).\n/**\n * Fill in feature flags a host older than the field does not send (R3-335).\n *\n * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it\n * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in\n * features` check, which is exactly the kind of difference that produces one wrong\n * branch a year later — so it is normalized here, once, rather than left to every\n * caller. Absent means \"does not reason\": the fail-closed reading.\n *\n * Exported for its own test; not part of the public surface (`index.ts` re-exports\n * this module wholesale, so it is reachable — it is documented as internal rather\n * than hidden behind a lie).\n * @internal\n */\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The wire value is whatever the host sent, which may predate `reasoning` — so read\n // it as partial rather than trusting the declared type, and decide the flag explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures };\n}\n\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(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 = (listener: (provider: ChatProviderInfo | null) => void): (() => void) =>\n channel.onChange(listener);\n\n/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (listener: (state: ChatProviderState) => void): (() => void) =>\n channel.onChange((p) => listener(stateOf(p)));\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\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";AAeA,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,cAAc,4BAA4B;AA8G5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,SAAO,aAAoC,YAAY,QAA8C,MAAM;AAC7G;AAyEO,SAAS,sBAAsB,UAA4D;AAChG,MAAI,CAAC,SAAU,QAAO;AAGtB,QAAM,OAAO,SAAS;AACtB,SAAO,EAAE,GAAG,UAAU,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,cAAc,KAAK,EAAkB;AAClG;AAEA,IAAI,WAAW;AACf,MAAM,UAAU,kBAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAO,sBAAuB,IAAI,YAAwC,IAAI;AAAA,EAChF;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAAC,aACnC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,4BAA4B,CAAC,aACxC,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIvC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}
|
package/dist/loading.cjs
CHANGED
|
@@ -67,22 +67,12 @@ function SkeletonRow({
|
|
|
67
67
|
style
|
|
68
68
|
}) {
|
|
69
69
|
useLoadingStyles();
|
|
70
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
71
|
-
"div",
|
|
72
|
-
{
|
|
73
|
-
className: "ir-sdk-shim",
|
|
74
|
-
"aria-hidden": "true",
|
|
75
|
-
style: { ...PLACEHOLDER, width, height, ...style }
|
|
76
|
-
}
|
|
77
|
-
);
|
|
70
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ir-sdk-shim", "aria-hidden": "true", style: { ...PLACEHOLDER, width, height, ...style } });
|
|
78
71
|
}
|
|
79
72
|
function rows(specs) {
|
|
80
73
|
return specs.map(({ key, ...s }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ir-sdk-shim", "aria-hidden": "true", style: { ...PLACEHOLDER, ...s } }, key));
|
|
81
74
|
}
|
|
82
|
-
function Skeleton({
|
|
83
|
-
archetype = "generic",
|
|
84
|
-
style
|
|
85
|
-
}) {
|
|
75
|
+
function Skeleton({ archetype = "generic", style }) {
|
|
86
76
|
useLoadingStyles();
|
|
87
77
|
const wrap = {
|
|
88
78
|
display: "flex",
|
|
@@ -118,7 +108,14 @@ function Skeleton({
|
|
|
118
108
|
case "generic":
|
|
119
109
|
default:
|
|
120
110
|
body = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
121
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
111
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
112
|
+
"div",
|
|
113
|
+
{
|
|
114
|
+
className: "ir-sdk-shim",
|
|
115
|
+
"aria-hidden": "true",
|
|
116
|
+
style: { ...PLACEHOLDER, height: 16, width: "45%", borderRadius: 4 }
|
|
117
|
+
}
|
|
118
|
+
),
|
|
122
119
|
rows([0, 1].map((key) => ({ key, height: 56, borderRadius: 8 })))
|
|
123
120
|
] });
|
|
124
121
|
}
|
|
@@ -170,7 +167,14 @@ function LoadingRegion({
|
|
|
170
167
|
"div",
|
|
171
168
|
{
|
|
172
169
|
"aria-busy": "true",
|
|
173
|
-
style: {
|
|
170
|
+
style: {
|
|
171
|
+
display: "flex",
|
|
172
|
+
alignItems: "center",
|
|
173
|
+
justifyContent: "center",
|
|
174
|
+
width: "100%",
|
|
175
|
+
height: "100%",
|
|
176
|
+
minHeight: 48
|
|
177
|
+
},
|
|
174
178
|
children: fallback ?? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { label })
|
|
175
179
|
}
|
|
176
180
|
);
|