@immediately-run/sdk 0.57.0 → 0.57.4
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/agentChatClient.cjs +112 -0
- package/dist/agentChatClient.cjs.map +1 -0
- package/dist/agentChatClient.d.cts +9 -0
- package/dist/agentChatClient.d.ts +9 -0
- package/dist/agentChatClient.js +89 -0
- package/dist/agentChatClient.js.map +1 -0
- package/dist/agentContext.cjs +69 -0
- package/dist/agentContext.cjs.map +1 -0
- package/dist/agentContext.d.cts +45 -0
- package/dist/agentContext.d.ts +45 -0
- package/dist/agentContext.js +45 -0
- package/dist/agentContext.js.map +1 -0
- package/dist/agentLoop.cjs +301 -0
- package/dist/agentLoop.cjs.map +1 -0
- package/dist/agentLoop.d.cts +267 -0
- package/dist/agentLoop.d.ts +267 -0
- package/dist/agentLoop.js +271 -0
- package/dist/agentLoop.js.map +1 -0
- package/dist/agentSteering.cjs +127 -0
- package/dist/agentSteering.cjs.map +1 -0
- package/dist/agentSteering.d.cts +80 -0
- package/dist/agentSteering.d.ts +80 -0
- package/dist/agentSteering.js +98 -0
- package/dist/agentSteering.js.map +1 -0
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +33 -2
- package/dist/auth.d.ts +33 -2
- package/dist/auth.js.map +1 -1
- package/dist/bundle.cjs +91 -0
- package/dist/bundle.cjs.map +1 -0
- package/dist/bundle.d.cts +63 -0
- package/dist/bundle.d.ts +63 -0
- package/dist/bundle.js +62 -0
- package/dist/bundle.js.map +1 -0
- package/dist/collectHeadings.cjs +59 -0
- package/dist/collectHeadings.cjs.map +1 -0
- package/dist/collectHeadings.d.cts +16 -0
- package/dist/collectHeadings.d.ts +16 -0
- package/dist/collectHeadings.js +36 -0
- package/dist/collectHeadings.js.map +1 -0
- package/dist/components/MainContent.cjs +3 -1
- package/dist/components/MainContent.cjs.map +1 -1
- package/dist/components/MainContent.js +4 -2
- package/dist/components/MainContent.js.map +1 -1
- package/dist/corpus.cjs +9 -58
- package/dist/corpus.cjs.map +1 -1
- package/dist/corpus.d.cts +3 -63
- package/dist/corpus.d.ts +3 -63
- package/dist/corpus.js +15 -57
- package/dist/corpus.js.map +1 -1
- package/dist/debug.cjs +115 -0
- package/dist/debug.cjs.map +1 -1
- package/dist/debug.js +115 -0
- package/dist/debug.js.map +1 -1
- package/dist/fence.cjs +49 -0
- package/dist/fence.cjs.map +1 -0
- package/dist/fence.d.cts +4 -0
- package/dist/fence.d.ts +4 -0
- package/dist/fence.js +26 -0
- package/dist/fence.js.map +1 -0
- package/dist/formFactor.cjs.map +1 -1
- package/dist/formFactor.d.cts +11 -2
- package/dist/formFactor.d.ts +11 -2
- package/dist/formFactor.js.map +1 -1
- package/dist/fs.cjs.map +1 -1
- package/dist/fs.d.cts +16 -6
- package/dist/fs.d.ts +16 -6
- package/dist/fs.js.map +1 -1
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +7 -0
- package/dist/hooks.d.ts +7 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +19 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/launch.cjs +45 -6
- package/dist/launch.cjs.map +1 -1
- package/dist/launch.d.cts +3 -0
- package/dist/launch.d.ts +3 -0
- package/dist/launch.js +45 -6
- package/dist/launch.js.map +1 -1
- package/dist/llm.cjs.map +1 -1
- package/dist/llm.d.cts +20 -3
- package/dist/llm.d.ts +20 -3
- package/dist/llm.js.map +1 -1
- package/dist/metadataQueryTool.cjs +271 -0
- package/dist/metadataQueryTool.cjs.map +1 -0
- package/dist/metadataQueryTool.d.cts +159 -0
- package/dist/metadataQueryTool.d.ts +159 -0
- package/dist/metadataQueryTool.js +241 -0
- package/dist/metadataQueryTool.js.map +1 -0
- package/dist/mounts.cjs.map +1 -1
- package/dist/mounts.d.cts +22 -3
- package/dist/mounts.d.ts +22 -3
- package/dist/mounts.js.map +1 -1
- package/dist/onFsChange.cjs +13 -1
- package/dist/onFsChange.cjs.map +1 -1
- package/dist/onFsChange.d.cts +21 -1
- package/dist/onFsChange.d.ts +21 -1
- package/dist/onFsChange.js +13 -1
- package/dist/onFsChange.js.map +1 -1
- package/dist/protocolSchemes.cjs +1 -0
- package/dist/protocolSchemes.cjs.map +1 -1
- package/dist/protocolSchemes.d.cts +1 -0
- package/dist/protocolSchemes.d.ts +1 -0
- package/dist/protocolSchemes.js +2 -0
- package/dist/protocolSchemes.js.map +1 -1
- package/dist/recents.cjs +41 -0
- package/dist/recents.cjs.map +1 -0
- package/dist/recents.d.cts +23 -0
- package/dist/recents.d.ts +23 -0
- package/dist/recents.js +17 -0
- package/dist/recents.js.map +1 -0
- package/dist/tasks.cjs +41 -7
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.d.cts +136 -7
- package/dist/tasks.d.ts +136 -7
- package/dist/tasks.js +39 -7
- package/dist/tasks.js.map +1 -1
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.d.cts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +6 -5
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 /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n // R3-335 — the model's reasoning as it streams. `reasoning-delta` carries the text\n // incrementally (render it live); the terminal `reasoning` carries the WHOLE block\n // plus the `signature` the provider may require on the echo, and is what a caller\n // should put back into the conversation. A provider without reasoning emits neither.\n | { type: 'reasoning-delta'; text: string }\n | { type: 'reasoning'; text: string; signature?: string }\n | { type: 'reasoning-redacted'; data: string }\n // Token accounting for the turn. `cacheReadTokens`/`cacheWriteTokens` are present\n // only on providers that report prompt caching (R3-336) — they are what makes a\n // caching claim verifiable rather than believed, and their ABSENCE is meaningful:\n // it says this provider reports nothing, not that nothing was cached.\n | {\n type: 'usage';\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound the host fails the\n * stream into the SP-7 connect-me prompt (the user adds a key) — the generator\n * throws with `code: 'auth-required'`; an un-granted call throws `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n // Peel `signal` out of the request before it becomes wire params — an AbortSignal\n // can't cross the postMessage boundary as data; it drives the SDK-side cancel frame.\n const { signal, ...params } = req;\n return invokeStream<ChatDelta, ChatResult>('llm:chat', params as unknown as Record<string, unknown>, signal);\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n /** R3-335: the provider emits reasoning blocks. Read it to decide whether to render\n * a thinking surface at all — an empty affordance on a provider that never thinks\n * is worse than none. Normalized to `false` by the channel when a host predating\n * R3-335 omits it, so this is never `undefined` in practice. */\n reasoning: boolean;\n maxContextTokens: number;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n // NOTE (R3-300): `displayName`, `executor` and the resolved per-tier `models` belong\n // here — an app rendering provider state wants all three. They are NOT added yet,\n // deliberately: this interface IS the `llm-provider` channel's declared value, so\n // adding a field is a WIRE change, and the wire is owned by\n // `@immediately-run/sandbox-protocol` (descriptor edit → publish → pin bump on both\n // sides). The protocol snapshot gate enforces exactly that, and it is right to. The\n // enrichment rides R3-307's publish, which already has to touch those descriptors —\n // one publish for two additions rather than two.\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'configured'; provider: ChatProviderInfo };\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\", which is now REPRESENTABLE as distinct from \"not yet answered\".\n// The channel's VALUE stays exactly what the wire carries — `ChatProviderInfo | null` —\n// because the wire did not change here and the protocol snapshot gate reads this type as\n// the channel's shape. The three-state lives BESIDE it: `answered` records whether the host\n// has ever spoken on this channel, which is the one bit `null` cannot carry. Deriving the\n// state rather than widening the channel keeps the wire contract byte-identical, which it\n// is (SDK_PACKAGING_SPEC §9: the wire is additive-only, and this is not a wire change).\n/**\n * Fill in feature flags a host older than the field does not send (R3-335).\n *\n * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it\n * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in\n * features` check, which is exactly the kind of difference that produces one wrong\n * branch a year later — so it is normalized here, once, rather than left to every\n * caller. Absent means \"does not reason\": the fail-closed reading.\n *\n * Exported for its own test; not part of the public surface (`index.ts` re-exports\n * this module wholesale, so it is reachable — it is documented as internal rather\n * than hidden behind a lie).\n * @internal\n */\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The wire value is whatever the host sent, which may predate `reasoning` — so read\n // it as partial rather than trusting the declared type, and decide the flag explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures };\n}\n\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(channel.get());\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (listener: (provider: ChatProviderInfo | null) => void): (() => void) =>\n channel.onChange(listener);\n\n/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (listener: (state: ChatProviderState) => void): (() => void) =>\n channel.onChange((p) => listener(stateOf(p)));\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,qBAA6B;AAC7B,yBAAkC;AAClC,sBAAmD;AA8G5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,aAAO,6BAAoC,YAAY,QAA8C,MAAM;AAC7G;AAyEO,SAAS,sBAAsB,UAA4D;AAChG,MAAI,CAAC,SAAU,QAAO;AAGtB,QAAM,OAAO,SAAS;AACtB,SAAO,EAAE,GAAG,UAAU,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,cAAc,KAAK,EAAkB;AAClG;AAEA,IAAI,WAAW;AACf,MAAM,cAAU,sCAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAO,sBAAuB,IAAI,YAAwC,IAAI;AAAA,EAChF;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAAC,aACnC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,4BAA4B,CAAC,aACxC,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIvC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // A block of the model's own REASONING from a prior `assistant` turn (R3-335).\n // Honored only when the resolved provider advertises `features.reasoning`.\n //\n // Echo these back. On some providers a reasoning block must be replayed — with its\n // `signature` intact and BEFORE the turn's text/tool-use — for the following turn to\n // be accepted at all; a loop that drops them is quietly lossy across turns in a way\n // that shows up as degraded output rather than an error. Pairs with the streamed\n // `reasoning` {@link ChatDelta}.\n | { type: 'reasoning'; text: string; signature?: string }\n // Reasoning the provider REDACTED: opaque bytes with no readable text, which still\n // have to be echoed back in place to keep the chain valid. Never render it.\n | { type: 'reasoning-redacted'; data: string }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n // R3-335 — the model's reasoning as it streams. `reasoning-delta` carries the text\n // incrementally (render it live); the terminal `reasoning` carries the WHOLE block\n // plus the `signature` the provider may require on the echo, and is what a caller\n // should put back into the conversation. A provider without reasoning emits neither.\n | { type: 'reasoning-delta'; text: string }\n | { type: 'reasoning'; text: string; signature?: string }\n | { type: 'reasoning-redacted'; data: string }\n // Token accounting for the turn. `cacheReadTokens`/`cacheWriteTokens` are present\n // only on providers that report prompt caching (R3-336) — they are what makes a\n // caching claim verifiable rather than believed, and their ABSENCE is meaningful:\n // it says this provider reports nothing, not that nothing was cached.\n | {\n type: 'usage';\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound, the host 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/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n // NOTE (R3-300): `displayName`, `executor` and the resolved per-tier `models` belong\n // here — an app rendering provider state wants all three. They are NOT added yet,\n // deliberately: this interface IS the `llm-provider` channel's declared value, so\n // adding a field is a WIRE change, and the wire is owned by\n // `@immediately-run/sandbox-protocol` (descriptor edit → publish → pin bump on both\n // sides). The protocol snapshot gate enforces exactly that, and it is right to. The\n // enrichment rides R3-307's publish, which already has to touch those descriptors —\n // one publish for two additions rather than two.\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n *\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 * Fill in feature flags a host older than the field does not send (R3-335).\n *\n * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it\n * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in\n * features` check, which is exactly the kind of difference that produces one wrong\n * branch a year later — so it is normalized here, once, rather than left to every\n * caller. Absent means \"does not reason\": the fail-closed reading.\n *\n * Exported for its own test; not part of the public surface (`index.ts` re-exports\n * this module wholesale, so it is reachable — it is documented as internal rather\n * than hidden behind a lie).\n * @internal\n */\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The wire value is whatever the host sent, which may predate `reasoning` — so read\n // it as partial rather than trusting the declared type, and decide the flag explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures };\n}\n\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(channel.get());\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (listener: (provider: ChatProviderInfo | null) => void): (() => void) =>\n channel.onChange(listener);\n\n/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (listener: (state: ChatProviderState) => void): (() => void) =>\n channel.onChange((p) => listener(stateOf(p)));\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAeA,qBAA6B;AAC7B,yBAAkC;AAClC,sBAAmD;AAqH5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,aAAO,6BAAoC,YAAY,QAA8C,MAAM;AAC7G;AAmFO,SAAS,sBAAsB,UAA4D;AAChG,MAAI,CAAC,SAAU,QAAO;AAGtB,QAAM,OAAO,SAAS;AACtB,SAAO,EAAE,GAAG,UAAU,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,cAAc,KAAK,EAAkB;AAClG;AAEA,IAAI,WAAW;AACf,MAAM,cAAU,sCAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAO,sBAAuB,IAAI,YAAwC,IAAI;AAAA,EAChF;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAAC,aACnC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,4BAA4B,CAAC,aACxC,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIvC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}
|
package/dist/llm.d.cts
CHANGED
|
@@ -101,9 +101,16 @@ interface ChatResult {
|
|
|
101
101
|
* }
|
|
102
102
|
* ```
|
|
103
103
|
*
|
|
104
|
-
* Requires the `llm:chat` capability. If no provider is bound the host
|
|
105
|
-
*
|
|
106
|
-
*
|
|
104
|
+
* Requires the `llm:chat` capability. If no provider is bound, the host first
|
|
105
|
+
* draws the SP-7 connect-me gate itself (R3-456: the app never draws a
|
|
106
|
+
* credential prompt — that is host chrome, SECRETS_SPEC S3):
|
|
107
|
+
* - the user connects a key → the call retries once and streams normally;
|
|
108
|
+
* - the user declines → the generator throws `code: 'cancelled'` (the same code
|
|
109
|
+
* a declined powerbox produces — a working degraded state: catch it and
|
|
110
|
+
* degrade, e.g. skip the AI feature);
|
|
111
|
+
* - an older host without the gate throws `code: 'provider-not-configured'`.
|
|
112
|
+
* A signed-out user throws `code: 'auth-required'`; an un-granted call throws
|
|
113
|
+
* `forbidden`.
|
|
107
114
|
*/
|
|
108
115
|
declare function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void>;
|
|
109
116
|
/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read
|
|
@@ -137,6 +144,16 @@ interface ChatProviderInfo {
|
|
|
137
144
|
* cannot tell "you need a key" from "ask again in a moment", and consuming apps
|
|
138
145
|
* rendered a misleading "connect a key" banner at users who had one. `unknown` is the
|
|
139
146
|
* state before the host answers; it is not an error and not a prompt to act.
|
|
147
|
+
*
|
|
148
|
+
* **`unknown` is TRANSIENT — the host answers every frame** (R3-419;
|
|
149
|
+
* `LLM_AND_AGENTS_SPEC §4.1` R-LLM-1..3). An app that does not hold `llm:chat` is not
|
|
150
|
+
* met with silence: it is answered `not-configured`, the same terminal state as a user
|
|
151
|
+
* with no key, because from the app's side those are the same fact — do not render a
|
|
152
|
+
* provider, do offer the connect path. So it is correct to treat a `unknown` that
|
|
153
|
+
* persists as a host bug rather than as a state to design around, and WRONG to render a
|
|
154
|
+
* spinner with no timeout on it. (Before R3-419 the host withheld the channel entirely
|
|
155
|
+
* from an ungranted frame, and `unknown` stood forever — that is the failure this note
|
|
156
|
+
* exists to keep from being re-created on the app side.)
|
|
140
157
|
*/
|
|
141
158
|
type ChatProviderState = {
|
|
142
159
|
status: 'unknown';
|
package/dist/llm.d.ts
CHANGED
|
@@ -101,9 +101,16 @@ interface ChatResult {
|
|
|
101
101
|
* }
|
|
102
102
|
* ```
|
|
103
103
|
*
|
|
104
|
-
* Requires the `llm:chat` capability. If no provider is bound the host
|
|
105
|
-
*
|
|
106
|
-
*
|
|
104
|
+
* Requires the `llm:chat` capability. If no provider is bound, the host first
|
|
105
|
+
* draws the SP-7 connect-me gate itself (R3-456: the app never draws a
|
|
106
|
+
* credential prompt — that is host chrome, SECRETS_SPEC S3):
|
|
107
|
+
* - the user connects a key → the call retries once and streams normally;
|
|
108
|
+
* - the user declines → the generator throws `code: 'cancelled'` (the same code
|
|
109
|
+
* a declined powerbox produces — a working degraded state: catch it and
|
|
110
|
+
* degrade, e.g. skip the AI feature);
|
|
111
|
+
* - an older host without the gate throws `code: 'provider-not-configured'`.
|
|
112
|
+
* A signed-out user throws `code: 'auth-required'`; an un-granted call throws
|
|
113
|
+
* `forbidden`.
|
|
107
114
|
*/
|
|
108
115
|
declare function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void>;
|
|
109
116
|
/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read
|
|
@@ -137,6 +144,16 @@ interface ChatProviderInfo {
|
|
|
137
144
|
* cannot tell "you need a key" from "ask again in a moment", and consuming apps
|
|
138
145
|
* rendered a misleading "connect a key" banner at users who had one. `unknown` is the
|
|
139
146
|
* state before the host answers; it is not an error and not a prompt to act.
|
|
147
|
+
*
|
|
148
|
+
* **`unknown` is TRANSIENT — the host answers every frame** (R3-419;
|
|
149
|
+
* `LLM_AND_AGENTS_SPEC §4.1` R-LLM-1..3). An app that does not hold `llm:chat` is not
|
|
150
|
+
* met with silence: it is answered `not-configured`, the same terminal state as a user
|
|
151
|
+
* with no key, because from the app's side those are the same fact — do not render a
|
|
152
|
+
* provider, do offer the connect path. So it is correct to treat a `unknown` that
|
|
153
|
+
* persists as a host bug rather than as a state to design around, and WRONG to render a
|
|
154
|
+
* spinner with no timeout on it. (Before R3-419 the host withheld the channel entirely
|
|
155
|
+
* from an ungranted frame, and `unknown` stood forever — that is the failure this note
|
|
156
|
+
* exists to keep from being re-created on the app side.)
|
|
140
157
|
*/
|
|
141
158
|
type ChatProviderState = {
|
|
142
159
|
status: 'unknown';
|
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 /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n // R3-335 — the model's reasoning as it streams. `reasoning-delta` carries the text\n // incrementally (render it live); the terminal `reasoning` carries the WHOLE block\n // plus the `signature` the provider may require on the echo, and is what a caller\n // should put back into the conversation. A provider without reasoning emits neither.\n | { type: 'reasoning-delta'; text: string }\n | { type: 'reasoning'; text: string; signature?: string }\n | { type: 'reasoning-redacted'; data: string }\n // Token accounting for the turn. `cacheReadTokens`/`cacheWriteTokens` are present\n // only on providers that report prompt caching (R3-336) — they are what makes a\n // caching claim verifiable rather than believed, and their ABSENCE is meaningful:\n // it says this provider reports nothing, not that nothing was cached.\n | {\n type: 'usage';\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound the host fails the\n * stream into the SP-7 connect-me prompt (the user adds a key) — the generator\n * throws with `code: 'auth-required'`; an un-granted call throws `forbidden`.\n */\nexport function chat(req: ChatRequest): AsyncGenerator<ChatDelta, ChatResult, void> {\n // Peel `signal` out of the request before it becomes wire params — an AbortSignal\n // can't cross the postMessage boundary as data; it drives the SDK-side cancel frame.\n const { signal, ...params } = req;\n return invokeStream<ChatDelta, ChatResult>('llm:chat', params as unknown as Record<string, unknown>, signal);\n}\n\n/** The resolved provider's advertised abilities (SERVICE_PROVIDERS_SPEC §2.5) — read\n * to branch/degrade (offer image upload only when `vision`). */\nexport interface ChatFeatures {\n vision: boolean;\n tools: boolean;\n jsonMode: boolean;\n /** R3-335: the provider emits reasoning blocks. Read it to decide whether to render\n * a thinking surface at all — an empty affordance on a provider that never thinks\n * is worse than none. Normalized to `false` by the channel when a host predating\n * R3-335 omits it, so this is never `undefined` in practice. */\n reasoning: boolean;\n maxContextTokens: number;\n}\n\n/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n // NOTE (R3-300): `displayName`, `executor` and the resolved per-tier `models` belong\n // here — an app rendering provider state wants all three. They are NOT added yet,\n // deliberately: this interface IS the `llm-provider` channel's declared value, so\n // adding a field is a WIRE change, and the wire is owned by\n // `@immediately-run/sandbox-protocol` (descriptor edit → publish → pin bump on both\n // sides). The protocol snapshot gate enforces exactly that, and it is right to. The\n // enrichment rides R3-307's publish, which already has to touch those descriptors —\n // one publish for two additions rather than two.\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n */\nexport type ChatProviderState =\n | { status: 'unknown' }\n | { status: 'not-configured' }\n | { status: 'configured'; provider: ChatProviderInfo };\n\n// The `llm-provider` describe channel (Recipe A): the host pushes the resolved\n// provider info on change and replays it on register-frame, gated by `llm:chat`.\n// A message with no `provider` key is ignored; an explicit `null` means \"no provider\n// bound\", which is now REPRESENTABLE as distinct from \"not yet answered\".\n// The channel's VALUE stays exactly what the wire carries — `ChatProviderInfo | null` —\n// because the wire did not change here and the protocol snapshot gate reads this type as\n// the channel's shape. The three-state lives BESIDE it: `answered` records whether the host\n// has ever spoken on this channel, which is the one bit `null` cannot carry. Deriving the\n// state rather than widening the channel keeps the wire contract byte-identical, which it\n// is (SDK_PACKAGING_SPEC §9: the wire is additive-only, and this is not a wire change).\n/**\n * Fill in feature flags a host older than the field does not send (R3-335).\n *\n * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it\n * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in\n * features` check, which is exactly the kind of difference that produces one wrong\n * branch a year later — so it is normalized here, once, rather than left to every\n * caller. Absent means \"does not reason\": the fail-closed reading.\n *\n * Exported for its own test; not part of the public surface (`index.ts` re-exports\n * this module wholesale, so it is reachable — it is documented as internal rather\n * than hidden behind a lie).\n * @internal\n */\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The wire value is whatever the host sent, which may predate `reasoning` — so read\n // it as partial rather than trusting the declared type, and decide the flag explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures };\n}\n\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(channel.get());\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (listener: (provider: ChatProviderInfo | null) => void): (() => void) =>\n channel.onChange(listener);\n\n/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (listener: (state: ChatProviderState) => void): (() => void) =>\n channel.onChange((p) => listener(stateOf(p)));\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";AAeA,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,cAAc,4BAA4B;AA8G5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,SAAO,aAAoC,YAAY,QAA8C,MAAM;AAC7G;AAyEO,SAAS,sBAAsB,UAA4D;AAChG,MAAI,CAAC,SAAU,QAAO;AAGtB,QAAM,OAAO,SAAS;AACtB,SAAO,EAAE,GAAG,UAAU,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,cAAc,KAAK,EAAkB;AAClG;AAEA,IAAI,WAAW;AACf,MAAM,UAAU,kBAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAO,sBAAuB,IAAI,YAAwC,IAAI;AAAA,EAChF;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAAC,aACnC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,4BAA4B,CAAC,aACxC,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIvC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/llm.ts"],"sourcesContent":["// Provider-agnostic LLM chat — the `llm.chat@1` slot (SERVICE_PROVIDERS_SPEC;\n// LLM_AND_AGENTS_SPEC §8 D5).\n//\n// An app calls ONE chat slot and never worries about which provider the user has a\n// key for: the HOST resolves which vendor answers from the key the user holds\n// (`SecretView.boundOrigin`) plus their `preferredImplementation` choice, normalizes\n// the wire format, injects the key host-side at the §6 net:fetch point (the\n// look-at-nothing proxy), and streams normalized deltas back. The app never names a\n// vendor, never sees the key, and needs NO `net:fetch`/`secrets` grant of its own —\n// only the `llm:chat` capability (elevated, app-scoped: a fork earns it by consent).\n//\n// Inert until the host implements `protocol-llm` (the `chat` stream) + the\n// `llm-provider` describe channel; the contract ships here so apps (the file-explorer\n// summarize fork) can be written against it — exactly how `secrets.ts` shipped ahead\n// of `protocol-secrets`.\nimport { invokeStream } from './catalog';\nimport { createPushChannel } from './pushChannel';\nimport { LLM_PROVIDER, REQUEST_LLM_PROVIDER } from './generated/protocol';\n\n/** Who authored a {@link ChatMessage}. */\nexport type ChatRole = 'system' | 'user' | 'assistant' | 'tool';\n\n/** A part of a message. `image` is only honored when the resolved provider\n * advertises `features.vision` (§2.5); `tool-use`/`tool-result` only when it\n * advertises `features.tools` — branch on {@link describeChat} first. */\nexport type ContentPart =\n | { type: 'text'; text: string }\n | { type: 'image'; mimeType: string; data: string } // data: base64, no data: URL prefix\n // A tool call the model emitted on a prior `assistant` turn — replay it in the\n // conversation so a follow-up request carries the agentic history. Pairs with the\n // streamed `tool-call` {@link ChatDelta} that first surfaced it.\n | { type: 'tool-use'; id: string; name: string; input: Record<string, unknown> }\n // A block of the model's own REASONING from a prior `assistant` turn (R3-335).\n // Honored only when the resolved provider advertises `features.reasoning`.\n //\n // Echo these back. On some providers a reasoning block must be replayed — with its\n // `signature` intact and BEFORE the turn's text/tool-use — for the following turn to\n // be accepted at all; a loop that drops them is quietly lossy across turns in a way\n // that shows up as degraded output rather than an error. Pairs with the streamed\n // `reasoning` {@link ChatDelta}.\n | { type: 'reasoning'; text: string; signature?: string }\n // Reasoning the provider REDACTED: opaque bytes with no readable text, which still\n // have to be echoed back in place to keep the chain valid. Never render it.\n | { type: 'reasoning-redacted'; data: string }\n // The result of executing a `tool-use`, fed back so the model can continue. Carried\n // on a `user`/`tool`-role message; `toolCallId` matches the `tool-use` `id`.\n | { type: 'tool-result'; toolCallId: string; content: string; isError?: boolean };\n\n/** One message in a {@link ChatRequest}: a role plus its content parts. */\nexport interface ChatMessage {\n role: ChatRole;\n content: ContentPart[];\n}\n\n/** A tool the model may call — honored only when `features.tools`. */\nexport interface ToolDef {\n name: string;\n description?: string;\n /** JSON-Schema for the tool's arguments. */\n inputSchema: Record<string, unknown>;\n}\n\n/** A host-brokered chat completion request: the messages plus optional tools,\n * response format, and model hint (each honored per the provider's features). */\nexport interface ChatRequest {\n messages: ChatMessage[];\n /** Honored only when the resolved provider advertises `features.tools`. */\n tools?: ToolDef[];\n /** `'json'` honored only when `features.jsonMode`. Defaults to `'text'`. */\n responseFormat?: 'text' | 'json';\n maxTokens?: number;\n /** An ABSTRACT tier hint, never a vendor model id — the host maps it to a concrete\n * model on the resolved provider. Omit to take the provider's default. */\n modelHint?: 'fast' | 'smart';\n /** Abort the completion mid-stream. When it fires, the SDK sends the host a cancel\n * frame so the host aborts the upstream provider request and STOPS BILLING the\n * user's key — not merely stops the app-side iterator (LLM_AND_AGENTS_SPEC §3.3\n * \"abort the in-flight LLM request\", R3-224). Not sent over the wire (an\n * `AbortSignal` isn't serializable); handled SDK-side. */\n signal?: AbortSignal;\n}\n\n/** One streamed chunk. Consumers typically accumulate `text-delta`s. */\nexport type ChatDelta =\n | { type: 'text-delta'; text: string }\n | { type: 'tool-call'; id: string; name: string; input: unknown }\n // R3-335 — the model's reasoning as it streams. `reasoning-delta` carries the text\n // incrementally (render it live); the terminal `reasoning` carries the WHOLE block\n // plus the `signature` the provider may require on the echo, and is what a caller\n // should put back into the conversation. A provider without reasoning emits neither.\n | { type: 'reasoning-delta'; text: string }\n | { type: 'reasoning'; text: string; signature?: string }\n | { type: 'reasoning-redacted'; data: string }\n // Token accounting for the turn. `cacheReadTokens`/`cacheWriteTokens` are present\n // only on providers that report prompt caching (R3-336) — they are what makes a\n // caching claim verifiable rather than believed, and their ABSENCE is meaningful:\n // it says this provider reports nothing, not that nothing was cached.\n | {\n type: 'usage';\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n };\n\n/** Why generation stopped: natural `end`, `length` cap, a `tool` call, or content `filtered`. */\nexport type ChatStopReason = 'end' | 'length' | 'tool' | 'filtered';\n\n/** The terminal value of the {@link chat} stream. */\nexport interface ChatResult {\n stopReason: ChatStopReason;\n}\n\n/**\n * Stream a chat completion from whichever provider the user has configured.\n *\n * ```ts\n * let summary = '';\n * for await (const d of chat({ messages: [{ role: 'user', content: [{ type: 'text', text }] }] })) {\n * if (d.type === 'text-delta') summary += d.text;\n * }\n * ```\n *\n * Requires the `llm:chat` capability. If no provider is bound, the host 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/** Info about the provider the host resolved for this app. `null` when no provider\n * is bound (SP-7: prompt the user to add a key before calling {@link chat}). */\nexport interface ChatProviderInfo {\n /** Opaque provider id, e.g. `llm.chat.anthropic` — never a vendor secret or model id. */\n providerId: string;\n /** True for Host-proxied providers (host-vouched, SP-9); false for app-level ones,\n * whose `features` are an untrusted claim. */\n hostVouched: boolean;\n features: ChatFeatures;\n // NOTE (R3-300): `displayName`, `executor` and the resolved per-tier `models` belong\n // here — an app rendering provider state wants all three. They are NOT added yet,\n // deliberately: this interface IS the `llm-provider` channel's declared value, so\n // adding a field is a WIRE change, and the wire is owned by\n // `@immediately-run/sandbox-protocol` (descriptor edit → publish → pin bump on both\n // sides). The protocol snapshot gate enforces exactly that, and it is right to. The\n // enrichment rides R3-307's publish, which already has to touch those descriptors —\n // one publish for two additions rather than two.\n}\n\n/**\n * Whether the host has told us about a provider yet, and if so whether one is bound.\n *\n * THREE states, because two is the bug (R3-300). `describeChat()` returns `null` both\n * when no provider is configured AND when the channel has not answered — so an app\n * cannot tell \"you need a key\" from \"ask again in a moment\", and consuming apps\n * rendered a misleading \"connect a key\" banner at users who had one. `unknown` is the\n * state before the host answers; it is not an error and not a prompt to act.\n *\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 * Fill in feature flags a host older than the field does not send (R3-335).\n *\n * `features.reasoning` arrived after `ChatFeatures` shipped, so a host predating it\n * omits the key. `undefined` reads as falsy everywhere EXCEPT a `'reasoning' in\n * features` check, which is exactly the kind of difference that produces one wrong\n * branch a year later — so it is normalized here, once, rather than left to every\n * caller. Absent means \"does not reason\": the fail-closed reading.\n *\n * Exported for its own test; not part of the public surface (`index.ts` re-exports\n * this module wholesale, so it is reachable — it is documented as internal rather\n * than hidden behind a lie).\n * @internal\n */\nexport function normalizeProviderInfo(provider: ChatProviderInfo | null): ChatProviderInfo | null {\n if (!provider) return null;\n // The wire value is whatever the host sent, which may predate `reasoning` — so read\n // it as partial rather than trusting the declared type, and decide the flag explicitly.\n const wire = provider.features as Partial<ChatFeatures>;\n return { ...provider, features: { ...wire, reasoning: wire.reasoning === true } as ChatFeatures };\n}\n\nlet answered = false;\nconst channel = createPushChannel<ChatProviderInfo | null>({\n pushType: LLM_PROVIDER,\n requestType: REQUEST_LLM_PROVIDER,\n initial: null,\n parse: (msg) => {\n if (!('provider' in msg)) return undefined;\n answered = true;\n return normalizeProviderInfo((msg.provider as ChatProviderInfo | null) ?? null);\n },\n});\n\n/** Derive the three-state from the wire value plus whether the host has answered. */\nconst stateOf = (provider: ChatProviderInfo | null): ChatProviderState =>\n !answered ? { status: 'unknown' } : provider ? { status: 'configured', provider } : { status: 'not-configured' };\n\n/**\n * The provider the host resolved for this app, or `null`.\n *\n * Kept for compatibility (`ways_of_working §6`, additive-only): it collapses `unknown`\n * and `not-configured` to `null`. Prefer {@link describeChatState} when the difference\n * matters — which is any time you would render \"connect a key\", because doing that in\n * the `unknown` state is exactly the false banner R3-300 fixes.\n */\nexport const describeChat = (): ChatProviderInfo | null => channel.get();\n\n/** The three-state read: `unknown` before the host answers, then configured or not. */\nexport const describeChatState = (): ChatProviderState => stateOf(channel.get());\n\n/** Subscribe to provider changes (key added/revoked, preference changed). Invoked\n * immediately with the current value, then on every change. Returns unsubscribe. */\nexport const onChatProviderChange = (listener: (provider: ChatProviderInfo | null) => void): (() => void) =>\n channel.onChange(listener);\n\n/** Subscribe to the three-state provider description. */\nexport const onChatProviderStateChange = (listener: (state: ChatProviderState) => void): (() => void) =>\n channel.onChange((p) => listener(stateOf(p)));\n\n/** React hook returning the resolved chat provider (or `null`), re-rendering on\n * change — gate the summarize affordance on `provider !== null`. */\nexport const useChatProvider = (): ChatProviderInfo | null => channel.use();\n\n/**\n * React hook returning the three-state description.\n *\n * Use this to render provider state honestly: show nothing (or a neutral placeholder)\n * while `unknown`, the connect affordance only on `not-configured`, and the provider's\n * name on `configured`.\n */\nexport const useChatProviderState = (): ChatProviderState => stateOf(channel.use());\n"],"mappings":";AAeA,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,cAAc,4BAA4B;AAqH5C,SAAS,KAAK,KAA+D;AAGlF,QAAM,EAAE,QAAQ,GAAG,OAAO,IAAI;AAC9B,SAAO,aAAoC,YAAY,QAA8C,MAAM;AAC7G;AAmFO,SAAS,sBAAsB,UAA4D;AAChG,MAAI,CAAC,SAAU,QAAO;AAGtB,QAAM,OAAO,SAAS;AACtB,SAAO,EAAE,GAAG,UAAU,UAAU,EAAE,GAAG,MAAM,WAAW,KAAK,cAAc,KAAK,EAAkB;AAClG;AAEA,IAAI,WAAW;AACf,MAAM,UAAU,kBAA2C;AAAA,EACzD,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAQ;AACd,QAAI,EAAE,cAAc,KAAM,QAAO;AACjC,eAAW;AACX,WAAO,sBAAuB,IAAI,YAAwC,IAAI;AAAA,EAChF;AACF,CAAC;AAGD,MAAM,UAAU,CAAC,aACf,CAAC,WAAW,EAAE,QAAQ,UAAU,IAAI,WAAW,EAAE,QAAQ,cAAc,SAAS,IAAI,EAAE,QAAQ,iBAAiB;AAU1G,MAAM,eAAe,MAA+B,QAAQ,IAAI;AAGhE,MAAM,oBAAoB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;AAIxE,MAAM,uBAAuB,CAAC,aACnC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,4BAA4B,CAAC,aACxC,QAAQ,SAAS,CAAC,MAAM,SAAS,QAAQ,CAAC,CAAC,CAAC;AAIvC,MAAM,kBAAkB,MAA+B,QAAQ,IAAI;AASnE,MAAM,uBAAuB,MAAyB,QAAQ,QAAQ,IAAI,CAAC;","names":[]}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var metadataQueryTool_exports = {};
|
|
20
|
+
__export(metadataQueryTool_exports, {
|
|
21
|
+
METADATA_HEADINGS_KEY: () => METADATA_HEADINGS_KEY,
|
|
22
|
+
METADATA_QUERY_TOOL_DESCRIPTOR: () => METADATA_QUERY_TOOL_DESCRIPTOR,
|
|
23
|
+
METADATA_QUERY_TOOL_NAME: () => METADATA_QUERY_TOOL_NAME,
|
|
24
|
+
MetadataQueryError: () => MetadataQueryError,
|
|
25
|
+
createMetadataQueryTool: () => createMetadataQueryTool,
|
|
26
|
+
executeMetadataQuery: () => executeMetadataQuery,
|
|
27
|
+
globToRegExp: () => globToRegExp,
|
|
28
|
+
runMetadataQuery: () => runMetadataQuery
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(metadataQueryTool_exports);
|
|
31
|
+
var import_fence = require("./fence");
|
|
32
|
+
const METADATA_QUERY_TOOL_NAME = "metadata:query";
|
|
33
|
+
const METADATA_QUERY_TOOL_DESCRIPTOR = {
|
|
34
|
+
name: METADATA_QUERY_TOOL_NAME,
|
|
35
|
+
description: "Query the MDX corpus index: paths, frontmatter, and headings of entries. Declarative filters only (no expressions). Returns index rows \u2014 never file bodies.",
|
|
36
|
+
paramsSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
properties: {
|
|
40
|
+
pathGlob: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Glob over the entry path relative to the corpus root (`*` one segment, `**` any)."
|
|
43
|
+
},
|
|
44
|
+
where: {
|
|
45
|
+
type: "array",
|
|
46
|
+
items: {
|
|
47
|
+
type: "object",
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
required: ["key", "op"],
|
|
50
|
+
properties: {
|
|
51
|
+
key: { type: "string", description: "Dotted frontmatter field path." },
|
|
52
|
+
op: { type: "string", enum: ["eq", "contains", "in", "exists"] },
|
|
53
|
+
value: {
|
|
54
|
+
description: "Scalar for `eq`; array of scalars for `in`; unused by `exists`.",
|
|
55
|
+
anyOf: [
|
|
56
|
+
{ type: ["string", "number", "boolean", "null"] },
|
|
57
|
+
{ type: "array", items: { type: ["string", "number", "boolean", "null"] } }
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
sortBy: { type: "string" },
|
|
64
|
+
order: { type: "string", enum: ["asc", "desc"] },
|
|
65
|
+
limit: { type: "integer", minimum: 1, maximum: 500 },
|
|
66
|
+
select: { type: "array", items: { type: "string" } }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
class MetadataQueryError extends Error {
|
|
71
|
+
constructor() {
|
|
72
|
+
super(...arguments);
|
|
73
|
+
this.code = "invalid-params";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const RESERVED_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
77
|
+
const SCALAR = /* @__PURE__ */ new Set(["string", "number", "boolean"]);
|
|
78
|
+
function validateDottedKey(key, what) {
|
|
79
|
+
if (typeof key !== "string" || !key) throw new MetadataQueryError(`${what} must be a non-empty string`);
|
|
80
|
+
if (key.length > 128) throw new MetadataQueryError(`${what} is too long (max 128 chars)`);
|
|
81
|
+
if (/[/\\\u0000-\u001f]/.test(key))
|
|
82
|
+
throw new MetadataQueryError(`${what} must be a dotted path (no path separators)`);
|
|
83
|
+
for (const seg of key.split(".")) {
|
|
84
|
+
if (!seg) throw new MetadataQueryError(`${what} has an empty segment: ${JSON.stringify(key)}`);
|
|
85
|
+
if (RESERVED_SEGMENTS.has(seg)) throw new MetadataQueryError(`${what} contains a reserved segment: ${seg}`);
|
|
86
|
+
}
|
|
87
|
+
return key;
|
|
88
|
+
}
|
|
89
|
+
function validateInput(raw) {
|
|
90
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
91
|
+
throw new MetadataQueryError("input must be an object");
|
|
92
|
+
}
|
|
93
|
+
const src = raw;
|
|
94
|
+
for (const k of Object.keys(src)) {
|
|
95
|
+
if (!["pathGlob", "where", "sortBy", "order", "limit", "select"].includes(k)) {
|
|
96
|
+
throw new MetadataQueryError(`unknown argument: ${k}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const out = {};
|
|
100
|
+
if (src.pathGlob !== void 0) {
|
|
101
|
+
if (typeof src.pathGlob !== "string" || !src.pathGlob || src.pathGlob.length > 256) {
|
|
102
|
+
throw new MetadataQueryError("pathGlob must be a non-empty string (\u2264256 chars)");
|
|
103
|
+
}
|
|
104
|
+
if (/[\u0000\n\r]/.test(src.pathGlob)) throw new MetadataQueryError("pathGlob contains control characters");
|
|
105
|
+
out.pathGlob = src.pathGlob;
|
|
106
|
+
}
|
|
107
|
+
if (src.where !== void 0) {
|
|
108
|
+
if (!Array.isArray(src.where) || src.where.length > 32) {
|
|
109
|
+
throw new MetadataQueryError("where must be an array of at most 32 filters");
|
|
110
|
+
}
|
|
111
|
+
out.where = src.where.map((w, i) => {
|
|
112
|
+
if (typeof w !== "object" || w === null || Array.isArray(w)) {
|
|
113
|
+
throw new MetadataQueryError(`where[${i}] must be an object`);
|
|
114
|
+
}
|
|
115
|
+
const { key, op, value } = w;
|
|
116
|
+
validateDottedKey(key, `where[${i}].key`);
|
|
117
|
+
if (op !== "eq" && op !== "contains" && op !== "in" && op !== "exists") {
|
|
118
|
+
throw new MetadataQueryError(`where[${i}].op must be eq|contains|in|exists`);
|
|
119
|
+
}
|
|
120
|
+
if (value !== void 0) {
|
|
121
|
+
if (typeof value === "function") {
|
|
122
|
+
throw new MetadataQueryError(`where[${i}].value must be data, not a function`);
|
|
123
|
+
}
|
|
124
|
+
if (op === "in") {
|
|
125
|
+
if (!Array.isArray(value) || value.length > 100 || value.some((v) => !SCALAR.has(typeof v) && v !== null)) {
|
|
126
|
+
throw new MetadataQueryError(`where[${i}].value for op=in must be an array of scalars`);
|
|
127
|
+
}
|
|
128
|
+
} else if (!SCALAR.has(typeof value) && value !== null) {
|
|
129
|
+
if (typeof value === "object")
|
|
130
|
+
throw new MetadataQueryError(`where[${i}].value for op=${op} must be a scalar`);
|
|
131
|
+
throw new MetadataQueryError(`where[${i}].value for op=${op} must be a scalar`);
|
|
132
|
+
}
|
|
133
|
+
} else if (op !== "exists") {
|
|
134
|
+
throw new MetadataQueryError(`where[${i}].value is required for op=${op}`);
|
|
135
|
+
}
|
|
136
|
+
return { key, op, ...value !== void 0 ? { value } : {} };
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (src.sortBy !== void 0) out.sortBy = validateDottedKey(src.sortBy, "sortBy");
|
|
140
|
+
if (src.order !== void 0) {
|
|
141
|
+
if (src.order !== "asc" && src.order !== "desc") throw new MetadataQueryError("order must be asc|desc");
|
|
142
|
+
out.order = src.order;
|
|
143
|
+
}
|
|
144
|
+
if (src.limit !== void 0) {
|
|
145
|
+
if (typeof src.limit !== "number" || !Number.isInteger(src.limit) || src.limit < 1 || src.limit > 500) {
|
|
146
|
+
throw new MetadataQueryError("limit must be an integer 1\u2013500");
|
|
147
|
+
}
|
|
148
|
+
out.limit = src.limit;
|
|
149
|
+
}
|
|
150
|
+
if (src.select !== void 0) {
|
|
151
|
+
if (!Array.isArray(src.select) || src.select.length > 64) {
|
|
152
|
+
throw new MetadataQueryError("select must be an array of at most 64 field paths");
|
|
153
|
+
}
|
|
154
|
+
out.select = src.select.map((k, i) => validateDottedKey(k, `select[${i}]`));
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
function resolveOwn(meta, key) {
|
|
159
|
+
let cur = meta;
|
|
160
|
+
for (const seg of key.split(".")) {
|
|
161
|
+
if (typeof cur !== "object" || cur === null) return void 0;
|
|
162
|
+
cur = Object.prototype.hasOwnProperty.call(cur, seg) ? cur[seg] : void 0;
|
|
163
|
+
if (cur === void 0) return void 0;
|
|
164
|
+
}
|
|
165
|
+
return cur;
|
|
166
|
+
}
|
|
167
|
+
function globToRegExp(glob) {
|
|
168
|
+
const esc = glob.replace(/[.+^${}()|[\]\\?]/g, "\\$&");
|
|
169
|
+
const re = esc.replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\u0000/g, ".*");
|
|
170
|
+
return new RegExp(`^${re}$`);
|
|
171
|
+
}
|
|
172
|
+
function matches(row, w) {
|
|
173
|
+
const v = resolveOwn(row, w.key);
|
|
174
|
+
switch (w.op) {
|
|
175
|
+
case "exists":
|
|
176
|
+
return v !== void 0;
|
|
177
|
+
case "eq":
|
|
178
|
+
return v === w.value || v === null && w.value === null;
|
|
179
|
+
case "contains": {
|
|
180
|
+
const needle = w.value;
|
|
181
|
+
if (typeof needle !== "string") return false;
|
|
182
|
+
if (typeof v === "string") return v.includes(needle);
|
|
183
|
+
if (Array.isArray(v)) return v.some((x) => typeof x === "string" && x.includes(needle));
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
case "in": {
|
|
187
|
+
if (!Array.isArray(w.value)) return false;
|
|
188
|
+
return w.value.some((needle) => v === needle);
|
|
189
|
+
}
|
|
190
|
+
default:
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function compare(a, b) {
|
|
195
|
+
const rank = (v) => v === void 0 ? 0 : 1;
|
|
196
|
+
if (rank(a) !== rank(b)) return rank(a) - rank(b);
|
|
197
|
+
if (a === void 0) return 0;
|
|
198
|
+
const as = typeof a === "number" && typeof b === "number" ? a : String(a);
|
|
199
|
+
const bs = typeof a === "number" && typeof b === "number" ? b : String(b);
|
|
200
|
+
return as < bs ? -1 : as > bs ? 1 : 0;
|
|
201
|
+
}
|
|
202
|
+
function normalizeChroot(chroot) {
|
|
203
|
+
const c = chroot.startsWith("/") ? chroot : `/${chroot}`;
|
|
204
|
+
return c.endsWith("/") ? c : `${c}/`;
|
|
205
|
+
}
|
|
206
|
+
function runMetadataQuery(index, chroot, raw, filter) {
|
|
207
|
+
const input = validateInput(raw);
|
|
208
|
+
const root = normalizeChroot(chroot);
|
|
209
|
+
const globRe = input.pathGlob !== void 0 ? globToRegExp(input.pathGlob) : null;
|
|
210
|
+
const rows = [];
|
|
211
|
+
for (const [abs, rowMeta] of Object.entries(index)) {
|
|
212
|
+
if (!abs.startsWith(root)) continue;
|
|
213
|
+
if (filter && !filter(abs)) continue;
|
|
214
|
+
const rel = abs.slice(root.length);
|
|
215
|
+
if (globRe && !globRe.test(rel)) continue;
|
|
216
|
+
const { headings, ...frontmatter } = rowMeta;
|
|
217
|
+
const meta = frontmatter;
|
|
218
|
+
if (input.where && !input.where.every((w) => matches(meta, w))) continue;
|
|
219
|
+
rows.push({ rel, meta, headings: Array.isArray(headings) ? headings : void 0 });
|
|
220
|
+
}
|
|
221
|
+
if (input.sortBy) {
|
|
222
|
+
const dir = input.order === "desc" ? -1 : 1;
|
|
223
|
+
rows.sort(
|
|
224
|
+
(x, y) => dir * compare(resolveOwn(x.meta, input.sortBy), resolveOwn(y.meta, input.sortBy))
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const limit = input.limit ?? 100;
|
|
228
|
+
const sliced = rows.slice(0, limit);
|
|
229
|
+
return sliced.map(({ rel, meta, headings }) => {
|
|
230
|
+
let out = meta;
|
|
231
|
+
if (input.select) {
|
|
232
|
+
out = {};
|
|
233
|
+
for (const key of input.select) {
|
|
234
|
+
const v = resolveOwn(meta, key);
|
|
235
|
+
if (v !== void 0) out[key] = v;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return { path: rel, meta: out, ...headings ? { headings } : {} };
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const METADATA_HEADINGS_KEY = "headings";
|
|
242
|
+
function executeMetadataQuery(index, chroot, raw, filter) {
|
|
243
|
+
try {
|
|
244
|
+
const rows = runMetadataQuery(index, chroot, raw, filter);
|
|
245
|
+
return { content: (0, import_fence.fenceUntrusted)("tool-result: metadata rows", JSON.stringify(rows, null, 2)) };
|
|
246
|
+
} catch (e) {
|
|
247
|
+
const code = e?.code ?? "invalid-params";
|
|
248
|
+
return { content: `${code}: ${e.message}`, isError: true };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function createMetadataQueryTool(options) {
|
|
252
|
+
const d = METADATA_QUERY_TOOL_DESCRIPTOR;
|
|
253
|
+
return {
|
|
254
|
+
name: d.name,
|
|
255
|
+
description: d.description,
|
|
256
|
+
inputSchema: d.paramsSchema,
|
|
257
|
+
execute: (raw) => executeMetadataQuery(options.getIndex(), options.chroot, raw, options.filter)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
261
|
+
0 && (module.exports = {
|
|
262
|
+
METADATA_HEADINGS_KEY,
|
|
263
|
+
METADATA_QUERY_TOOL_DESCRIPTOR,
|
|
264
|
+
METADATA_QUERY_TOOL_NAME,
|
|
265
|
+
MetadataQueryError,
|
|
266
|
+
createMetadataQueryTool,
|
|
267
|
+
executeMetadataQuery,
|
|
268
|
+
globToRegExp,
|
|
269
|
+
runMetadataQuery
|
|
270
|
+
});
|
|
271
|
+
//# sourceMappingURL=metadataQueryTool.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/metadataQueryTool.ts"],"sourcesContent":["// The MDX metadata query tool (GROVE_AGENT_SPEC §4) — the agent-facing form of the\n// frontmatter index the platform already builds (`filesMetadata`).\n//\n// An embedded agent answers \"which entries are tagged `security` and updated since\n// June\" in ONE call against an in-sandbox index instead of N `read_entry` round-trips\n// over every file. SDK-LOCAL execution: a projection of the index the app already\n// holds — no host round-trip, no new authority. Results are INDEX ROWS (path +\n// frontmatter subset + the entry's heading list), never file bodies: the cheap call\n// stays cheap, and the read that taints is the explicit one (`read_entry`).\n//\n// SECURITY SHAPE (G-GA-3/G-GA-4/G-GA-11):\n// • Declarative filters, never expressions — the arguments are DATA, validated at\n// this boundary. Model-supplied arguments are untrusted input; a predicate DSL\n// would be an interpreter fed by it (the BUNDLE_EMBEDDING §4.1 rejection,\n// restated for tools: dotted own-property paths, `__proto__`/`constructor`/\n// `prototype` segments rejected).\n// • Execution is pure over the index — it cannot open a file body even by accident.\n// • Rows are confined to a caller-supplied chroot: the raw index can span more than\n// the corpus mount, and no row whose body the read tool cannot legally open may\n// be returned.\n//\n// TAINT (GROVE_AGENT_SPEC §4, \"the enforceable statement\"): taint attaches where the\n// host can see — at the mount reads that BUILD the index — not at this query. An\n// external consumer executing the canonical descriptor must route through a\n// host-visible read of the backing mount, never a copy of this in-sandbox structure.\n\nimport type { FilesMetadata, Metadata } from './sandboxTypes';\nimport { fenceUntrusted } from './fence';\n\n/** The tool's canonical name — `metadata:query`, catalog-shaped. */\nexport const METADATA_QUERY_TOOL_NAME = 'metadata:query';\n\n/** One heading of an indexed entry — the additive index extension (GROVE_AGENT_SPEC\n * §4: `headings: [{id, text, depth}]`, ids from the mdx-plugins heading-anchor canon). */\nexport interface HeadingSummary {\n id: string;\n text: string;\n /** 1–6, the ATX depth. */\n depth: number;\n}\n\n/** The declarative filter set — the tool's whole argument vocabulary. */\nexport interface MetadataQueryInput {\n /** Glob over the row's path RELATIVE to the chroot root (`*` within one segment,\n * `**` across segments). Default: every row. */\n pathGlob?: string;\n /** Frontmatter filters, ANDed together. */\n where?: Array<{\n /** Dotted own-property path into the row's frontmatter (one nesting level is\n * representable in frontmatter; deeper paths simply never match). */\n key: string;\n op: 'eq' | 'contains' | 'in' | 'exists';\n /** For `eq`: a scalar. For `in`: an array of scalars. Unused by `exists`. */\n value?: unknown;\n }>;\n /** Dotted frontmatter key to sort by (rows lacking it sort last, stably). */\n sortBy?: string;\n /** Default `'asc'`. */\n order?: 'asc' | 'desc';\n /** Cap on returned rows (default 100, max 500 — the cheap call stays cheap). */\n limit?: number;\n /** Frontmatter fields to include per row (dotted). Default: the whole record. */\n select?: string[];\n}\n\n/** One query result row: the path (chroot-relative), the selected frontmatter (or\n * all of it), and the entry's headings when the index carries them. */\nexport interface MetadataQueryRow {\n path: string;\n meta: Metadata;\n headings?: HeadingSummary[];\n}\n\n/** Canonical, catalog-shaped descriptor for the tool — the same definition every\n * consumer reads (the app's loop, the MCP bridge, a future external executor), so\n * there is exactly one `metadata:query` in the world. */\nexport const METADATA_QUERY_TOOL_DESCRIPTOR = {\n name: METADATA_QUERY_TOOL_NAME,\n description:\n 'Query the MDX corpus index: paths, frontmatter, and headings of entries. ' +\n 'Declarative filters only (no expressions). Returns index rows — never file bodies.',\n paramsSchema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n pathGlob: {\n type: 'string',\n description: 'Glob over the entry path relative to the corpus root (`*` one segment, `**` any).',\n },\n where: {\n type: 'array',\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['key', 'op'],\n properties: {\n key: { type: 'string', description: 'Dotted frontmatter field path.' },\n op: { type: 'string', enum: ['eq', 'contains', 'in', 'exists'] },\n value: {\n description: 'Scalar for `eq`; array of scalars for `in`; unused by `exists`.',\n anyOf: [\n { type: ['string', 'number', 'boolean', 'null'] },\n { type: 'array', items: { type: ['string', 'number', 'boolean', 'null'] } },\n ],\n },\n },\n },\n },\n sortBy: { type: 'string' },\n order: { type: 'string', enum: ['asc', 'desc'] },\n limit: { type: 'integer', minimum: 1, maximum: 500 },\n select: { type: 'array', items: { type: 'string' } },\n },\n },\n} as const;\n\n/** A validation failure — the boundary's only error shape (`invalid-params`, the\n * platform's typed-error vocabulary). */\nexport class MetadataQueryError extends Error {\n code = 'invalid-params' as const;\n}\n\n// ── boundary validation ────────────────────────────────────────────────────────\n\nconst RESERVED_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);\nconst SCALAR = new Set(['string', 'number', 'boolean']);\n\n/** BUNDLE_EMBEDDING §4.1 hygiene: a dotted path is own-property segments only, and\n * no segment may be `__proto__`/`constructor`/`prototype`. Rejecting the segment\n * names (rather than trusting `hasOwn` alone) is belt-and-braces: it keeps the\n * attack string out of the traversal entirely, the same way the projection parser\n * does. */\nfunction validateDottedKey(key: unknown, what: string): string {\n if (typeof key !== 'string' || !key) throw new MetadataQueryError(`${what} must be a non-empty string`);\n if (key.length > 128) throw new MetadataQueryError(`${what} is too long (max 128 chars)`);\n if (/[/\\\\\\u0000-\\u001f]/.test(key))\n throw new MetadataQueryError(`${what} must be a dotted path (no path separators)`);\n for (const seg of key.split('.')) {\n if (!seg) throw new MetadataQueryError(`${what} has an empty segment: ${JSON.stringify(key)}`);\n if (RESERVED_SEGMENTS.has(seg)) throw new MetadataQueryError(`${what} contains a reserved segment: ${seg}`);\n }\n return key;\n}\n\nfunction validateInput(raw: unknown): MetadataQueryInput {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw new MetadataQueryError('input must be an object');\n }\n const src = raw as Record<string, unknown>;\n for (const k of Object.keys(src)) {\n if (!['pathGlob', 'where', 'sortBy', 'order', 'limit', 'select'].includes(k)) {\n throw new MetadataQueryError(`unknown argument: ${k}`);\n }\n }\n const out: MetadataQueryInput = {};\n if (src.pathGlob !== undefined) {\n if (typeof src.pathGlob !== 'string' || !src.pathGlob || src.pathGlob.length > 256) {\n throw new MetadataQueryError('pathGlob must be a non-empty string (≤256 chars)');\n }\n if (/[\\u0000\\n\\r]/.test(src.pathGlob)) throw new MetadataQueryError('pathGlob contains control characters');\n out.pathGlob = src.pathGlob;\n }\n if (src.where !== undefined) {\n if (!Array.isArray(src.where) || src.where.length > 32) {\n throw new MetadataQueryError('where must be an array of at most 32 filters');\n }\n out.where = src.where.map((w, i) => {\n if (typeof w !== 'object' || w === null || Array.isArray(w)) {\n throw new MetadataQueryError(`where[${i}] must be an object`);\n }\n const { key, op, value } = w as Record<string, unknown>;\n validateDottedKey(key, `where[${i}].key`);\n if (op !== 'eq' && op !== 'contains' && op !== 'in' && op !== 'exists') {\n throw new MetadataQueryError(`where[${i}].op must be eq|contains|in|exists`);\n }\n if (value !== undefined) {\n if (typeof value === 'function') {\n throw new MetadataQueryError(`where[${i}].value must be data, not a function`);\n }\n if (op === 'in') {\n if (!Array.isArray(value) || value.length > 100 || value.some((v) => !SCALAR.has(typeof v) && v !== null)) {\n throw new MetadataQueryError(`where[${i}].value for op=in must be an array of scalars`);\n }\n } else if (!SCALAR.has(typeof value) && value !== null) {\n if (typeof value === 'object')\n throw new MetadataQueryError(`where[${i}].value for op=${op} must be a scalar`);\n throw new MetadataQueryError(`where[${i}].value for op=${op} must be a scalar`);\n }\n } else if (op !== 'exists') {\n throw new MetadataQueryError(`where[${i}].value is required for op=${op}`);\n }\n return { key: key as string, op, ...(value !== undefined ? { value } : {}) };\n });\n }\n if (src.sortBy !== undefined) out.sortBy = validateDottedKey(src.sortBy, 'sortBy');\n if (src.order !== undefined) {\n if (src.order !== 'asc' && src.order !== 'desc') throw new MetadataQueryError('order must be asc|desc');\n out.order = src.order;\n }\n if (src.limit !== undefined) {\n if (typeof src.limit !== 'number' || !Number.isInteger(src.limit) || src.limit < 1 || src.limit > 500) {\n throw new MetadataQueryError('limit must be an integer 1–500');\n }\n out.limit = src.limit;\n }\n if (src.select !== undefined) {\n if (!Array.isArray(src.select) || src.select.length > 64) {\n throw new MetadataQueryError('select must be an array of at most 64 field paths');\n }\n out.select = src.select.map((k, i) => validateDottedKey(k, `select[${i}]`));\n }\n return out;\n}\n\n// ── evaluation (pure) ──────────────────────────────────────────────────────────\n\n/** Resolve a validated dotted key over a frontmatter record by own properties only.\n * `undefined` at any hop means \"absent\" (never a match, never a prototype value). */\nfunction resolveOwn(meta: Metadata, key: string): unknown {\n let cur: unknown = meta;\n for (const seg of key.split('.')) {\n if (typeof cur !== 'object' || cur === null) return undefined;\n cur = Object.prototype.hasOwnProperty.call(cur, seg) ? (cur as Record<string, unknown>)[seg] : undefined;\n if (cur === undefined) return undefined;\n }\n return cur;\n}\n\n/** `*` within a segment, `**` across segments — the whole vocabulary. Compiled once\n * per query; anchored so `a/b` never matches `x/a/b`. */\nexport function globToRegExp(glob: string): RegExp {\n const esc = glob.replace(/[.+^${}()|[\\]\\\\?]/g, '\\\\$&');\n const re = esc\n .replace(/\\*\\*/g, '\\u0000')\n .replace(/\\*/g, '[^/]*')\n .replace(/\\u0000/g, '.*');\n return new RegExp(`^${re}$`);\n}\n\nfunction matches(row: Metadata, w: { key: string; op: string; value?: unknown }): boolean {\n const v = resolveOwn(row, w.key);\n switch (w.op) {\n case 'exists':\n return v !== undefined;\n case 'eq':\n return v === w.value || (v === null && w.value === null);\n case 'contains': {\n const needle = w.value;\n if (typeof needle !== 'string') return false;\n if (typeof v === 'string') return v.includes(needle);\n if (Array.isArray(v)) return v.some((x) => typeof x === 'string' && x.includes(needle));\n return false;\n }\n case 'in': {\n if (!Array.isArray(w.value)) return false;\n return w.value.some((needle) => v === needle);\n }\n default:\n return false;\n }\n}\n\nfunction compare(a: unknown, b: unknown): number {\n // Scalars only (the boundary guarantees it for `value`; sortBy paths resolve to\n // whatever frontmatter carries — non-scalars compare as their string form, last).\n const rank = (v: unknown): number => (v === undefined ? 0 : 1);\n if (rank(a) !== rank(b)) return rank(a) - rank(b);\n if (a === undefined) return 0;\n const as = typeof a === 'number' && typeof b === 'number' ? a : String(a);\n const bs = typeof a === 'number' && typeof b === 'number' ? b : String(b);\n return as < bs ? -1 : as > bs ? 1 : 0;\n}\n\n/** Normalize a chroot prefix to `starts-with` form: absolute, trailing slash. */\nfunction normalizeChroot(chroot: string): string {\n const c = chroot.startsWith('/') ? chroot : `/${chroot}`;\n return c.endsWith('/') ? c : `${c}/`;\n}\n\n/**\n * Run a metadata query over an index — PURE (G-GA-4: it cannot read a file body).\n *\n * @param index the in-scope metadata store (`useAllMetadata()` / a scan result),\n * keyed by absolute path.\n * @param chroot the corpus root rows are confined to (G-GA-11): the SAME root the\n * app's `read_entry` chroots to, so the two tools describe one corpus.\n * @param raw the model-supplied arguments, validated here at the boundary.\n * @param filter optional app row-policy (e.g. excluding `_`-prefixed structural\n * files) applied after the chroot filter.\n * @throws {MetadataQueryError} (`code: 'invalid-params'`) on any non-declarative or\n * proto-polluting argument.\n */\nexport function runMetadataQuery(\n index: FilesMetadata,\n chroot: string,\n raw: unknown,\n filter?: (absPath: string) => boolean,\n): MetadataQueryRow[] {\n const input = validateInput(raw);\n const root = normalizeChroot(chroot);\n const globRe = input.pathGlob !== undefined ? globToRegExp(input.pathGlob) : null;\n\n const rows: Array<{ rel: string; meta: Metadata; headings?: HeadingSummary[] }> = [];\n for (const [abs, rowMeta] of Object.entries(index)) {\n if (!abs.startsWith(root)) continue; // G-GA-11 — the chroot is the corpus\n if (filter && !filter(abs)) continue;\n const rel = abs.slice(root.length);\n if (globRe && !globRe.test(rel)) continue;\n const { headings, ...frontmatter } = rowMeta as Metadata & { headings?: HeadingSummary[] };\n const meta = frontmatter as Metadata;\n if (input.where && !input.where.every((w) => matches(meta, w))) continue;\n rows.push({ rel, meta, headings: Array.isArray(headings) ? headings : undefined });\n }\n\n if (input.sortBy) {\n const dir = input.order === 'desc' ? -1 : 1;\n rows.sort(\n (x, y) => dir * compare(resolveOwn(x.meta, input.sortBy as string), resolveOwn(y.meta, input.sortBy as string)),\n );\n }\n\n const limit = input.limit ?? 100;\n const sliced = rows.slice(0, limit);\n return sliced.map(({ rel, meta, headings }) => {\n let out: Metadata = meta;\n if (input.select) {\n out = {};\n for (const key of input.select) {\n const v = resolveOwn(meta, key);\n if (v !== undefined) out[key] = v as never;\n }\n }\n return { path: rel, meta: out, ...(headings ? { headings } : {}) };\n });\n}\n\n/** The `headings` index field rides the row itself; `runMetadataQuery` hoists it out\n * of `meta`. Exported so scans (which BUILD rows) and queries (which read them) agree\n * on the key. */\nexport const METADATA_HEADINGS_KEY = 'headings';\n\n/** Execute the tool for an agent loop: validate, query, and return the fenced result\n * (index rows are corpus-derived bytes — R-GA-7 fences them where they enter the\n * loop). Errors come back as `isError` results, never thrown, so the model adapts. */\nexport function executeMetadataQuery(\n index: FilesMetadata,\n chroot: string,\n raw: unknown,\n filter?: (absPath: string) => boolean,\n): { content: string; isError?: boolean } {\n try {\n const rows = runMetadataQuery(index, chroot, raw, filter);\n return { content: fenceUntrusted('tool-result: metadata rows', JSON.stringify(rows, null, 2)) };\n } catch (e) {\n const code = (e as { code?: string })?.code ?? 'invalid-params';\n return { content: `${code}: ${(e as Error).message}`, isError: true };\n }\n}\n\n/** A loop-ready tool over an index the app supplies per call (the metadata store can\n * be rescanned; the executor reads through the getter, so it never goes stale). */\nexport function createMetadataQueryTool(options: {\n /** Absolute corpus root — rows outside it are invisible (G-GA-11). */\n chroot: string;\n /** The index to query (e.g. `useAllMetadata()` at call time, or a scan result). */\n getIndex: () => FilesMetadata;\n /** App row-policy after the chroot filter (e.g. no `_`-prefixed files). */\n filter?: (absPath: string) => boolean;\n}): {\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n execute: (raw: unknown) => { content: string; isError?: boolean };\n} {\n const d = METADATA_QUERY_TOOL_DESCRIPTOR;\n return {\n name: d.name,\n description: d.description,\n inputSchema: d.paramsSchema as Record<string, unknown>,\n execute: (raw: unknown) => executeMetadataQuery(options.getIndex(), options.chroot, raw, options.filter),\n };\n}\n"],"mappings":"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","names":[]}
|