@immediately-run/sdk 0.71.0 → 0.72.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/index.d.cts CHANGED
@@ -33,7 +33,7 @@ export { SecretError, SecretGrant, SecretHints, SecretQuery, SecretType, SecretV
33
33
  export { RecentProject, clearRecentProjects, listRecentProjects } from './recents.cjs';
34
34
  export { OpenRepositoryError, OpenRepositoryErrorCode, RepositoryCoordinates, openRepository } from './openRepository.cjs';
35
35
  export { OpenExternalError, OpenExternalErrorCode, openExternal } from './openExternal.cjs';
36
- export { ChatDelta, ChatExecutor, ChatFeatures, ChatMessage, ChatProviderChoice, ChatProviderInfo, ChatProviderState, ChatRequest, ChatResult, ChatRole, ChatStopReason, ChatTierModels, ContentPart, ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState } from './llm.cjs';
36
+ export { ChatDelta, ChatExecutor, ChatFeatures, ChatMessage, ChatProviderChoice, ChatProviderInfo, ChatProviderState, ChatRequest, ChatResult, ChatRole, ChatStopReason, ChatTierModels, ContentPart, ToolDef, chat, deriveChatProviderState, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState } from './llm.cjs';
37
37
  export { BuildError, ConsoleEntry, ConsoleLevel, Diagnostics, DiagnosticsProvenance, getDiagnostics, onDiagnosticsChange, useDiagnostics } from './diagnostics.cjs';
38
38
  export { VcsActionError, VcsBranch, VcsChange, VcsPR, VcsState, getVcsState, onVcsStateChange, refreshDiff, refreshPRs, resetWorkingTree, useVcsState } from './vcs.cjs';
39
39
  export { FsChange, MountChange, getFsChange, onFsChange, useFsChange } from './onFsChange.cjs';
package/dist/index.d.ts CHANGED
@@ -33,7 +33,7 @@ export { SecretError, SecretGrant, SecretHints, SecretQuery, SecretType, SecretV
33
33
  export { RecentProject, clearRecentProjects, listRecentProjects } from './recents.js';
34
34
  export { OpenRepositoryError, OpenRepositoryErrorCode, RepositoryCoordinates, openRepository } from './openRepository.js';
35
35
  export { OpenExternalError, OpenExternalErrorCode, openExternal } from './openExternal.js';
36
- export { ChatDelta, ChatExecutor, ChatFeatures, ChatMessage, ChatProviderChoice, ChatProviderInfo, ChatProviderState, ChatRequest, ChatResult, ChatRole, ChatStopReason, ChatTierModels, ContentPart, ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState } from './llm.js';
36
+ export { ChatDelta, ChatExecutor, ChatFeatures, ChatMessage, ChatProviderChoice, ChatProviderInfo, ChatProviderState, ChatRequest, ChatResult, ChatRole, ChatStopReason, ChatTierModels, ContentPart, ToolDef, chat, deriveChatProviderState, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState } from './llm.js';
37
37
  export { BuildError, ConsoleEntry, ConsoleLevel, Diagnostics, DiagnosticsProvenance, getDiagnostics, onDiagnosticsChange, useDiagnostics } from './diagnostics.js';
38
38
  export { VcsActionError, VcsBranch, VcsChange, VcsPR, VcsState, getVcsState, onVcsStateChange, refreshDiff, refreshPRs, resetWorkingTree, useVcsState } from './vcs.js';
39
39
  export { FsChange, MountChange, getFsChange, onFsChange, useFsChange } from './onFsChange.js';
package/dist/llm.cjs CHANGED
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var llm_exports = {};
20
20
  __export(llm_exports, {
21
21
  chat: () => chat,
22
+ deriveChatProviderState: () => deriveChatProviderState,
22
23
  describeChat: () => describeChat,
23
24
  describeChatState: () => describeChatState,
24
25
  normalizeProviderInfo: () => normalizeProviderInfo,
@@ -78,6 +79,7 @@ function normalizeProviderInfo(provider) {
78
79
  };
79
80
  }
80
81
  let answered = false;
82
+ let ungrantedMark = false;
81
83
  const channel = (0, import_pushChannel.createPushChannel)({
82
84
  pushType: import_protocol.LLM_PROVIDER,
83
85
  requestType: import_protocol.REQUEST_LLM_PROVIDER,
@@ -85,10 +87,16 @@ const channel = (0, import_pushChannel.createPushChannel)({
85
87
  parse: (msg) => {
86
88
  if (!("provider" in msg)) return void 0;
87
89
  answered = true;
90
+ ungrantedMark = msg.ungranted === true;
88
91
  return normalizeProviderInfo(msg.provider ?? null);
89
92
  }
90
93
  });
91
- const stateOf = (provider) => !answered ? { status: "unknown" } : provider ? { status: "configured", provider } : { status: "not-configured" };
94
+ function deriveChatProviderState(answered2, ungranted, provider) {
95
+ if (!answered2) return { status: "unknown" };
96
+ if (ungranted) return { status: "ungranted" };
97
+ return provider ? { status: "configured", provider } : { status: "not-configured" };
98
+ }
99
+ const stateOf = (provider) => deriveChatProviderState(answered, ungrantedMark, provider);
92
100
  const describeChat = () => channel.get();
93
101
  const describeChatState = () => stateOf(channel.get());
94
102
  const onChatProviderChange = (listener) => channel.onChange(listener);
@@ -98,6 +106,7 @@ const useChatProviderState = () => stateOf(channel.use());
98
106
  // Annotate the CommonJS export names for ESM import in node:
99
107
  0 && (module.exports = {
100
108
  chat,
109
+ deriveChatProviderState,
101
110
  describeChat,
102
111
  describeChatState,
103
112
  normalizeProviderInfo,
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 // 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 /** A concrete provider-and-model choice, naming one of the user's CONNECTED providers\n * (R3-620, LLM_AND_AGENTS_SPEC §0 editing-session exception). When present it WINS over\n * `modelHint`; the host validates the pair against the user's connected set and refuses\n * with `provider-not-connected` otherwise. Only an editing-session principal may read the\n * chooseable set (via `describeChat()`'s `connectedProviders`, gated `llm:chooseModel`);\n * a stage app still passes at most the abstract hint. */\n model?: { providerId: string; model: string };\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 first\n * draws the SP-7 connect-me gate itself (R3-456: the app never draws a\n * credential prompt — that is host chrome, SECRETS_SPEC S3):\n * - the user connects a key → the call retries once and streams normally;\n * - the user declines → the generator throws `code: 'cancelled'` (the same code\n * a declined powerbox produces — a working degraded state: catch it and\n * degrade, e.g. skip the AI feature);\n * - an older host without the gate throws `code: 'provider-not-configured'`.\n * A signed-out user throws `code: 'auth-required'`; an un-granted call throws\n * `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/** How the resolved provider's requests physically leave the browser. Read it to\n * DESCRIBE routing where that matters to the person (\"runs in your browser\" vs\n * \"routed through immediately.run\") — never to draw host chrome or a consent prompt,\n * which remain the host's (UI_AS_APPS §8 T15). */\nexport type ChatExecutor = 'browser-direct' | 'backend-proxied';\n\n/** The concrete model each abstract tier resolves to right now. */\nexport interface ChatTierModels {\n fast: string;\n smart: string;\n}\n\n/** One CONNECTED provider the user may choose a model from, for a per-conversation model\n * choice in an editing-session workbench (R3-620, LLM_AND_AGENTS_SPEC §0). Names and ids\n * only — never keys, usage, balance or routing. */\nexport interface ChatProviderChoice {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — matches {@link ChatRequest.model}.providerId. */\n providerId: string;\n /** The provider's human name, for a picker label. */\n displayName: string;\n /** The chooseable model names — the catalogue-recommended set plus the two the user has\n * chosen. A closed list here would re-introduce the ids-rot problem, so `model` is passed\n * through to the adapter exactly as the Settings field is; this list is suggestions. */\n models: string[];\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 /** The provider's human name, e.g. `OpenRouter` — what to put in front of a person.\n * Absent on a host that predates the field: fall back to your own copy rather than\n * rendering the id, which is a platform identifier and not a name. */\n displayName?: string;\n /** How this provider's requests leave the browser. Absent on a host that predates the\n * field, which is NOT the same as `browser-direct` — say nothing about routing rather\n * than guess at it. */\n executor?: ChatExecutor;\n /**\n * The concrete model each {@link ChatRequest.modelHint} tier resolves to — what a\n * `smart` request would actually run, after the user's own preference.\n *\n * Read-only, and it does not weaken `LLM_AND_AGENTS_SPEC §0`: an app still names no\n * model, and {@link ChatRequest} still carries only the abstract hint. It is here so an\n * app can be HONEST about what answered — a transcript that says which model wrote a\n * reply, a warning that names the model about to be spent on — instead of showing a\n * blank where the platform knows the answer. The user picks the model in host settings;\n * the app reports it.\n *\n * Absent on a host that predates the field. It changes when the user changes their\n * preference, so read it through {@link onChatProviderChange} rather than caching it.\n */\n models?: ChatTierModels;\n /**\n * The user's CONNECTED providers and their chooseable models, for a per-conversation\n * model choice (R3-620). Only present when this frame holds the ELEVATED `llm:chooseModel`\n * capability (an editing-session workbench); the host strips it for everyone else, so a\n * stage app sees `undefined` and can offer only the abstract `modelHint` path.\n *\n * Read-only: names and ids only. No keys, usage, balance or routing. Absent also on a host\n * that predates the field.\n */\n connectedProviders?: ChatProviderChoice[];\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 *\n * **`unknown` is TRANSIENT — the host answers every frame** (R3-419;\n * `LLM_AND_AGENTS_SPEC §4.1` R-LLM-1..3). An app that does not hold `llm:chat` is not\n * met with silence: it is answered `not-configured`, the same terminal state as a user\n * with no key, because from the app's side those are the same fact — do not render a\n * provider, do offer the connect path. So it is correct to treat a `unknown` that\n * persists as a host bug rather than as a state to design around, and WRONG to render a\n * spinner with no timeout on it. (Before R3-419 the host withheld the channel entirely\n * from an ungranted frame, and `unknown` stood forever — that is the failure this note\n * exists to keep from being re-created on the app side.)\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 * Reconcile what the host actually sent with what this SDK declares.\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 * `displayName`, `executor` and `models` arrived later still, and for them absence is a\n * REAL answer an app is told to handle (\"this host does not say\"), so they are left\n * absent rather than filled in. What is dropped is a value that is present but not\n * usable — an `executor` outside the union, a `models` missing a tier — because a\n * half-answer rendered as fact is worse than the honest blank the app already handles.\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 */\nconst EXECUTORS: readonly ChatExecutor[] = ['browser-direct', 'backend-proxied'];\n\nconst usableModels = (raw: unknown): ChatTierModels | undefined => {\n if (!raw || typeof raw !== 'object') return undefined;\n const { fast, smart } = raw as Partial<ChatTierModels>;\n return typeof fast === 'string' && fast && typeof smart === 'string' && smart ? { fast, smart } : undefined;\n};\n\n/** Validate the wire's `connectedProviders` list, keeping only usable entries. The gating\n * (whether the list arrives at all) is the host's — `normalizeProviderInfo` merely refuses\n * to pass through a malformed list, exactly as it refuses a half-answered `models` pair. */\nconst usableConnectedProviders = (raw: unknown): ChatProviderChoice[] | undefined => {\n if (!Array.isArray(raw)) return undefined;\n const out: ChatProviderChoice[] = [];\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue;\n const { providerId, displayName, models } = item as Partial<ChatProviderChoice>;\n if (typeof providerId !== 'string' || !providerId) continue;\n if (typeof displayName !== 'string' || !displayName) continue;\n const cleanModels = Array.isArray(models) ? models.filter((m): m is string => typeof m === 'string' && !!m) : [];\n out.push({ providerId, displayName, models: cleanModels });\n }\n return out.length > 0 ? out : undefined;\n};\n\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The LATER fields (displayName/executor/models/connectedProviders) are taken OFF the value\n // and put back only if usable — spreading and then overwriting would leave an unusable key\n // present, and `key in provider` is exactly how an app is told to ask whether the host said\n // anything.\n const {\n displayName: rawName,\n executor: rawExecutor,\n models: rawModels,\n connectedProviders: rawConnected,\n ...rest\n } = provider;\n // The wire value is whatever the host sent, which may predate any of these fields — so\n // read it as partial rather than trusting the declared type, and decide each explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n const executor = EXECUTORS.includes(rawExecutor as ChatExecutor) ? (rawExecutor as ChatExecutor) : undefined;\n const displayName = typeof rawName === 'string' && rawName ? rawName : undefined;\n const models = usableModels(rawModels);\n const connectedProviders = usableConnectedProviders(rawConnected);\n return {\n ...rest,\n features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures,\n ...(displayName ? { displayName } : {}),\n ...(executor ? { executor } : {}),\n ...(models ? { models } : {}),\n ...(connectedProviders ? { connectedProviders } : {}),\n };\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":"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","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 /** A concrete provider-and-model choice, naming one of the user's CONNECTED providers\n * (R3-620, LLM_AND_AGENTS_SPEC §0 editing-session exception). When present it WINS over\n * `modelHint`; the host validates the pair against the user's connected set and refuses\n * with `provider-not-connected` otherwise. Only an editing-session principal may read the\n * chooseable set (via `describeChat()`'s `connectedProviders`, gated `llm:chooseModel`);\n * a stage app still passes at most the abstract hint. */\n model?: { providerId: string; model: string };\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 first\n * draws the SP-7 connect-me gate itself (R3-456: the app never draws a\n * credential prompt — that is host chrome, SECRETS_SPEC S3):\n * - the user connects a key → the call retries once and streams normally;\n * - the user declines → the generator throws `code: 'cancelled'` (the same code\n * a declined powerbox produces — a working degraded state: catch it and\n * degrade, e.g. skip the AI feature);\n * - an older host without the gate throws `code: 'provider-not-configured'`.\n * A signed-out user throws `code: 'auth-required'`; an un-granted call throws\n * `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/** How the resolved provider's requests physically leave the browser. Read it to\n * DESCRIBE routing where that matters to the person (\"runs in your browser\" vs\n * \"routed through immediately.run\") — never to draw host chrome or a consent prompt,\n * which remain the host's (UI_AS_APPS §8 T15). */\nexport type ChatExecutor = 'browser-direct' | 'backend-proxied';\n\n/** The concrete model each abstract tier resolves to right now. */\nexport interface ChatTierModels {\n fast: string;\n smart: string;\n}\n\n/** One CONNECTED provider the user may choose a model from, for a per-conversation model\n * choice in an editing-session workbench (R3-620, LLM_AND_AGENTS_SPEC §0). Names and ids\n * only — never keys, usage, balance or routing. */\nexport interface ChatProviderChoice {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — matches {@link ChatRequest.model}.providerId. */\n providerId: string;\n /** The provider's human name, for a picker label. */\n displayName: string;\n /** The chooseable model names — the catalogue-recommended set plus the two the user has\n * chosen. A closed list here would re-introduce the ids-rot problem, so `model` is passed\n * through to the adapter exactly as the Settings field is; this list is suggestions. */\n models: string[];\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 /** The provider's human name, e.g. `OpenRouter` — what to put in front of a person.\n * Absent on a host that predates the field: fall back to your own copy rather than\n * rendering the id, which is a platform identifier and not a name. */\n displayName?: string;\n /** How this provider's requests leave the browser. Absent on a host that predates the\n * field, which is NOT the same as `browser-direct` — say nothing about routing rather\n * than guess at it. */\n executor?: ChatExecutor;\n /**\n * The concrete model each {@link ChatRequest.modelHint} tier resolves to — what a\n * `smart` request would actually run, after the user's own preference.\n *\n * Read-only, and it does not weaken `LLM_AND_AGENTS_SPEC §0`: an app still names no\n * model, and {@link ChatRequest} still carries only the abstract hint. It is here so an\n * app can be HONEST about what answered — a transcript that says which model wrote a\n * reply, a warning that names the model about to be spent on — instead of showing a\n * blank where the platform knows the answer. The user picks the model in host settings;\n * the app reports it.\n *\n * Absent on a host that predates the field. It changes when the user changes their\n * preference, so read it through {@link onChatProviderChange} rather than caching it.\n */\n models?: ChatTierModels;\n /**\n * The user's CONNECTED providers and their chooseable models, for a per-conversation\n * model choice (R3-620). Only present when this frame holds the ELEVATED `llm:chooseModel`\n * capability (an editing-session workbench); the host strips it for everyone else, so a\n * stage app sees `undefined` and can offer only the abstract `modelHint` path.\n *\n * Read-only: names and ids only. No keys, usage, balance or routing. Absent also on a host\n * that predates the field.\n */\n connectedProviders?: ChatProviderChoice[];\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound —\n * and, since R3-688, whether this frame may ask at all.\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 *\n * **`unknown` is TRANSIENT — the host answers every frame** (R3-419;\n * `LLM_AND_AGENTS_SPEC §4.1` R-LLM-1..3). An app that does not hold `llm:chat` is not\n * met with silence. So it is correct to treat a `unknown` that persists as a host bug\n * rather than as a state to design around, and WRONG to render a spinner with no\n * timeout on it. (Before R3-419 the host withheld the channel entirely from an\n * ungranted frame, and `unknown` stood forever — that is the failure this note exists\n * to keep from being re-created on the app side.)\n *\n * The FOURTH state, `ungranted` (R3-688; `LLM_AND_AGENTS_SPEC §4.1` R-LLM-2 as\n * annotated, `GROVE_AGENT_SPEC` G-GA-10): the host answered, and the reason this frame\n * cannot reach a model is that **it does not hold `llm:chat`** — not that the user has\n * no key. R3-419 deliberately answered an ungranted frame with the same payload as a\n * keyless one, because the SDK could not carry the difference; that made the distinct\n * \"not granted\" cause the reach-card contract promises (G-GA-10) uncomputable — an\n * ungranted fork of an app rendered \"connect a key\" at a user who had one. The host now\n * marks the grantless answer with an optional `ungranted: true` on the SAME message\n * (additive — an older SDK never reads the field and degrades to `not-configured`;\n * an older host never sends the field and the app degrades the same way). The mark is\n * HOST-derived from the frame's resolved grant set at push time (the channel view in\n * `channelAcl.ts`), never from anything this frame asserts.\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'ungranted' }\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 protocol snapshot gate reads this type as the channel's shape. The state\n// read lives BESIDE it: `answered` records whether the host has ever spoken on this\n// channel, which is the one bit `null` cannot carry, and `ungrantedMark` (R3-688) reads\n// the host's optional grantless marker off the same message — also beside the value, not\n// inside it. Deriving the state rather than widening the channel keeps the declared wire\n// VALUE unchanged (SDK_PACKAGING_SPEC §9: additive-only; the optional message field\n// follows the `theme`/`form-factor` insets precedent — old readers ignore it, and the\n// sandbox-protocol descriptor's \"read off\" line catches up at its next release).\n/**\n * Reconcile what the host actually sent with what this SDK declares.\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 * `displayName`, `executor` and `models` arrived later still, and for them absence is a\n * REAL answer an app is told to handle (\"this host does not say\"), so they are left\n * absent rather than filled in. What is dropped is a value that is present but not\n * usable — an `executor` outside the union, a `models` missing a tier — because a\n * half-answer rendered as fact is worse than the honest blank the app already handles.\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 */\nconst EXECUTORS: readonly ChatExecutor[] = ['browser-direct', 'backend-proxied'];\n\nconst usableModels = (raw: unknown): ChatTierModels | undefined => {\n if (!raw || typeof raw !== 'object') return undefined;\n const { fast, smart } = raw as Partial<ChatTierModels>;\n return typeof fast === 'string' && fast && typeof smart === 'string' && smart ? { fast, smart } : undefined;\n};\n\n/** Validate the wire's `connectedProviders` list, keeping only usable entries. The gating\n * (whether the list arrives at all) is the host's — `normalizeProviderInfo` merely refuses\n * to pass through a malformed list, exactly as it refuses a half-answered `models` pair. */\nconst usableConnectedProviders = (raw: unknown): ChatProviderChoice[] | undefined => {\n if (!Array.isArray(raw)) return undefined;\n const out: ChatProviderChoice[] = [];\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue;\n const { providerId, displayName, models } = item as Partial<ChatProviderChoice>;\n if (typeof providerId !== 'string' || !providerId) continue;\n if (typeof displayName !== 'string' || !displayName) continue;\n const cleanModels = Array.isArray(models) ? models.filter((m): m is string => typeof m === 'string' && !!m) : [];\n out.push({ providerId, displayName, models: cleanModels });\n }\n return out.length > 0 ? out : undefined;\n};\n\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The LATER fields (displayName/executor/models/connectedProviders) are taken OFF the value\n // and put back only if usable — spreading and then overwriting would leave an unusable key\n // present, and `key in provider` is exactly how an app is told to ask whether the host said\n // anything.\n const {\n displayName: rawName,\n executor: rawExecutor,\n models: rawModels,\n connectedProviders: rawConnected,\n ...rest\n } = provider;\n // The wire value is whatever the host sent, which may predate any of these fields — so\n // read it as partial rather than trusting the declared type, and decide each explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n const executor = EXECUTORS.includes(rawExecutor as ChatExecutor) ? (rawExecutor as ChatExecutor) : undefined;\n const displayName = typeof rawName === 'string' && rawName ? rawName : undefined;\n const models = usableModels(rawModels);\n const connectedProviders = usableConnectedProviders(rawConnected);\n return {\n ...rest,\n features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures,\n ...(displayName ? { displayName } : {}),\n ...(executor ? { executor } : {}),\n ...(models ? { models } : {}),\n ...(connectedProviders ? { connectedProviders } : {}),\n };\n}\n\nlet answered = false;\n// R3-688 — the host's grantless marker, read off the SAME message (`ungranted: true`\n// beside `provider`). It is message-arrival state beside `answered`, not part of the\n// wire VALUE: the channel's value stays `ChatProviderInfo | null`, so the protocol\n// snapshot gate keeps reading the unchanged shape.\nlet ungrantedMark = 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 ungrantedMark = msg.ungranted === true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/**\n * Derive the four-state read from what the host has said. Pure, and exported for its\n * own test (`llmProviderState.test.ts`) like `normalizeProviderInfo` — not part of the\n * public surface beyond `index.ts`'s wholesale re-export.\n *\n * Order is load-bearing: `unknown` wins until the host answers (an `ungranted` mark\n * cannot exist before an answer — it arrives ON the answer); then the host's grantless\n * mark; then the provider. A host that predates the mark never sets it, so its\n * grantless answer derives `not-configured` — the exact pre-R3-688 contract, which is\n * the additive degradation the fourth state promises.\n *\n * @internal\n */\nexport function deriveChatProviderState(\n answered: boolean,\n ungranted: boolean,\n provider: ChatProviderInfo | null,\n): ChatProviderState {\n if (!answered) return { status: 'unknown' };\n if (ungranted) return { status: 'ungranted' };\n return provider ? { status: 'configured', provider } : { status: 'not-configured' };\n}\n\n/** Derive the state from the wire value plus what the host has said beside it. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n deriveChatProviderState(answered, ungrantedMark, provider);\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 four-state read: `unknown` before the host answers; then the host's `ungranted`\n * mark, or the provider (configured / not-configured). */\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 provider-state 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 four-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`, the NOT-GRANTED\n * cause — never the connect copy — on `ungranted`, and the provider's name on\n * `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":"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","names":["answered"]}
package/dist/llm.d.cts CHANGED
@@ -203,7 +203,8 @@ interface ChatProviderInfo {
203
203
  connectedProviders?: ChatProviderChoice[];
204
204
  }
205
205
  /**
206
- * Whether the host has told us about a provider yet, and if so whether one is bound.
206
+ * Whether the host has told us about a provider yet, and if so whether one is bound —
207
+ * and, since R3-688, whether this frame may ask at all.
207
208
  *
208
209
  * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both
209
210
  * when no provider is configured AND when the channel has not answered — so an app
@@ -213,23 +214,50 @@ interface ChatProviderInfo {
213
214
  *
214
215
  * **`unknown` is TRANSIENT — the host answers every frame** (R3-419;
215
216
  * `LLM_AND_AGENTS_SPEC §4.1` R-LLM-1..3). An app that does not hold `llm:chat` is not
216
- * met with silence: it is answered `not-configured`, the same terminal state as a user
217
- * with no key, because from the app's side those are the same fact — do not render a
218
- * provider, do offer the connect path. So it is correct to treat a `unknown` that
219
- * persists as a host bug rather than as a state to design around, and WRONG to render a
220
- * spinner with no timeout on it. (Before R3-419 the host withheld the channel entirely
221
- * from an ungranted frame, and `unknown` stood forever — that is the failure this note
222
- * exists to keep from being re-created on the app side.)
217
+ * met with silence. So it is correct to treat a `unknown` that persists as a host bug
218
+ * rather than as a state to design around, and WRONG to render a spinner with no
219
+ * timeout on it. (Before R3-419 the host withheld the channel entirely from an
220
+ * ungranted frame, and `unknown` stood forever — that is the failure this note exists
221
+ * to keep from being re-created on the app side.)
222
+ *
223
+ * The FOURTH state, `ungranted` (R3-688; `LLM_AND_AGENTS_SPEC §4.1` R-LLM-2 as
224
+ * annotated, `GROVE_AGENT_SPEC` G-GA-10): the host answered, and the reason this frame
225
+ * cannot reach a model is that **it does not hold `llm:chat`** — not that the user has
226
+ * no key. R3-419 deliberately answered an ungranted frame with the same payload as a
227
+ * keyless one, because the SDK could not carry the difference; that made the distinct
228
+ * "not granted" cause the reach-card contract promises (G-GA-10) uncomputable — an
229
+ * ungranted fork of an app rendered "connect a key" at a user who had one. The host now
230
+ * marks the grantless answer with an optional `ungranted: true` on the SAME message
231
+ * (additive — an older SDK never reads the field and degrades to `not-configured`;
232
+ * an older host never sends the field and the app degrades the same way). The mark is
233
+ * HOST-derived from the frame's resolved grant set at push time (the channel view in
234
+ * `channelAcl.ts`), never from anything this frame asserts.
223
235
  */
224
236
  type ChatProviderState = {
225
237
  status: 'unknown';
226
238
  } | {
227
239
  status: 'not-configured';
240
+ } | {
241
+ status: 'ungranted';
228
242
  } | {
229
243
  status: 'configured';
230
244
  provider: ChatProviderInfo;
231
245
  };
232
246
  declare function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null;
247
+ /**
248
+ * Derive the four-state read from what the host has said. Pure, and exported for its
249
+ * own test (`llmProviderState.test.ts`) like `normalizeProviderInfo` — not part of the
250
+ * public surface beyond `index.ts`'s wholesale re-export.
251
+ *
252
+ * Order is load-bearing: `unknown` wins until the host answers (an `ungranted` mark
253
+ * cannot exist before an answer — it arrives ON the answer); then the host's grantless
254
+ * mark; then the provider. A host that predates the mark never sets it, so its
255
+ * grantless answer derives `not-configured` — the exact pre-R3-688 contract, which is
256
+ * the additive degradation the fourth state promises.
257
+ *
258
+ * @internal
259
+ */
260
+ declare function deriveChatProviderState(answered: boolean, ungranted: boolean, provider: ChatProviderInfo | null): ChatProviderState;
233
261
  /**
234
262
  * The provider the host resolved for this app, or `null`.
235
263
  *
@@ -239,23 +267,25 @@ declare function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatP
239
267
  * the `unknown` state is exactly the false banner R3-300 fixes.
240
268
  */
241
269
  declare const describeChat: () => ChatProviderInfo | null;
242
- /** The three-state read: `unknown` before the host answers, then configured or not. */
270
+ /** The four-state read: `unknown` before the host answers; then the host's `ungranted`
271
+ * mark, or the provider (configured / not-configured). */
243
272
  declare const describeChatState: () => ChatProviderState;
244
273
  /** Subscribe to provider changes (key added/revoked, preference changed). Invoked
245
274
  * immediately with the current value, then on every change. Returns unsubscribe. */
246
275
  declare const onChatProviderChange: (listener: (provider: ChatProviderInfo | null) => void) => (() => void);
247
- /** Subscribe to the three-state provider description. */
276
+ /** Subscribe to the provider-state description. */
248
277
  declare const onChatProviderStateChange: (listener: (state: ChatProviderState) => void) => (() => void);
249
278
  /** React hook returning the resolved chat provider (or `null`), re-rendering on
250
279
  * change — gate the summarize affordance on `provider !== null`. */
251
280
  declare const useChatProvider: () => ChatProviderInfo | null;
252
281
  /**
253
- * React hook returning the three-state description.
282
+ * React hook returning the four-state description.
254
283
  *
255
284
  * Use this to render provider state honestly: show nothing (or a neutral placeholder)
256
- * while `unknown`, the connect affordance only on `not-configured`, and the provider's
257
- * name on `configured`.
285
+ * while `unknown`, the connect affordance only on `not-configured`, the NOT-GRANTED
286
+ * cause — never the connect copy — on `ungranted`, and the provider's name on
287
+ * `configured`.
258
288
  */
259
289
  declare const useChatProviderState: () => ChatProviderState;
260
290
 
261
- export { type ChatDelta, type ChatExecutor, type ChatFeatures, type ChatMessage, type ChatProviderChoice, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ChatTierModels, type ContentPart, type ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
291
+ export { type ChatDelta, type ChatExecutor, type ChatFeatures, type ChatMessage, type ChatProviderChoice, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ChatTierModels, type ContentPart, type ToolDef, chat, deriveChatProviderState, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
package/dist/llm.d.ts CHANGED
@@ -203,7 +203,8 @@ interface ChatProviderInfo {
203
203
  connectedProviders?: ChatProviderChoice[];
204
204
  }
205
205
  /**
206
- * Whether the host has told us about a provider yet, and if so whether one is bound.
206
+ * Whether the host has told us about a provider yet, and if so whether one is bound —
207
+ * and, since R3-688, whether this frame may ask at all.
207
208
  *
208
209
  * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both
209
210
  * when no provider is configured AND when the channel has not answered — so an app
@@ -213,23 +214,50 @@ interface ChatProviderInfo {
213
214
  *
214
215
  * **`unknown` is TRANSIENT — the host answers every frame** (R3-419;
215
216
  * `LLM_AND_AGENTS_SPEC §4.1` R-LLM-1..3). An app that does not hold `llm:chat` is not
216
- * met with silence: it is answered `not-configured`, the same terminal state as a user
217
- * with no key, because from the app's side those are the same fact — do not render a
218
- * provider, do offer the connect path. So it is correct to treat a `unknown` that
219
- * persists as a host bug rather than as a state to design around, and WRONG to render a
220
- * spinner with no timeout on it. (Before R3-419 the host withheld the channel entirely
221
- * from an ungranted frame, and `unknown` stood forever — that is the failure this note
222
- * exists to keep from being re-created on the app side.)
217
+ * met with silence. So it is correct to treat a `unknown` that persists as a host bug
218
+ * rather than as a state to design around, and WRONG to render a spinner with no
219
+ * timeout on it. (Before R3-419 the host withheld the channel entirely from an
220
+ * ungranted frame, and `unknown` stood forever — that is the failure this note exists
221
+ * to keep from being re-created on the app side.)
222
+ *
223
+ * The FOURTH state, `ungranted` (R3-688; `LLM_AND_AGENTS_SPEC §4.1` R-LLM-2 as
224
+ * annotated, `GROVE_AGENT_SPEC` G-GA-10): the host answered, and the reason this frame
225
+ * cannot reach a model is that **it does not hold `llm:chat`** — not that the user has
226
+ * no key. R3-419 deliberately answered an ungranted frame with the same payload as a
227
+ * keyless one, because the SDK could not carry the difference; that made the distinct
228
+ * "not granted" cause the reach-card contract promises (G-GA-10) uncomputable — an
229
+ * ungranted fork of an app rendered "connect a key" at a user who had one. The host now
230
+ * marks the grantless answer with an optional `ungranted: true` on the SAME message
231
+ * (additive — an older SDK never reads the field and degrades to `not-configured`;
232
+ * an older host never sends the field and the app degrades the same way). The mark is
233
+ * HOST-derived from the frame's resolved grant set at push time (the channel view in
234
+ * `channelAcl.ts`), never from anything this frame asserts.
223
235
  */
224
236
  type ChatProviderState = {
225
237
  status: 'unknown';
226
238
  } | {
227
239
  status: 'not-configured';
240
+ } | {
241
+ status: 'ungranted';
228
242
  } | {
229
243
  status: 'configured';
230
244
  provider: ChatProviderInfo;
231
245
  };
232
246
  declare function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null;
247
+ /**
248
+ * Derive the four-state read from what the host has said. Pure, and exported for its
249
+ * own test (`llmProviderState.test.ts`) like `normalizeProviderInfo` — not part of the
250
+ * public surface beyond `index.ts`'s wholesale re-export.
251
+ *
252
+ * Order is load-bearing: `unknown` wins until the host answers (an `ungranted` mark
253
+ * cannot exist before an answer — it arrives ON the answer); then the host's grantless
254
+ * mark; then the provider. A host that predates the mark never sets it, so its
255
+ * grantless answer derives `not-configured` — the exact pre-R3-688 contract, which is
256
+ * the additive degradation the fourth state promises.
257
+ *
258
+ * @internal
259
+ */
260
+ declare function deriveChatProviderState(answered: boolean, ungranted: boolean, provider: ChatProviderInfo | null): ChatProviderState;
233
261
  /**
234
262
  * The provider the host resolved for this app, or `null`.
235
263
  *
@@ -239,23 +267,25 @@ declare function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatP
239
267
  * the `unknown` state is exactly the false banner R3-300 fixes.
240
268
  */
241
269
  declare const describeChat: () => ChatProviderInfo | null;
242
- /** The three-state read: `unknown` before the host answers, then configured or not. */
270
+ /** The four-state read: `unknown` before the host answers; then the host's `ungranted`
271
+ * mark, or the provider (configured / not-configured). */
243
272
  declare const describeChatState: () => ChatProviderState;
244
273
  /** Subscribe to provider changes (key added/revoked, preference changed). Invoked
245
274
  * immediately with the current value, then on every change. Returns unsubscribe. */
246
275
  declare const onChatProviderChange: (listener: (provider: ChatProviderInfo | null) => void) => (() => void);
247
- /** Subscribe to the three-state provider description. */
276
+ /** Subscribe to the provider-state description. */
248
277
  declare const onChatProviderStateChange: (listener: (state: ChatProviderState) => void) => (() => void);
249
278
  /** React hook returning the resolved chat provider (or `null`), re-rendering on
250
279
  * change — gate the summarize affordance on `provider !== null`. */
251
280
  declare const useChatProvider: () => ChatProviderInfo | null;
252
281
  /**
253
- * React hook returning the three-state description.
282
+ * React hook returning the four-state description.
254
283
  *
255
284
  * Use this to render provider state honestly: show nothing (or a neutral placeholder)
256
- * while `unknown`, the connect affordance only on `not-configured`, and the provider's
257
- * name on `configured`.
285
+ * while `unknown`, the connect affordance only on `not-configured`, the NOT-GRANTED
286
+ * cause — never the connect copy — on `ungranted`, and the provider's name on
287
+ * `configured`.
258
288
  */
259
289
  declare const useChatProviderState: () => ChatProviderState;
260
290
 
261
- export { type ChatDelta, type ChatExecutor, type ChatFeatures, type ChatMessage, type ChatProviderChoice, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ChatTierModels, type ContentPart, type ToolDef, chat, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
291
+ export { type ChatDelta, type ChatExecutor, type ChatFeatures, type ChatMessage, type ChatProviderChoice, type ChatProviderInfo, type ChatProviderState, type ChatRequest, type ChatResult, type ChatRole, type ChatStopReason, type ChatTierModels, type ContentPart, type ToolDef, chat, deriveChatProviderState, describeChat, describeChatState, normalizeProviderInfo, onChatProviderChange, onChatProviderStateChange, useChatProvider, useChatProviderState };
package/dist/llm.js CHANGED
@@ -49,6 +49,7 @@ function normalizeProviderInfo(provider) {
49
49
  };
50
50
  }
51
51
  let answered = false;
52
+ let ungrantedMark = false;
52
53
  const channel = createPushChannel({
53
54
  pushType: LLM_PROVIDER,
54
55
  requestType: REQUEST_LLM_PROVIDER,
@@ -56,10 +57,16 @@ const channel = createPushChannel({
56
57
  parse: (msg) => {
57
58
  if (!("provider" in msg)) return void 0;
58
59
  answered = true;
60
+ ungrantedMark = msg.ungranted === true;
59
61
  return normalizeProviderInfo(msg.provider ?? null);
60
62
  }
61
63
  });
62
- const stateOf = (provider) => !answered ? { status: "unknown" } : provider ? { status: "configured", provider } : { status: "not-configured" };
64
+ function deriveChatProviderState(answered2, ungranted, provider) {
65
+ if (!answered2) return { status: "unknown" };
66
+ if (ungranted) return { status: "ungranted" };
67
+ return provider ? { status: "configured", provider } : { status: "not-configured" };
68
+ }
69
+ const stateOf = (provider) => deriveChatProviderState(answered, ungrantedMark, provider);
63
70
  const describeChat = () => channel.get();
64
71
  const describeChatState = () => stateOf(channel.get());
65
72
  const onChatProviderChange = (listener) => channel.onChange(listener);
@@ -68,6 +75,7 @@ const useChatProvider = () => channel.use();
68
75
  const useChatProviderState = () => stateOf(channel.use());
69
76
  export {
70
77
  chat,
78
+ deriveChatProviderState,
71
79
  describeChat,
72
80
  describeChatState,
73
81
  normalizeProviderInfo,
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 // 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 /** A concrete provider-and-model choice, naming one of the user's CONNECTED providers\n * (R3-620, LLM_AND_AGENTS_SPEC §0 editing-session exception). When present it WINS over\n * `modelHint`; the host validates the pair against the user's connected set and refuses\n * with `provider-not-connected` otherwise. Only an editing-session principal may read the\n * chooseable set (via `describeChat()`'s `connectedProviders`, gated `llm:chooseModel`);\n * a stage app still passes at most the abstract hint. */\n model?: { providerId: string; model: string };\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 first\n * draws the SP-7 connect-me gate itself (R3-456: the app never draws a\n * credential prompt — that is host chrome, SECRETS_SPEC S3):\n * - the user connects a key → the call retries once and streams normally;\n * - the user declines → the generator throws `code: 'cancelled'` (the same code\n * a declined powerbox produces — a working degraded state: catch it and\n * degrade, e.g. skip the AI feature);\n * - an older host without the gate throws `code: 'provider-not-configured'`.\n * A signed-out user throws `code: 'auth-required'`; an un-granted call throws\n * `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/** How the resolved provider's requests physically leave the browser. Read it to\n * DESCRIBE routing where that matters to the person (\"runs in your browser\" vs\n * \"routed through immediately.run\") — never to draw host chrome or a consent prompt,\n * which remain the host's (UI_AS_APPS §8 T15). */\nexport type ChatExecutor = 'browser-direct' | 'backend-proxied';\n\n/** The concrete model each abstract tier resolves to right now. */\nexport interface ChatTierModels {\n fast: string;\n smart: string;\n}\n\n/** One CONNECTED provider the user may choose a model from, for a per-conversation model\n * choice in an editing-session workbench (R3-620, LLM_AND_AGENTS_SPEC §0). Names and ids\n * only — never keys, usage, balance or routing. */\nexport interface ChatProviderChoice {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — matches {@link ChatRequest.model}.providerId. */\n providerId: string;\n /** The provider's human name, for a picker label. */\n displayName: string;\n /** The chooseable model names — the catalogue-recommended set plus the two the user has\n * chosen. A closed list here would re-introduce the ids-rot problem, so `model` is passed\n * through to the adapter exactly as the Settings field is; this list is suggestions. */\n models: string[];\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 /** The provider's human name, e.g. `OpenRouter` — what to put in front of a person.\n * Absent on a host that predates the field: fall back to your own copy rather than\n * rendering the id, which is a platform identifier and not a name. */\n displayName?: string;\n /** How this provider's requests leave the browser. Absent on a host that predates the\n * field, which is NOT the same as `browser-direct` — say nothing about routing rather\n * than guess at it. */\n executor?: ChatExecutor;\n /**\n * The concrete model each {@link ChatRequest.modelHint} tier resolves to — what a\n * `smart` request would actually run, after the user's own preference.\n *\n * Read-only, and it does not weaken `LLM_AND_AGENTS_SPEC §0`: an app still names no\n * model, and {@link ChatRequest} still carries only the abstract hint. It is here so an\n * app can be HONEST about what answered — a transcript that says which model wrote a\n * reply, a warning that names the model about to be spent on — instead of showing a\n * blank where the platform knows the answer. The user picks the model in host settings;\n * the app reports it.\n *\n * Absent on a host that predates the field. It changes when the user changes their\n * preference, so read it through {@link onChatProviderChange} rather than caching it.\n */\n models?: ChatTierModels;\n /**\n * The user's CONNECTED providers and their chooseable models, for a per-conversation\n * model choice (R3-620). Only present when this frame holds the ELEVATED `llm:chooseModel`\n * capability (an editing-session workbench); the host strips it for everyone else, so a\n * stage app sees `undefined` and can offer only the abstract `modelHint` path.\n *\n * Read-only: names and ids only. No keys, usage, balance or routing. Absent also on a host\n * that predates the field.\n */\n connectedProviders?: ChatProviderChoice[];\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 *\n * **`unknown` is TRANSIENT — the host answers every frame** (R3-419;\n * `LLM_AND_AGENTS_SPEC §4.1` R-LLM-1..3). An app that does not hold `llm:chat` is not\n * met with silence: it is answered `not-configured`, the same terminal state as a user\n * with no key, because from the app's side those are the same fact — do not render a\n * provider, do offer the connect path. So it is correct to treat a `unknown` that\n * persists as a host bug rather than as a state to design around, and WRONG to render a\n * spinner with no timeout on it. (Before R3-419 the host withheld the channel entirely\n * from an ungranted frame, and `unknown` stood forever — that is the failure this note\n * exists to keep from being re-created on the app side.)\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 * Reconcile what the host actually sent with what this SDK declares.\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 * `displayName`, `executor` and `models` arrived later still, and for them absence is a\n * REAL answer an app is told to handle (\"this host does not say\"), so they are left\n * absent rather than filled in. What is dropped is a value that is present but not\n * usable — an `executor` outside the union, a `models` missing a tier — because a\n * half-answer rendered as fact is worse than the honest blank the app already handles.\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 */\nconst EXECUTORS: readonly ChatExecutor[] = ['browser-direct', 'backend-proxied'];\n\nconst usableModels = (raw: unknown): ChatTierModels | undefined => {\n if (!raw || typeof raw !== 'object') return undefined;\n const { fast, smart } = raw as Partial<ChatTierModels>;\n return typeof fast === 'string' && fast && typeof smart === 'string' && smart ? { fast, smart } : undefined;\n};\n\n/** Validate the wire's `connectedProviders` list, keeping only usable entries. The gating\n * (whether the list arrives at all) is the host's — `normalizeProviderInfo` merely refuses\n * to pass through a malformed list, exactly as it refuses a half-answered `models` pair. */\nconst usableConnectedProviders = (raw: unknown): ChatProviderChoice[] | undefined => {\n if (!Array.isArray(raw)) return undefined;\n const out: ChatProviderChoice[] = [];\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue;\n const { providerId, displayName, models } = item as Partial<ChatProviderChoice>;\n if (typeof providerId !== 'string' || !providerId) continue;\n if (typeof displayName !== 'string' || !displayName) continue;\n const cleanModels = Array.isArray(models) ? models.filter((m): m is string => typeof m === 'string' && !!m) : [];\n out.push({ providerId, displayName, models: cleanModels });\n }\n return out.length > 0 ? out : undefined;\n};\n\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The LATER fields (displayName/executor/models/connectedProviders) are taken OFF the value\n // and put back only if usable — spreading and then overwriting would leave an unusable key\n // present, and `key in provider` is exactly how an app is told to ask whether the host said\n // anything.\n const {\n displayName: rawName,\n executor: rawExecutor,\n models: rawModels,\n connectedProviders: rawConnected,\n ...rest\n } = provider;\n // The wire value is whatever the host sent, which may predate any of these fields — so\n // read it as partial rather than trusting the declared type, and decide each explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n const executor = EXECUTORS.includes(rawExecutor as ChatExecutor) ? (rawExecutor as ChatExecutor) : undefined;\n const displayName = typeof rawName === 'string' && rawName ? rawName : undefined;\n const models = usableModels(rawModels);\n const connectedProviders = usableConnectedProviders(rawConnected);\n return {\n ...rest,\n features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures,\n ...(displayName ? { displayName } : {}),\n ...(executor ? { executor } : {}),\n ...(models ? { models } : {}),\n ...(connectedProviders ? { connectedProviders } : {}),\n };\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;AA4H5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,SAAO,aAAoC,YAAY,QAA8C,MAAM;AAC7G;AA4IA,MAAM,YAAqC,CAAC,kBAAkB,iBAAiB;AAE/E,MAAM,eAAe,CAAC,QAA6C;AACjE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,EAAE,MAAM,MAAM,IAAI;AACxB,SAAO,OAAO,SAAS,YAAY,QAAQ,OAAO,UAAU,YAAY,QAAQ,EAAE,MAAM,MAAM,IAAI;AACpG;AAKA,MAAM,2BAA2B,CAAC,QAAmD;AACnF,MAAI,CAAC,MAAM,QAAQ,GAAG,EAAG,QAAO;AAChC,QAAM,MAA4B,CAAC;AACnC,aAAW,QAAQ,KAAK;AACtB,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,UAAM,EAAE,YAAY,aAAa,OAAO,IAAI;AAC5C,QAAI,OAAO,eAAe,YAAY,CAAC,WAAY;AACnD,QAAI,OAAO,gBAAgB,YAAY,CAAC,YAAa;AACrD,UAAM,cAAc,MAAM,QAAQ,MAAM,IAAI,OAAO,OAAO,CAAC,MAAmB,OAAO,MAAM,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/G,QAAI,KAAK,EAAE,YAAY,aAAa,QAAQ,YAAY,CAAC;AAAA,EAC3D;AACA,SAAO,IAAI,SAAS,IAAI,MAAM;AAChC;AAEO,SAAS,sBAAsB,UAA4D;AAChG,MAAI,CAAC,SAAU,QAAO;AAKtB,QAAM;AAAA,IACJ,aAAa;AAAA,IACb,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,oBAAoB;AAAA,IACpB,GAAG;AAAA,EACL,IAAI;AAGJ,QAAM,OAAO,SAAS;AACtB,QAAM,WAAW,UAAU,SAAS,WAA2B,IAAK,cAA+B;AACnG,QAAM,cAAc,OAAO,YAAY,YAAY,UAAU,UAAU;AACvE,QAAM,SAAS,aAAa,SAAS;AACrC,QAAM,qBAAqB,yBAAyB,YAAY;AAChE,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,cAAc,KAAK;AAAA,IACxD,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;AAAA,IACrC,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,IAC/B,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,qBAAqB,EAAE,mBAAmB,IAAI,CAAC;AAAA,EACrD;AACF;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":[]}
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 /** A concrete provider-and-model choice, naming one of the user's CONNECTED providers\n * (R3-620, LLM_AND_AGENTS_SPEC §0 editing-session exception). When present it WINS over\n * `modelHint`; the host validates the pair against the user's connected set and refuses\n * with `provider-not-connected` otherwise. Only an editing-session principal may read the\n * chooseable set (via `describeChat()`'s `connectedProviders`, gated `llm:chooseModel`);\n * a stage app still passes at most the abstract hint. */\n model?: { providerId: string; model: string };\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 first\n * draws the SP-7 connect-me gate itself (R3-456: the app never draws a\n * credential prompt — that is host chrome, SECRETS_SPEC S3):\n * - the user connects a key → the call retries once and streams normally;\n * - the user declines → the generator throws `code: 'cancelled'` (the same code\n * a declined powerbox produces — a working degraded state: catch it and\n * degrade, e.g. skip the AI feature);\n * - an older host without the gate throws `code: 'provider-not-configured'`.\n * A signed-out user throws `code: 'auth-required'`; an un-granted call throws\n * `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/** How the resolved provider's requests physically leave the browser. Read it to\n * DESCRIBE routing where that matters to the person (\"runs in your browser\" vs\n * \"routed through immediately.run\") — never to draw host chrome or a consent prompt,\n * which remain the host's (UI_AS_APPS §8 T15). */\nexport type ChatExecutor = 'browser-direct' | 'backend-proxied';\n\n/** The concrete model each abstract tier resolves to right now. */\nexport interface ChatTierModels {\n fast: string;\n smart: string;\n}\n\n/** One CONNECTED provider the user may choose a model from, for a per-conversation model\n * choice in an editing-session workbench (R3-620, LLM_AND_AGENTS_SPEC §0). Names and ids\n * only — never keys, usage, balance or routing. */\nexport interface ChatProviderChoice {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — matches {@link ChatRequest.model}.providerId. */\n providerId: string;\n /** The provider's human name, for a picker label. */\n displayName: string;\n /** The chooseable model names — the catalogue-recommended set plus the two the user has\n * chosen. A closed list here would re-introduce the ids-rot problem, so `model` is passed\n * through to the adapter exactly as the Settings field is; this list is suggestions. */\n models: string[];\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 /** The provider's human name, e.g. `OpenRouter` — what to put in front of a person.\n * Absent on a host that predates the field: fall back to your own copy rather than\n * rendering the id, which is a platform identifier and not a name. */\n displayName?: string;\n /** How this provider's requests leave the browser. Absent on a host that predates the\n * field, which is NOT the same as `browser-direct` — say nothing about routing rather\n * than guess at it. */\n executor?: ChatExecutor;\n /**\n * The concrete model each {@link ChatRequest.modelHint} tier resolves to — what a\n * `smart` request would actually run, after the user's own preference.\n *\n * Read-only, and it does not weaken `LLM_AND_AGENTS_SPEC §0`: an app still names no\n * model, and {@link ChatRequest} still carries only the abstract hint. It is here so an\n * app can be HONEST about what answered — a transcript that says which model wrote a\n * reply, a warning that names the model about to be spent on — instead of showing a\n * blank where the platform knows the answer. The user picks the model in host settings;\n * the app reports it.\n *\n * Absent on a host that predates the field. It changes when the user changes their\n * preference, so read it through {@link onChatProviderChange} rather than caching it.\n */\n models?: ChatTierModels;\n /**\n * The user's CONNECTED providers and their chooseable models, for a per-conversation\n * model choice (R3-620). Only present when this frame holds the ELEVATED `llm:chooseModel`\n * capability (an editing-session workbench); the host strips it for everyone else, so a\n * stage app sees `undefined` and can offer only the abstract `modelHint` path.\n *\n * Read-only: names and ids only. No keys, usage, balance or routing. Absent also on a host\n * that predates the field.\n */\n connectedProviders?: ChatProviderChoice[];\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound —\n * and, since R3-688, whether this frame may ask at all.\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 *\n * **`unknown` is TRANSIENT — the host answers every frame** (R3-419;\n * `LLM_AND_AGENTS_SPEC §4.1` R-LLM-1..3). An app that does not hold `llm:chat` is not\n * met with silence. So it is correct to treat a `unknown` that persists as a host bug\n * rather than as a state to design around, and WRONG to render a spinner with no\n * timeout on it. (Before R3-419 the host withheld the channel entirely from an\n * ungranted frame, and `unknown` stood forever — that is the failure this note exists\n * to keep from being re-created on the app side.)\n *\n * The FOURTH state, `ungranted` (R3-688; `LLM_AND_AGENTS_SPEC §4.1` R-LLM-2 as\n * annotated, `GROVE_AGENT_SPEC` G-GA-10): the host answered, and the reason this frame\n * cannot reach a model is that **it does not hold `llm:chat`** — not that the user has\n * no key. R3-419 deliberately answered an ungranted frame with the same payload as a\n * keyless one, because the SDK could not carry the difference; that made the distinct\n * \"not granted\" cause the reach-card contract promises (G-GA-10) uncomputable — an\n * ungranted fork of an app rendered \"connect a key\" at a user who had one. The host now\n * marks the grantless answer with an optional `ungranted: true` on the SAME message\n * (additive — an older SDK never reads the field and degrades to `not-configured`;\n * an older host never sends the field and the app degrades the same way). The mark is\n * HOST-derived from the frame's resolved grant set at push time (the channel view in\n * `channelAcl.ts`), never from anything this frame asserts.\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'ungranted' }\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 protocol snapshot gate reads this type as the channel's shape. The state\n// read lives BESIDE it: `answered` records whether the host has ever spoken on this\n// channel, which is the one bit `null` cannot carry, and `ungrantedMark` (R3-688) reads\n// the host's optional grantless marker off the same message — also beside the value, not\n// inside it. Deriving the state rather than widening the channel keeps the declared wire\n// VALUE unchanged (SDK_PACKAGING_SPEC §9: additive-only; the optional message field\n// follows the `theme`/`form-factor` insets precedent — old readers ignore it, and the\n// sandbox-protocol descriptor's \"read off\" line catches up at its next release).\n/**\n * Reconcile what the host actually sent with what this SDK declares.\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 * `displayName`, `executor` and `models` arrived later still, and for them absence is a\n * REAL answer an app is told to handle (\"this host does not say\"), so they are left\n * absent rather than filled in. What is dropped is a value that is present but not\n * usable — an `executor` outside the union, a `models` missing a tier — because a\n * half-answer rendered as fact is worse than the honest blank the app already handles.\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 */\nconst EXECUTORS: readonly ChatExecutor[] = ['browser-direct', 'backend-proxied'];\n\nconst usableModels = (raw: unknown): ChatTierModels | undefined => {\n if (!raw || typeof raw !== 'object') return undefined;\n const { fast, smart } = raw as Partial<ChatTierModels>;\n return typeof fast === 'string' && fast && typeof smart === 'string' && smart ? { fast, smart } : undefined;\n};\n\n/** Validate the wire's `connectedProviders` list, keeping only usable entries. The gating\n * (whether the list arrives at all) is the host's — `normalizeProviderInfo` merely refuses\n * to pass through a malformed list, exactly as it refuses a half-answered `models` pair. */\nconst usableConnectedProviders = (raw: unknown): ChatProviderChoice[] | undefined => {\n if (!Array.isArray(raw)) return undefined;\n const out: ChatProviderChoice[] = [];\n for (const item of raw) {\n if (!item || typeof item !== 'object') continue;\n const { providerId, displayName, models } = item as Partial<ChatProviderChoice>;\n if (typeof providerId !== 'string' || !providerId) continue;\n if (typeof displayName !== 'string' || !displayName) continue;\n const cleanModels = Array.isArray(models) ? models.filter((m): m is string => typeof m === 'string' && !!m) : [];\n out.push({ providerId, displayName, models: cleanModels });\n }\n return out.length > 0 ? out : undefined;\n};\n\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The LATER fields (displayName/executor/models/connectedProviders) are taken OFF the value\n // and put back only if usable — spreading and then overwriting would leave an unusable key\n // present, and `key in provider` is exactly how an app is told to ask whether the host said\n // anything.\n const {\n displayName: rawName,\n executor: rawExecutor,\n models: rawModels,\n connectedProviders: rawConnected,\n ...rest\n } = provider;\n // The wire value is whatever the host sent, which may predate any of these fields — so\n // read it as partial rather than trusting the declared type, and decide each explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n const executor = EXECUTORS.includes(rawExecutor as ChatExecutor) ? (rawExecutor as ChatExecutor) : undefined;\n const displayName = typeof rawName === 'string' && rawName ? rawName : undefined;\n const models = usableModels(rawModels);\n const connectedProviders = usableConnectedProviders(rawConnected);\n return {\n ...rest,\n features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures,\n ...(displayName ? { displayName } : {}),\n ...(executor ? { executor } : {}),\n ...(models ? { models } : {}),\n ...(connectedProviders ? { connectedProviders } : {}),\n };\n}\n\nlet answered = false;\n// R3-688 — the host's grantless marker, read off the SAME message (`ungranted: true`\n// beside `provider`). It is message-arrival state beside `answered`, not part of the\n// wire VALUE: the channel's value stays `ChatProviderInfo | null`, so the protocol\n// snapshot gate keeps reading the unchanged shape.\nlet ungrantedMark = 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 ungrantedMark = msg.ungranted === true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/**\n * Derive the four-state read from what the host has said. Pure, and exported for its\n * own test (`llmProviderState.test.ts`) like `normalizeProviderInfo` — not part of the\n * public surface beyond `index.ts`'s wholesale re-export.\n *\n * Order is load-bearing: `unknown` wins until the host answers (an `ungranted` mark\n * cannot exist before an answer — it arrives ON the answer); then the host's grantless\n * mark; then the provider. A host that predates the mark never sets it, so its\n * grantless answer derives `not-configured` — the exact pre-R3-688 contract, which is\n * the additive degradation the fourth state promises.\n *\n * @internal\n */\nexport function deriveChatProviderState(\n answered: boolean,\n ungranted: boolean,\n provider: ChatProviderInfo | null,\n): ChatProviderState {\n if (!answered) return { status: 'unknown' };\n if (ungranted) return { status: 'ungranted' };\n return provider ? { status: 'configured', provider } : { status: 'not-configured' };\n}\n\n/** Derive the state from the wire value plus what the host has said beside it. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n deriveChatProviderState(answered, ungrantedMark, provider);\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 four-state read: `unknown` before the host answers; then the host's `ungranted`\n * mark, or the provider (configured / not-configured). */\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 provider-state 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 four-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`, the NOT-GRANTED\n * cause — never the connect copy — on `ungranted`, and the provider's name on\n * `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":"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","names":["answered"]}
package/dist/version.cjs CHANGED
@@ -21,7 +21,7 @@ __export(version_exports, {
21
21
  SDK_VERSION: () => SDK_VERSION
22
22
  });
23
23
  module.exports = __toCommonJS(version_exports);
24
- const SDK_VERSION = "0.71.0";
24
+ const SDK_VERSION = "0.72.0";
25
25
  // Annotate the CommonJS export names for ESM import in node:
26
26
  0 && (module.exports = {
27
27
  SDK_VERSION
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.71.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.72.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIO,MAAM,cAAc;","names":[]}
@@ -1,4 +1,4 @@
1
1
  /** This SDK's package version, baked from package.json at build (SP2-6). */
2
- declare const SDK_VERSION = "0.71.0";
2
+ declare const SDK_VERSION = "0.72.0";
3
3
 
4
4
  export { SDK_VERSION };
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** This SDK's package version, baked from package.json at build (SP2-6). */
2
- declare const SDK_VERSION = "0.71.0";
2
+ declare const SDK_VERSION = "0.72.0";
3
3
 
4
4
  export { SDK_VERSION };
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./chunk-VHAA22YE.js";
2
- const SDK_VERSION = "0.71.0";
2
+ const SDK_VERSION = "0.72.0";
3
3
  export {
4
4
  SDK_VERSION
5
5
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.71.0';\n"],"mappings":";AAIO,MAAM,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["// GENERATED by scripts/gen-version.mjs from package.json — do not edit by hand.\n// Regenerated on every build (prebuild); kept honest by version.test.ts.\n\n/** This SDK's package version, baked from package.json at build (SP2-6). */\nexport const SDK_VERSION = '0.72.0';\n"],"mappings":";AAIO,MAAM,cAAc;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immediately-run/sdk",
3
- "version": "0.71.0",
3
+ "version": "0.72.0",
4
4
  "description": "Runtime SDK for code executing inside an immediately.run sandbox.",
5
5
  "license": "MIT",
6
6
  "repository": "github:immediately-run/immediately-run-sdk",
@@ -71,7 +71,7 @@
71
71
  "@immediately-run/mdx-plugins": "0.7.1",
72
72
  "@immediately-run/platform-constants": "0.2.0",
73
73
  "@immediately-run/safe-content": "0.1.0",
74
- "@immediately-run/sandbox-protocol": "0.10.6",
74
+ "@immediately-run/sandbox-protocol": "0.10.7",
75
75
  "react-error-boundary": "^6.0.0"
76
76
  },
77
77
  "devDependencies": {