@tangle-network/agent-app 0.60.207 → 0.60.209

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.
Files changed (46) hide show
  1. package/dist/assistant/index.js +1 -1
  2. package/dist/chat-routes/index.js +6 -2
  3. package/dist/chat-routes/index.js.map +1 -1
  4. package/dist/{chunk-65SDSB6D.js → chunk-7LMHBLFH.js} +19 -2
  5. package/dist/chunk-7LMHBLFH.js.map +1 -0
  6. package/dist/{chunk-JDEGS53O.js → chunk-LRW36WCF.js} +25 -2
  7. package/dist/chunk-LRW36WCF.js.map +1 -0
  8. package/dist/{chunk-2CBRCREL.js → chunk-R75P63W5.js} +2 -2
  9. package/dist/intakes/drizzle.js +114 -6
  10. package/dist/intakes/drizzle.js.map +1 -1
  11. package/dist/intakes/index.d.ts +2 -6
  12. package/dist/intakes/index.js +6 -8
  13. package/dist/intakes/index.js.map +1 -1
  14. package/dist/intakes/model.d.ts +1 -2
  15. package/dist/public-consultation/index.d.ts +2 -2
  16. package/dist/runtime/index.js +2 -2
  17. package/dist/runtime/openai-stream.d.ts +7 -0
  18. package/dist/runtime/router-turn.d.ts +14 -1
  19. package/dist/runtime/router-turn.js +1 -1
  20. package/dist/stories/panels/fixtures.d.ts +3 -29
  21. package/dist/web-react/index.js +3 -3
  22. package/package.json +1 -16
  23. package/dist/IntakeInterview-OKT6DCBN.js +0 -8
  24. package/dist/IntakeInterview-OKT6DCBN.js.map +0 -1
  25. package/dist/chunk-65SDSB6D.js.map +0 -1
  26. package/dist/chunk-I2CA5N7V.js +0 -254
  27. package/dist/chunk-I2CA5N7V.js.map +0 -1
  28. package/dist/chunk-JDEGS53O.js.map +0 -1
  29. package/dist/chunk-VWAEWOWK.js +0 -124
  30. package/dist/chunk-VWAEWOWK.js.map +0 -1
  31. package/dist/chunk-YZ6KQULN.js +0 -30
  32. package/dist/chunk-YZ6KQULN.js.map +0 -1
  33. package/dist/intakes/api.d.ts +0 -54
  34. package/dist/intakes/api.js +0 -63
  35. package/dist/intakes/api.js.map +0 -1
  36. package/dist/intakes-react/components/BrandMark.d.ts +0 -10
  37. package/dist/intakes-react/components/IntakeInterview.d.ts +0 -15
  38. package/dist/intakes-react/components/index.d.ts +0 -1
  39. package/dist/intakes-react/contracts.d.ts +0 -42
  40. package/dist/intakes-react/index.d.ts +0 -14
  41. package/dist/intakes-react/index.js +0 -8
  42. package/dist/intakes-react/index.js.map +0 -1
  43. package/dist/intakes-react/lazy.d.ts +0 -10
  44. package/dist/intakes-react/lazy.js +0 -9
  45. package/dist/intakes-react/lazy.js.map +0 -1
  46. /package/dist/{chunk-2CBRCREL.js.map → chunk-R75P63W5.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/runtime/loop.ts","../src/runtime/openai-stream.ts","../src/runtime/turn-knowledge.ts","../src/runtime/reply-language.ts","../src/runtime/router-turn.ts","../src/runtime/turn-continuation.ts"],"sourcesContent":["/**\n * The bounded agent tool-loop — owned by `@tangle-network/agent-runtime`.\n *\n * A model turn may emit tool calls (integration-hub actions, the app tools from\n * `../tools`, delegation). The loop streams a turn, collects the executable tool\n * calls, dispatches each, appends the results to history in OpenAI\n * function-calling shape, and re-runs so the model reads them — bounded by\n * `maxToolTurns`, a wall-clock `deadlineMs`, and a `maxCostUsd` budget.\n *\n * The history shape is the OpenAI function-calling contract: the assistant turn\n * that emitted tool calls is preserved as an `assistant` message carrying its\n * `tool_calls` array, and each result is its own `{ role: 'tool', tool_call_id,\n * content }` message keyed to the call. A strict model (Claude, and any\n * OpenAI-compatible provider that validates tool history) needs this to read its\n * own tool use back; folding results into a `user` message makes such models\n * re-issue the same call in a loop.\n *\n * The loop is substrate-owned (`runToolLoop` / `streamToolLoop`); the app\n * supplies `streamTurn` (wrapping its model endpoint) and `executeToolCall`\n * (routing to its integration + app-tool executors). The app-facing names below\n * are 1:1 aliases of the canonical symbols, kept so this package's consumers and\n * the in-package `createAgentRuntime` read against a single, stable vocabulary.\n *\n * This is the LEAF the runtime barrel and its children both import — keeping the\n * tool-loop vocabulary out of any import cycle. The barrel re-exports it.\n */\n\nexport {\n runToolLoop as runAppToolLoop,\n streamToolLoop as streamAppToolLoop,\n} from '@tangle-network/agent-runtime/tool-loop'\nexport type {\n ToolLoopCall as LoopToolCall,\n ToolLoopAssistantToolCall as LoopAssistantToolCall,\n ToolLoopMessage as LoopMessage,\n ToolLoopEvent,\n ToolLoopStopReason,\n ToolLoopResult,\n RunToolLoopOptions as AppToolLoopOptions,\n StreamToolLoopOptions as StreamAppToolLoopOptions,\n StreamToolLoopYield as StreamLoopYield,\n} from '@tangle-network/agent-runtime/tool-loop'\n\n/**\n * Events the app's OpenAI-compat stream adapter ({@link toLoopEvents}) yields.\n *\n * This is the app's own `Raw` event type for the streaming loop — the canonical\n * `streamToolLoop<Raw>` is generic over it. It widens the substrate's\n * tool-loop event with `reasoning` (DeepSeek/router `reasoning_content` /\n * `thinking` deltas, rendered as thinking sections) and `usage` (per-message\n * token accounting) — neither belongs in the substrate's loop contract, so they\n * stay here. The adapter maps each into the `streamTurn` seam; `text` and\n * `tool_call` drive the loop, `reasoning` / `usage` pass through to the UI.\n */\nexport type LoopEvent =\n | { type: 'text'; text: string }\n | { type: 'reasoning'; text: string }\n /** The model has begun a tool call: its name is known, its arguments are\n * still streaming. Emitted once per call, before its `tool_call`. Lets a\n * caller start slow work the call will need (resuming a sandbox box for\n * `sandbox_run`) while the arguments arrive. Consumers that do not care\n * ignore it: it never drives the loop. */\n | { type: 'tool_call_start'; toolCallId?: string; toolName: string }\n | {\n type: 'tool_call'\n call: import('@tangle-network/agent-runtime/tool-loop').ToolLoopCall\n }\n | { type: 'usage'; usage: { promptTokens: number; completionTokens: number } }\n | { type: 'other'; event: unknown }\n","/**\n * OpenAI-compatible stream → `LoopEvent` adapter, for NON-sandbox copilots.\n *\n * `streamAppToolLoop` takes a `streamTurn` seam that yields `LoopEvent`s. A\n * sandboxed agent produces those from its container; a browser/edge copilot\n * instead calls a model directly. The Tangle Router, the tcloud SDK, and most\n * providers all speak the OpenAI Chat Completions streaming shape — so the ONE\n * reusable piece is assembling that stream (content deltas + FRAGMENTED\n * tool-call deltas) into `LoopEvent`s. That assembly is the boilerplate every\n * copilot would re-write (and get wrong — OpenAI streams tool-call arguments in\n * pieces across chunks).\n *\n * This does NOT implement an HTTP client beyond a minimal `fetch` + SSE reader\n * (browser/edge/Node-safe, zero deps). For richer transport use the tcloud SDK\n * or the Vercel AI SDK and pipe their stream through {@link toLoopEvents}.\n */\nimport type { LoopEvent, LoopMessage, LoopToolCall } from './loop'\nimport { normalizeModelId } from './model-catalog'\n\n/** Minimal OpenAI Chat Completions streaming chunk (structural — no `openai` dep). */\nexport interface OpenAIStreamChunk {\n /** The model that produced this chunk. The Tangle Router reports the DATED\n * upstream id here (`gpt-5-2025-08-07`) whether or not it substituted, so\n * this is a served-model source only after id folding — see\n * {@link OpenAICompatServedModel}. */\n model?: string\n choices?: Array<{\n delta?: {\n content?: string | null\n /** Reasoning deltas — DeepSeek/router use `reasoning_content`; some proxies use `thinking`. */\n reasoning_content?: string | null\n thinking?: string | null\n tool_calls?: Array<{\n index: number\n id?: string\n function?: { name?: string; arguments?: string }\n /** Provider-specific call data the next request must echo back. Gemini 3\n * sends `{ google: { thought_signature } }` here and answers HTTP 400\n * when a later request's assistant `tool_calls` entry lacks it. */\n extra_content?: unknown\n }>\n }\n finish_reason?: string | null\n }>\n /** Token accounting (requires `stream_options.include_usage`). Cumulative for\n * the call: the Tangle Router can send it on more than one chunk. */\n usage?: {\n prompt_tokens?: number\n completion_tokens?: number\n } | null\n}\n\ninterface PartialToolCall {\n id?: string\n name: string\n args: string\n extraContent?: unknown\n announced: boolean\n}\n\n/**\n * Map an OpenAI-compat streaming chunk iterator to `LoopEvent`s: each content\n * delta → a `text` event; tool-call deltas are accumulated by index across\n * chunks and emitted as one complete `tool_call` event when the stream finishes\n * (arguments JSON-parsed; an empty/garbled args string yields `{}` rather than\n * throwing). Works for the Tangle Router, tcloud, or any OpenAI-compat source.\n *\n * A call's `extra_content` is kept as `providerFields.extra_content`, which the\n * tool loop echoes on the call's assistant `tool_calls` entry in the next request.\n * Usage is cumulative per call and may arrive on several chunks, so it is\n * yielded once, the last value, when the stream ends.\n *\n * Each call also yields one `tool_call_start` as soon as its name arrives,\n * while its arguments are still streaming, so a caller can start work the call\n * will need. Providers send the whole name in the call's first delta.\n */\nexport async function* toLoopEvents(chunks: AsyncIterable<OpenAIStreamChunk>): AsyncIterable<LoopEvent> {\n const calls = new Map<number, PartialToolCall>()\n let usage: { promptTokens: number; completionTokens: number } | undefined\n for await (const chunk of chunks) {\n // Usage can ride a chunk with an empty choices array — read it before the\n // choice guard. Each report is cumulative, so the last one is the call's.\n if (chunk.usage?.prompt_tokens != null || chunk.usage?.completion_tokens != null) {\n usage = {\n promptTokens: chunk.usage.prompt_tokens ?? 0,\n completionTokens: chunk.usage.completion_tokens ?? 0,\n }\n }\n const choice = chunk.choices?.[0]\n if (!choice) continue\n const content = choice.delta?.content\n if (content) yield { type: 'text', text: content }\n const reasoning = choice.delta?.reasoning_content ?? choice.delta?.thinking\n if (reasoning) yield { type: 'reasoning', text: reasoning }\n for (const tc of choice.delta?.tool_calls ?? []) {\n const cur = calls.get(tc.index) ?? { name: '', args: '', announced: false }\n if (tc.id) cur.id = tc.id\n if (tc.function?.name) cur.name += tc.function.name\n if (tc.function?.arguments) cur.args += tc.function.arguments\n if (tc.extra_content !== undefined) cur.extraContent = tc.extra_content\n calls.set(tc.index, cur)\n if (!cur.announced && cur.name) {\n cur.announced = true\n yield { type: 'tool_call_start', ...(cur.id ? { toolCallId: cur.id } : {}), toolName: cur.name }\n }\n }\n }\n if (usage) yield { type: 'usage', usage }\n for (const [, c] of [...calls.entries()].sort((a, b) => a[0] - b[0])) {\n if (!c.name) continue\n yield {\n type: 'tool_call',\n call: {\n toolCallId: c.id,\n toolName: c.name,\n args: safeParse(c.args),\n ...(c.extraContent !== undefined ? { providerFields: { extra_content: c.extraContent } } : {}),\n } satisfies LoopToolCall,\n }\n }\n}\n\nfunction safeParse(s: string): Record<string, unknown> {\n if (!s.trim()) return {}\n try {\n const v = JSON.parse(s)\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {}\n } catch {\n return {}\n }\n}\n\n/**\n * Which model actually served one direct-router turn.\n *\n * The router substitutes models on purpose — a quota-walled primary comes back\n * `200` answered by a different model — and says so in response headers. This\n * lane used to drop the whole `Response` after taking `.body`, so a turn\n * requested as `claude-sonnet-4-6` and answered by `openai/gpt-5` was recorded\n * by its caller as Claude: per-model quality scoring blamed the wrong model and\n * cost used the wrong price basis.\n *\n * Map this onto the shell's existing attribution contract rather than inventing\n * a second channel — `ChatTurnRouteProducer.modelAttribution()` (`/chat-routes`):\n *\n * modelAttribution: () => ({ requestedModel, servedModel, echoReceived: true })\n *\n * Leave that contract's `servedSource` unset: its union is sandbox\n * profile-resolution vocabulary with no router analogue.\n */\nexport interface OpenAICompatServedModel {\n /** The model id this turn asked for (`OpenAICompatStreamTurnOptions.model`). */\n requestedModel: string\n /** The model the router/provider reports having actually served. */\n servedModel: string\n /** Where `servedModel` was read from. The header is the router's own\n * substitution signal; the body is the backstop that survives CORS. */\n source: 'router_header' | 'response_body'\n /** True when served differs from requested after id folding. Folded, not\n * compared raw: the body reports a dated id on EVERY turn, so `!==` would\n * claim a substitution every time. */\n substituted: boolean\n /** `x-tangle-failover` `trigger=` — why the router swapped. Absent when the\n * router did not inject the substitute (a caller-supplied fallback chain\n * sets the served-model header without the failover one). */\n trigger?: string\n /** `x-tangle-failover` `degraded=`. */\n degraded?: boolean\n}\n\n/** Parse `from=…; to=…; trigger=…; degraded=…` down to the fields we report.\n * Absent/garbled segments are simply omitted — attribution is best-effort and\n * must never fail a turn that is otherwise streaming fine. */\nfunction parseFailoverHeader(raw: string | null): { trigger?: string; degraded?: boolean } {\n if (!raw) return {}\n const fields = new Map<string, string>()\n for (const segment of raw.split(';')) {\n const eq = segment.indexOf('=')\n if (eq === -1) continue\n fields.set(segment.slice(0, eq).trim().toLowerCase(), segment.slice(eq + 1).trim())\n }\n const trigger = fields.get('trigger')\n const degraded = fields.get('degraded')\n return {\n ...(trigger ? { trigger } : {}),\n ...(degraded != null ? { degraded: degraded === 'true' } : {}),\n }\n}\n\n/** Define options for configuring an OpenAI-compatible streaming chat turn including API details and tools */\nexport interface OpenAICompatStreamTurnOptions {\n /** OpenAI-compat base URL (e.g. the Tangle Router `https://router.tangle.tools/v1`). */\n baseUrl: string\n apiKey: string\n model: string\n /** OpenAI tool definitions — pass `buildAppToolOpenAITools(taxonomy)` so the\n * model can call the app tools. Omit for a tool-free copilot. */\n tools?: unknown[]\n temperature?: number\n fetchImpl?: typeof fetch\n /** Extra body fields (e.g. `max_tokens`). */\n extraBody?: Record<string, unknown>\n /** Extra request headers, e.g. a trace id the router records with the generation. */\n headers?: Record<string, string>\n /**\n * Called at most ONCE per turn, as soon as the serving model is\n * determinable, with what actually answered. Never called when neither the\n * header nor the body names a model — silence means \"learned nothing\", not\n * \"nothing was substituted\".\n *\n * `streamTurn` runs once per TOOL turn, so a multi-turn `runAppToolLoop`\n * fires this once per turn; take the last for row attribution.\n */\n onServedModel?: (served: OpenAICompatServedModel) => void\n}\n\n/**\n * Build a `streamTurn` that calls an OpenAI-compatible `/chat/completions`\n * endpoint (Tangle Router / tcloud / any compat provider) with `stream: true`\n * and yields `LoopEvent`s via {@link toLoopEvents}. Browser/edge/Node-safe —\n * just `fetch` + an SSE reader. Drop straight into `streamAppToolLoop`:\n *\n * const cfg = resolveTangleModelConfig() // or { baseUrl, apiKey, model }\n * streamAppToolLoop({ streamTurn: createOpenAICompatStreamTurn({ ...cfg, tools }), executeToolCall, ... })\n */\nexport function createOpenAICompatStreamTurn(\n opts: OpenAICompatStreamTurnOptions,\n): (messages: LoopMessage[]) => AsyncIterable<LoopEvent> {\n const base = opts.baseUrl.replace(/\\/+$/, '')\n const doFetch = opts.fetchImpl ?? fetch\n return (messages) =>\n toLoopEvents(\n streamChatCompletions(doFetch, `${base}/chat/completions`, opts.apiKey, {\n model: opts.model,\n messages,\n stream: true,\n stream_options: { include_usage: true },\n ...(opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {}),\n ...(opts.temperature != null ? { temperature: opts.temperature } : {}),\n ...opts.extraBody,\n }, opts.onServedModel ? { requestedModel: opts.model, report: opts.onServedModel } : undefined, opts.headers),\n )\n}\n\n/**\n * The buffered sibling of {@link createOpenAICompatStreamTurn}: one\n * `/chat/completions` request without `stream`, answered by one JSON\n * completion, yielded as the same `LoopEvent`s (the same tool-call assembly and\n * served-model attribution). For an endpoint or ledger that expects one buffered\n * completion per model call.\n */\nexport function createOpenAICompatCompletionTurn(\n opts: OpenAICompatStreamTurnOptions,\n): (messages: LoopMessage[]) => AsyncIterable<LoopEvent> {\n const base = opts.baseUrl.replace(/\\/+$/, '')\n const doFetch = opts.fetchImpl ?? fetch\n return (messages) =>\n toLoopEvents(\n completionAsChunk(doFetch, `${base}/chat/completions`, opts.apiKey, {\n model: opts.model,\n messages,\n ...(opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {}),\n ...(opts.temperature != null ? { temperature: opts.temperature } : {}),\n ...opts.extraBody,\n }, opts.onServedModel ? { requestedModel: opts.model, report: opts.onServedModel } : undefined, opts.headers),\n )\n}\n\n/** One buffered OpenAI-compat completion (structural — no `openai` dep). */\ninterface OpenAICompletion {\n model?: string\n choices?: Array<{\n message?: {\n content?: string | null\n reasoning_content?: string | null\n tool_calls?: Array<{ id?: string; function?: { name?: string; arguments?: string }; extra_content?: unknown }>\n }\n finish_reason?: string | null\n }>\n usage?: { prompt_tokens?: number; completion_tokens?: number } | null\n}\n\nasync function* completionAsChunk(\n doFetch: typeof fetch,\n url: string,\n apiKey: string,\n body: Record<string, unknown>,\n attribution?: { requestedModel: string; report: (served: OpenAICompatServedModel) => void },\n headers?: Record<string, string>,\n): AsyncIterable<OpenAIStreamChunk> {\n const res = await doFetch(url, {\n method: 'POST',\n headers: { ...headers, Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', Accept: 'application/json' },\n body: JSON.stringify(body),\n })\n if (!res.ok) throw await httpFailure(res, 'completion')\n const completion = await res.json() as OpenAICompletion\n if (attribution) {\n const served = servedFromHeader(res, attribution.requestedModel)\n ?? (completion.model ? servedFromBody(completion.model, attribution.requestedModel) : undefined)\n if (served) attribution.report(served)\n }\n const choice = completion.choices?.[0]\n const message = choice?.message\n // The whole completion as one chunk, so `toLoopEvents` assembles it exactly as a stream.\n yield {\n ...(completion.model ? { model: completion.model } : {}),\n choices: [{\n delta: {\n content: message?.content ?? null,\n reasoning_content: message?.reasoning_content ?? null,\n tool_calls: (message?.tool_calls ?? []).map((call, index) => ({\n index,\n ...(call.id ? { id: call.id } : {}),\n function: { name: call.function?.name ?? '', arguments: call.function?.arguments ?? '' },\n ...(call.extra_content !== undefined ? { extra_content: call.extra_content } : {}),\n })),\n },\n finish_reason: choice?.finish_reason ?? null,\n }],\n ...(completion.usage ? { usage: completion.usage } : {}),\n }\n}\n\nasync function httpFailure(res: Response, kind: 'stream' | 'completion' | 'responses'): Promise<Error> {\n const text = res.body ? await res.text().catch(() => '') : ''\n const error = new Error(`OpenAI-compat ${kind} failed (HTTP ${res.status})${text ? `: ${text.slice(0, 1000)}` : ''}`)\n // Stamp the NUMERIC status. `isUpstreamUnavailable` (`/model-resolution`)\n // reads a numeric field first and prose only as a backstop, and a\n // Cloudflare EDGE 502 gives it nothing else to read: `content-type:\n // text/plain`, a body of `error code: 502`, and none of the router's own\n // `x-tangle-*` headers, because the origin never ran. Carrying the status\n // as data keeps THIS path — the browser/edge copilot lane, which calls the\n // router directly — out of the string-matching business entirely.\n Object.assign(error, { status: res.status })\n return error\n}\n\nfunction servedFromHeader(res: Response, requestedModel: string): OpenAICompatServedModel | undefined {\n const servedHeader = res.headers.get('x-tangle-served-model')\n if (!servedHeader) return undefined\n return {\n requestedModel,\n servedModel: servedHeader,\n source: 'router_header',\n substituted: normalizeModelId(servedHeader) !== normalizeModelId(requestedModel),\n ...parseFailoverHeader(res.headers.get('x-tangle-failover')),\n }\n}\n\nfunction servedFromBody(model: string, requestedModel: string): OpenAICompatServedModel {\n return {\n requestedModel,\n servedModel: model,\n source: 'response_body',\n substituted: normalizeModelId(model) !== normalizeModelId(requestedModel),\n }\n}\n\n/** Stream + parse an OpenAI-compat SSE response into chunks. Tolerates `data:`\n * framing, multi-line buffers, and the terminal `[DONE]`. */\nasync function* streamChatCompletions(\n doFetch: typeof fetch,\n url: string,\n apiKey: string,\n body: Record<string, unknown>,\n attribution?: { requestedModel: string; report: (served: OpenAICompatServedModel) => void },\n headers?: Record<string, string>,\n): AsyncIterable<OpenAIStreamChunk> {\n const res = await doFetch(url, {\n method: 'POST',\n headers: { ...headers, Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', Accept: 'text/event-stream' },\n body: JSON.stringify(body),\n })\n if (!res.ok || !res.body) throw await httpFailure(res, 'stream')\n // Served-model attribution, fired at most once, as early as it is knowable.\n //\n // The header comes first because it is the router's OWN substitution signal:\n // `X-Tangle-Served-Model` is set only when the served model differs from the\n // requested one, so its presence is authoritative and it arrives before the\n // first byte of content. `X-Tangle-Failover` rides along only when the router\n // itself picked the substitute (a caller-supplied fallback chain sets the\n // former without the latter), which is why `trigger`/`degraded` are optional.\n //\n // The body is the backstop for when the header cannot reach us. In a BROWSER,\n // headers are invisible to JS unless the server lists them in\n // `Access-Control-Expose-Headers` — tangle-router#324 added both, but that\n // only helps once it is DEPLOYED, and any other OpenAI-compat endpoint\n // pointed at this client exposes nothing. The chunk `model` field is not\n // subject to CORS, so it keeps the browser lane attributable regardless.\n let reported = false\n const report = (served: OpenAICompatServedModel): void => {\n if (reported || !attribution) return\n reported = true\n attribution.report(served)\n }\n if (attribution) {\n const served = servedFromHeader(res, attribution.requestedModel)\n if (served) report(served)\n }\n const reader = res.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('data:')) continue\n const data = trimmed.slice(5).trim()\n if (data === '[DONE]') return\n let chunk: OpenAIStreamChunk\n try {\n chunk = JSON.parse(data) as OpenAIStreamChunk\n } catch {\n continue /* skip a partial/garbled SSE frame */\n }\n // Body fallback: only when the header said nothing. Fold both ids before\n // comparing — the router reports the dated upstream id (`gpt-5-2025-08-07`)\n // for a request of `openai/gpt-5` even with NO substitution, so a raw\n // `!==` would report a swap on literally every turn.\n if (attribution && !reported && chunk.model) report(servedFromBody(chunk.model, attribution.requestedModel))\n yield chunk\n }\n }\n}\n\n/** OpenAI models answer reasoning summaries only on the Responses API; chat completions carry none. */\nexport function isOpenAIResponsesModel(model: string): boolean {\n return /^(?:openai\\/)?(?:gpt-|o\\d|chatgpt-)/i.test(model.trim())\n}\n\n/** The chat-completions history the tool loop keeps, as Responses `instructions` and `input` items. */\nexport function toResponsesInput(messages: LoopMessage[]): { instructions?: string; input: Array<Record<string, unknown>> } {\n const instructions: string[] = []\n const input: Array<Record<string, unknown>> = []\n for (const message of messages) {\n const content = typeof message.content === 'string' ? message.content : ''\n if (message.role === 'system') {\n if (content) instructions.push(content)\n } else if (message.role === 'tool') {\n input.push({ type: 'function_call_output', call_id: message.tool_call_id ?? '', output: content })\n } else if (message.role === 'assistant' && message.tool_calls?.length) {\n if (content.trim()) input.push({ role: 'assistant', content })\n for (const call of message.tool_calls) {\n input.push({ type: 'function_call', call_id: call.id, name: call.function.name, arguments: call.function.arguments })\n }\n } else if (message.role === 'user' || message.role === 'assistant') {\n input.push({ role: message.role, content })\n }\n }\n return { ...(instructions.length ? { instructions: instructions.join('\\n\\n') } : {}), input }\n}\n\n/** Chat-completions tool definitions as Responses function tools. */\nfunction toResponsesTools(tools: unknown[] | undefined): Array<Record<string, unknown>> {\n return (tools ?? []).flatMap((tool) => {\n const fn = (tool as { function?: { name?: string; description?: string; parameters?: unknown } }).function\n return fn?.name ? [{ type: 'function', name: fn.name, ...(fn.description ? { description: fn.description } : {}),\n parameters: fn.parameters ?? { type: 'object', properties: {} } }] : []\n })\n}\n\n/**\n * Build a `streamTurn` on the OpenAI Responses API (`POST /responses`, streamed),\n * for OpenAI models whose reasoning reaches a caller only there, as\n * `response.reasoning_summary_text.delta` events. Each Responses event is\n * mapped onto the chat-completions chunk shape {@link toLoopEvents} reads, so\n * text, reasoning, fragmented function calls, usage and served-model\n * attribution behave exactly as on {@link createOpenAICompatStreamTurn}.\n *\n * `extraBody` keys from the chat path are translated: `max_tokens` and\n * `max_completion_tokens` become `max_output_tokens`, and `reasoning_effort`\n * becomes `reasoning.effort` with `summary: 'auto'`. Stateless: earlier tool\n * calls are replayed as `function_call` / `function_call_output` items.\n */\nexport function createOpenAIResponsesTurn(\n opts: OpenAICompatStreamTurnOptions,\n): (messages: LoopMessage[]) => AsyncIterable<LoopEvent> {\n const base = opts.baseUrl.replace(/\\/+$/, '')\n const doFetch = opts.fetchImpl ?? fetch\n const { max_tokens: maxTokens, max_completion_tokens: maxCompletion, reasoning_effort: effort, ...rest } = opts.extraBody ?? {}\n const maxOutput = maxCompletion ?? maxTokens\n return (messages) => {\n const { instructions, input } = toResponsesInput(messages)\n const tools = toResponsesTools(opts.tools)\n return toLoopEvents(streamResponses(doFetch, `${base}/responses`, opts.apiKey, {\n model: opts.model,\n ...(instructions ? { instructions } : {}),\n input,\n stream: true,\n store: false,\n ...(tools.length ? { tools } : {}),\n ...(opts.temperature != null ? { temperature: opts.temperature } : {}),\n ...(maxOutput !== undefined ? { max_output_tokens: maxOutput } : {}),\n ...(effort !== undefined ? { reasoning: { effort, summary: 'auto' } } : {}),\n ...rest,\n }, opts.onServedModel ? { requestedModel: opts.model, report: opts.onServedModel } : undefined, opts.headers))\n }\n}\n\ninterface ResponsesEvent {\n type?: string\n delta?: string\n output_index?: number\n summary_index?: number\n item?: { type?: string; call_id?: string; name?: string; arguments?: string }\n response?: { model?: string; usage?: { input_tokens?: number; output_tokens?: number }; error?: { code?: string; message?: string } }\n error?: { code?: string; message?: string }\n message?: string\n}\n\n/** Stream a Responses API call as chat-completions chunks. A failed or errored response throws. */\nasync function* streamResponses(\n doFetch: typeof fetch,\n url: string,\n apiKey: string,\n body: Record<string, unknown>,\n attribution?: { requestedModel: string; report: (served: OpenAICompatServedModel) => void },\n headers?: Record<string, string>,\n): AsyncIterable<OpenAIStreamChunk> {\n const res = await doFetch(url, {\n method: 'POST',\n headers: { ...headers, Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', Accept: 'text/event-stream' },\n body: JSON.stringify(body),\n })\n if (!res.ok || !res.body) throw await httpFailure(res, 'responses')\n let reported = false\n const report = (served: OpenAICompatServedModel) => {\n if (reported || !attribution) return\n reported = true\n attribution.report(served)\n }\n if (attribution) {\n const served = servedFromHeader(res, attribution.requestedModel)\n if (served) report(served)\n }\n const reader = res.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n let lastSummary: string | null = null\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('data:')) continue\n const data = trimmed.slice(5).trim()\n if (data === '[DONE]') return\n let event: ResponsesEvent\n try {\n event = JSON.parse(data) as ResponsesEvent\n } catch {\n continue\n }\n switch (event.type) {\n case 'response.output_text.delta':\n if (event.delta) yield { choices: [{ delta: { content: event.delta } }] }\n break\n case 'response.reasoning_summary_text.delta': {\n if (!event.delta) break\n // Separate summary parts the way the model wrote them.\n const part = `${event.output_index ?? 0}:${event.summary_index ?? 0}`\n const lead = lastSummary !== null && lastSummary !== part ? '\\n\\n' : ''\n lastSummary = part\n yield { choices: [{ delta: { reasoning_content: lead + event.delta } }] }\n break\n }\n case 'response.output_item.added':\n if (event.item?.type === 'function_call' && event.item.name) {\n yield { choices: [{ delta: { tool_calls: [{ index: event.output_index ?? 0,\n ...(event.item.call_id ? { id: event.item.call_id } : {}),\n function: { name: event.item.name, arguments: event.item.arguments ?? '' } }] } }] }\n }\n break\n case 'response.function_call_arguments.delta':\n if (event.delta) yield { choices: [{ delta: { tool_calls: [{ index: event.output_index ?? 0, function: { arguments: event.delta } }] } }] }\n break\n case 'response.completed':\n case 'response.incomplete': {\n const model = event.response?.model\n if (attribution && !reported && model) report(servedFromBody(model, attribution.requestedModel))\n const usage = event.response?.usage\n yield { ...(model ? { model } : {}), choices: [],\n ...(usage ? { usage: { prompt_tokens: usage.input_tokens ?? 0, completion_tokens: usage.output_tokens ?? 0 } } : {}) }\n return\n }\n case 'response.failed':\n case 'error': {\n const failure = event.response?.error ?? event.error ?? { message: event.message }\n throw new Error(`OpenAI-compat responses failed: ${(failure?.code ? `${failure.code}: ` : '') + (failure?.message ?? 'no detail')}`.slice(0, 300))\n }\n default:\n break\n }\n }\n }\n}\n","/**\n * Knowledge in every turn: the index of an app's knowledge files, the full\n * text of the small ones, and the rule that keeps answers on them.\n *\n * Measured on Hospitality production (2026-10-11): a Router turn that had to\n * discover files through list/read tools answered 17/17 correctly but needed\n * 3 tool calls (first words 12-17 s), and a turn that skipped the tools\n * answered from model defaults (\"checkout 11:00\" when the file said 12 pm).\n * Putting the text in the prompt removes both failure modes.\n */\n/** The fields read from an agent-runtime workspace-store entry; structural so older runtimes still type-check. */\ninterface KnowledgeEntry { path: string; kind: string; content?: string; bytes?: { size: number } }\n\n/** One knowledge file a turn carries; `text` is null when it is too large or not readable here. */\nexport interface TurnKnowledgeFile {\n path: string\n size: number\n text: string | null\n}\n\nexport interface TurnKnowledgeBudget {\n /** Files at most this many bytes have their text included. Default 8 000. */\n fileBytes?: number\n /** Total text bytes across files. Default 16 000. */\n textBytes?: number\n /** Most files listed. Default 60. */\n maxFiles?: number\n}\n\nconst DEFAULT_FILE_BYTES = 8_000\nconst DEFAULT_TEXT_BYTES = 16_000\nconst DEFAULT_MAX_FILES = 60\n\n/** Apply the byte budget in path order: small files keep their text until the total is spent. */\nexport function budgetTurnKnowledge(files: readonly TurnKnowledgeFile[], budget: TurnKnowledgeBudget = {}): TurnKnowledgeFile[] {\n const fileBytes = budget.fileBytes ?? DEFAULT_FILE_BYTES\n let remaining = budget.textBytes ?? DEFAULT_TEXT_BYTES\n return [...files].sort((a, b) => a.path.localeCompare(b.path)).slice(0, budget.maxFiles ?? DEFAULT_MAX_FILES)\n .map(file => {\n const fits = file.text !== null && file.size <= fileBytes && file.size <= remaining\n if (fits) remaining -= file.size\n return { path: file.path, size: file.size, text: fits ? file.text : null }\n })\n}\n\n/**\n * The prompt lines for a turn's knowledge. `readTool` names the tool that\n * opens a listed file whose text is not shown; without one, such a file is\n * listed by name only.\n */\nexport function turnKnowledgeInstructions(files: readonly TurnKnowledgeFile[], options: { readTool?: string } = {}): string[] {\n const read = options.readTool\n return [\n 'Answer questions about this business or its work (hours, times, policies, codes, passwords, prices, contacts, amenities, procedures) only from the knowledge in these instructions, the knowledge files below, or a tool result. Never give a typical or assumed value, and do not say something is absent unless a source says so.' +\n (read ? ` When a listed file without its text may cover the question, read it with ${read} before answering.` : '') +\n ' When nothing covers it, say you do not have that information and offer to ask the owner.' +\n ' When two sources disagree, give each value with the source that states it and ask the owner which is current; never pick one silently.',\n ...files.map(file => file.text !== null ? `Knowledge file ${file.path}:\\n${file.text}`\n : `Knowledge file ${file.path} (${file.size} bytes, text not shown)${read ? `: read it with ${read} when the question may be covered by it.` : '.'}`),\n // The disagree rule above, read before the files, named a conflict in 10 of 20 bench turns\n // (hospitality, 3 of 5 in production); repeated after the files as a check it named 20 of 20,\n // and 10 of 10 single-file answers stayed unchanged (gemini-3.5-flash-lite, 2026-10-12,\n // hospitality-agent #441).\n ...(files.length > 1 ? ['Before answering from these files, check every file that states the asked fact. When they state different values, give each value with its file name and ask the owner which is current.'] : []),\n ]\n}\n\n/** A workspace store's knowledge files (kind `file` under `prefix`) as turn knowledge, within the budget. */\nexport async function workspaceKnowledgeForTurn(\n store: { list(workspaceId: string, prefix: string): Promise<readonly KnowledgeEntry[]> },\n workspaceId: string,\n options: TurnKnowledgeBudget & { prefix?: string } = {},\n): Promise<TurnKnowledgeFile[]> {\n const entries = await store.list(workspaceId, options.prefix ?? '/files/')\n return budgetTurnKnowledge(entries.filter(entry => entry.kind === 'file').map(entry => ({\n path: entry.path,\n size: entry.content !== undefined ? new TextEncoder().encode(entry.content).byteLength : entry.bytes?.size ?? 0,\n text: entry.content ?? null,\n })), options)\n}\n","/**\n * The language a Router turn replies in, for every agent app: the sender's current\n * message decides, then the conversation's. It names no language on purpose. On\n * 2026-10-12 a hospitality turn whose context said \"saved language for notices is\n * Spanish\" answered 10 of 19 English questions in Spanish; a model follows any\n * language the prompt names. Saved preferences belong to messages the app starts.\n */\nexport function replyLanguageInstruction(): string {\n return 'Reply in the language of the sender\\'s current message, or in the language they ask for in it. ' +\n 'When its language is unclear (a short acknowledgement, a photo, a voice note), continue in the conversation\\'s language. ' +\n 'Change a saved language only when the sender asks for it in this message; a message written in another language is not that request.'\n}\n\n// ISO 639-1. Names come from Intl, in English, Spanish and the language's own name,\n// so a request for any language is recognised without a hand-kept list.\nconst LANGUAGE_CODES = ('aa ab af ak am an ar as av ay az ba be bg bi bm bn bo br bs ca ce ch co cr cs cu cv cy da de dv dz ee el en eo es ' +\n 'et eu fa ff fi fj fo fr fy ga gd gl gn gu gv ha he hi ho hr ht hu hy hz ia id ie ig ii ik io is it iu ja jv ka kg ki kj kk kl km kn ko kr ' +\n 'ks ku kv kw ky la lb lg li ln lo lt lu lv mg mh mi mk ml mn mr ms mt my na nb nd ne ng nl nn no nr nv ny oc oj om or os pa pi pl ps pt qu ' +\n 'rm rn ro ru rw sa sc sd se sg si sk sl sm sn so sq sr ss st su sv sw ta te tg th ti tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa wo ' +\n 'xh yi yo za zh zu').split(' ')\n// Always recognised, whatever the runtime's Intl data: generic words and the common\n// languages in English, Spanish and their own names (plus हिंदी beside हिन्दी).\nconst CORE_NAMES = [\n 'language', 'languages', 'idioma', 'idiomas', 'langue', 'lingua', 'língua', 'sprache',\n 'english', 'inglés', 'ingles', 'spanish', 'español', 'espanol', 'castellano', 'french', 'français', 'francés',\n 'portuguese', 'português', 'portugués', 'german', 'deutsch', 'alemán', 'italian', 'italiano', 'japanese', '日本語',\n 'japonés', 'chinese', '中文', '汉语', 'chino', 'korean', '한국어', 'coreano', 'arabic', 'العربية', 'árabe', 'hindi',\n 'हिन्दी', 'हिंदी', 'russian', 'русский', 'ruso', 'dutch', 'nederlands', 'neerlandés', 'hebrew', 'עברית', 'hebreo',\n 'turkish', 'türkçe', 'turco', 'vietnamese', 'tiếng việt', 'vietnamita', 'catalan', 'català', 'catalán',\n]\n// Language names that are everyday words: \"ya he ido\", \"él cree\", \"que me venda\",\n// \"odia\", \"from afar\", \"ewe\", \"Latin America\", \"shoe polish\".\nconst COMMON_WORDS = new Set(['ido', 'cree', 'venda', 'odia', 'afar', 'ewe', 'latin', 'polish'])\n\nlet patterns: RegExp[] | undefined\nfunction languagePatterns(): RegExp[] {\n if (patterns) return patterns\n const names = new Set(CORE_NAMES)\n for (const locale of ['en', 'es', ...LANGUAGE_CODES]) {\n let display: Intl.DisplayNames\n // A runtime without Intl language data still has CORE_NAMES.\n try { display = new Intl.DisplayNames([locale], { type: 'language' }) } catch { continue }\n for (const code of locale === 'en' || locale === 'es' ? LANGUAGE_CODES : [locale]) {\n const name = display.of(code)?.toLowerCase()\n if (name && name !== code && name.length >= 3 && !COMMON_WORDS.has(name)) names.add(name)\n }\n }\n // Latin-script names need word edges (\"pedido\" must not match Ido); scripts written\n // without spaces, such as 日本語, match anywhere.\n return patterns = [...names].map(name => {\n const escaped = name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n return /^[\\p{Script=Latin}\\s'-]+$/u.test(name) ? new RegExp(`(?<!\\\\p{L})${escaped}(?!\\\\p{L})`, 'iu') : new RegExp(escaped, 'iu')\n })\n}\n\n/**\n * True when the message names a language. A tool that saves a reply-language\n * preference requires it, using its `context.userMessage`, so the model cannot\n * save a language the sender never asked for.\n */\nexport function asksForLanguage(message: string | undefined): boolean {\n return typeof message === 'string' && languagePatterns().some(pattern => pattern.test(message))\n}\n","/**\n * One conversational turn answered by a model through the Tangle Router, with\n * the app's own tools executed in-process — no sandbox in the path.\n *\n * This is the default runtime for conversational turns in an agent app: the\n * product supplies the principal's tools, the profile prompt and the\n * conversation; the engine streams words as the model writes them, runs tool\n * calls through the bounded `streamAppToolLoop`, and returns one outcome.\n * Long, file or code work belongs to the sandbox, reached through a tool the\n * product adds to `tools`.\n *\n * Every failure is an outcome, never a throw. A durable turn owner retries\n * thrown errors, and a retry after a tool ran would repeat that tool's effect\n * (\"never re-run a turn automatically\", docs/agent-app-lessons.md). The only\n * automatic repeat is one model call that failed before any word or tool.\n *\n * Bounded paid lanes run this same engine inside a protected Router model grant\n * (`runProtectedRouterTurn` in `./protected-router-turn`, outside this entry so a\n * Worker bundle never pulls the grant client).\n */\nimport { streamAppToolLoop, type LoopEvent, type LoopMessage, type LoopToolCall, type ToolLoopStopReason } from './loop'\nimport { DEFAULT_TANGLE_ROUTER_BASE_URL } from './model'\nimport { createOpenAICompatCompletionTurn, createOpenAICompatStreamTurn, createOpenAIResponsesTurn, isOpenAIResponsesModel } from './openai-stream'\nimport { turnKnowledgeInstructions, type TurnKnowledgeFile } from './turn-knowledge'\nimport { replyLanguageInstruction } from './reply-language'\nimport { placeExecution, type PlacementRecord, type VacuousAxis } from '@tangle-network/agent-runtime/placement'\nimport { turnTraceId, type TurnSpan, type TurnTrace } from '../turn-trace/recorder'\nimport { nextCallIndex, tracedModelFetch } from '../turn-trace/traced-fetch'\nexport { DURABLE_HOP_BUDGET_MS, turnCheckpointPath, workspaceTurnCheckpoints, type CheckpointWorkspaceStore, type TurnCheckpointStore } from './turn-continuation'\nexport { budgetTurnKnowledge, turnKnowledgeInstructions, workspaceKnowledgeForTurn, type TurnKnowledgeBudget, type TurnKnowledgeFile } from './turn-knowledge'\nexport { asksForLanguage, replyLanguageInstruction } from './reply-language'\n\n/** A tool the turn may call. Structurally an `McpToolDefinition` whose env is already bound. */\nexport interface RouterTurnTool {\n name: string\n description: string\n /** JSON Schema for the arguments object. */\n inputSchema: Record<string, unknown>\n run(args: Record<string, unknown>, context: RouterTurnToolContext): Promise<unknown>\n}\n\n/** What a running tool gets besides its arguments. */\nexport interface RouterTurnToolContext {\n /**\n * Report a typed side event while the tool runs (a graph edit, a file written, a\n * progress step). It streams at once as a `tool_event` and is recorded in the\n * outcome's `toolEvents`. Events sent after the call settles are dropped.\n */\n emit(kind: string, data?: unknown): void\n /** The sender's message this turn answers, so a tool can act only on what was asked (see {@link asksForLanguage}). */\n userMessage: string\n /** The turn's trace, when the turn records one. A tool that waits on a box or a service adds its own child spans under {@link span}. */\n trace?: TurnTrace\n /** This tool call's `tool.call` span; the parent of any span the tool records. */\n span?: TurnSpan\n}\n\n/** One side event a tool reported while it ran. */\nexport interface RouterTurnToolEvent {\n callId: string\n toolName: string\n kind: string\n data: unknown\n at: number\n}\n\n/** What the turn is doing, in the order it happens, for a live view. */\nexport type RouterTurnEvent =\n | { type: 'text'; segment: number; delta: string }\n /** Hidden reasoning the model streamed (`reasoning_content` / `thinking`), for a thinking view. Never part of the answer. */\n | { type: 'reasoning'; delta: string }\n | { type: 'tool'; callId: string; toolName: string; status: 'running'; input: Record<string, unknown>; startedAt: number }\n | { type: 'tool'; callId: string; toolName: string; status: 'completed'; output: unknown; startedAt: number; endedAt: number }\n | { type: 'tool'; callId: string; toolName: string; status: 'error'; error: { code: string; message: string }; startedAt: number; endedAt: number }\n | { type: 'usage'; promptTokens: number; completionTokens: number }\n | ({ type: 'tool_event' } & RouterTurnToolEvent)\n\nexport interface RouterTurnFailure {\n /** `model_http_<status>`, `model_unavailable`, `model_empty`, `turn_<stopReason>`, `turn_aborted`,\n * `turn_tool_limit`, `router_unconfigured` or `placement_fallback_refused`; under a protected grant also the `grant_*` and\n * `settlement_*` codes of `./protected-router-turn`. */\n code: string\n status?: number\n message: string\n /** The model service's own error text, at most 1000 characters (e.g. `key_budget_exhausted`), so an app can\n * tell a spend cap from a missing balance. Never a credential. */\n detail?: string\n}\n\nexport interface RouterTurnOutcome {\n /** Every text segment the model wrote, joined by blank lines. */\n text: string\n /** The model the router reports having served, when it said. */\n servedModel: string | null\n toolCalls: Array<{ callId: string; toolName: string; ok: boolean }>\n /** Token use summed over every model call in the turn, as the router reported it. */\n usage: { promptTokens: number; completionTokens: number; modelCalls: number }\n /** Reasoning the turn streamed (deltas and characters). Zero when the model sent none; absent when no model was called. */\n reasoning?: { deltas: number; chars: number }\n /** Every side event the turn's tools reported, in order (at most 500). */\n toolEvents: RouterTurnToolEvent[]\n failure?: RouterTurnFailure\n /**\n * Set instead of a failure when the turn reached its budget at a clean point\n * (every tool call answered). The turn is not over: persist this and call\n * `runRouterTurn` again with `continueFrom` in a durable process to finish it.\n * `text` holds everything the turn has written so far, across hops.\n */\n continuation?: RouterTurnContinuation\n /** Where the turn was placed, when the caller asked for placement. */\n placement?: PlacementRecord\n}\n\n/** A checkpointed Router turn: the conversation after the system prompt, ready to continue. JSON-safe. */\nexport interface RouterTurnContinuation {\n messages: LoopMessage[]\n /** Hops this turn has already run, including this one. */\n hops: number\n /** Reply text every hop so far wrote; the final hop's outcome `text` includes it. */\n text: string\n}\n\n/**\n * What placement reads for a Router turn: the resolved profile, the tools the\n * turn would hold, and a model the sender chose. Declared data only.\n */\nexport interface RouterTurnPlacementInput {\n profile: object\n toolNames?: readonly string[]\n /** A model other than the profile's needs the sandbox to choose between them. */\n requestedModel?: string\n /** Deny-only permissions grant nothing a Router turn lacks. Default: ['permissions']. */\n vacuousAxes?: readonly VacuousAxis[]\n /** The app inlines the profile's skill text into the prompt. */\n inlineSkills?: boolean\n}\n\n/**\n * Where one conversational turn runs: `inference` or `loop` run here in\n * `runRouterTurn`; `sandbox` means the profile or request needs a computer\n * (a harness, files, MCP servers, skills that are not inlined, another model).\n * Throws `PlacementRefusedError` when no tier can meet every requirement.\n */\nexport function placeRouterTurn(input: RouterTurnPlacementInput): PlacementRecord {\n return placeExecution({\n profile: input.profile,\n requirements: {\n ...(input.toolNames?.length ? { tools: [...input.toolNames] } : {}),\n ...(input.requestedModel ? { model: input.requestedModel } : {}),\n },\n context: { interactive: false, inferenceAvailable: true, loopAvailable: true },\n vacuousAxes: input.vacuousAxes ?? ['permissions'],\n ...(input.inlineSkills ? { inlineSkills: true } : {}),\n })\n}\n\n/** One tool call's result as the model reads it. */\nexport type RouterTurnToolOutcome = { ok: true; result: unknown } | { ok: false; code: string; message: string }\n\nexport interface RouterTurnOptions {\n apiKey: string\n model: string\n /** Defaults to the Tangle Router. */\n baseUrl?: string\n systemPrompt: string\n userMessage: string\n /** Earlier turns. Plain text messages, or OpenAI function-calling history (an assistant message\n * carrying `tool_calls` and the `tool` messages answering them) so the model sees what it did. */\n history?: Array<{ role: 'user' | 'assistant'; content: string } | LoopMessage>\n tools?: RouterTurnTool[]\n /** Wall-clock budget in ms for this hop. Default 90 s. At the budget the outcome carries a\n * `continuation` instead of failing; the model call or tool already running finishes first. */\n budgetMs?: number\n /** Continue a checkpointed turn; `userMessage` and `history` are ignored. */\n continueFrom?: RouterTurnContinuation\n /** Runaway backstop on model turns. Default 12. */\n maxToolTurns?: number\n maxTokens?: number\n /** Sent as `x-tangle-trace-id` so the router's generation joins the app's trace. Characters the Router\n * refuses in that header (the `:` of an execution id) become `_`; see {@link turnTraceId}. Default: the\n * `trace`'s id. */\n traceId?: string\n /**\n * The turn's trace (`@tangle-network/agent-app/turn-trace`). Every model request is recorded as a\n * `model.call` span with the Router's `Server-Timing` split into network and server time, and every tool\n * call as a `tool.call` span that tool-side spans hang from. Without it nothing is recorded.\n */\n trace?: TurnTrace\n fetchImpl?: typeof fetch\n onEvent?: (event: RouterTurnEvent) => void\n /** Map a thrown tool error to what the model reads. Default: an error carrying string `code`\n * (and numeric `status`) is reported with its message; anything else is a generic failure. */\n toolError?: (error: unknown) => { code: string; message: string; status?: number } | null\n /** Called for a tool failure the default mapping hides from the model, for the app's logs. */\n onToolFailure?: (toolName: string, error: unknown) => void\n /** Cancels the turn: the model call in flight is aborted, no further tool starts, and the\n * outcome carries `turn_aborted`. Effects of tools that already ran are kept. */\n signal?: AbortSignal\n /** Default true: one repeat of a model call that failed before any word or tool, and of a blank\n * answer. Pass false when a second billable call needs the caller's own decision; the failure is\n * then returned as it happened. */\n repeat?: boolean\n /** Default true. False takes each model call as one buffered completion instead of a stream. */\n stream?: boolean\n /** Sent as `reasoning_effort`. */\n reasoningEffort?: string\n /** Sent as `max_completion_tokens`: one model call's visible and reasoning tokens together. */\n maxCompletionTokens?: number\n /** At most this many tool calls run. The next one is refused before it runs and the turn ends\n * with `turn_tool_limit`. */\n maxToolCalls?: number\n /** Replace the text the model reads for one tool result. `toolName` is the app's name. */\n renderToolResult?: (toolName: string, outcome: RouterTurnToolOutcome) => string\n /**\n * Place the turn before any model call, using `model` as the executed model rather than\n * the optional `requestedModel` hint. A turn placed in the sandbox returns at once\n * with `placement` and failure `placement_sandbox`, so the app runs it there instead.\n */\n placement?: Omit<RouterTurnPlacementInput, 'toolNames'>\n temperature?: number\n /** Models tried in order when a model call fails before any word or tool, or answers blank. Without it,\n * that safe repeat uses `model` again. `repeat: false` turns both off. */\n fallbackModels?: readonly string[]\n /** Resend one model request that answered 408, 425, 429 or 5xx, or failed to connect, before its stream\n * began, with exponential backoff (a `retry-after` up to 8 s is honoured). `attempts` counts the first\n * send (default 1). `timeoutMs` bounds each attempt, stream included. */\n retry?: { attempts?: number; timeoutMs?: number; baseDelayMs?: number }\n /** Characters of one tool result the model reads when no `renderToolResult` is given. Default 12 000. */\n toolResultLimit?: number\n /**\n * Which model API carries the turn. `auto` (default) uses the Responses API for an OpenAI model asked to\n * reason (`reasoningEffort` other than `none`), since OpenAI returns reasoning summaries only there, and\n * chat completions otherwise. A buffered turn (`stream: false`) always uses chat completions.\n */\n api?: 'auto' | 'chat' | 'responses'\n}\n\nconst DEFAULT_BUDGET_MS = 90_000\nconst RETRY_STATUS = new Set([408, 425, 429, 500, 502, 503, 504])\n// The Router names a refusal no resend of the same model can fix: every\n// credential for it is out of quota, rejected or suspended (tangle-router\n// `X-Tangle-Failure-Category`). The turn moves to its next fallback model at once.\nconst DEAD_CREDENTIAL_CATEGORIES = new Set(['provider_quota_exhausted', 'provider_key_invalid', 'provider_account_suspended'])\nconst MAX_RETRY_DELAY_MS = 8_000\nconst TOOL_EVENT_LIMIT = 500\n/** Marks a tool's typed event inside a loop progress update. */\nconst TOOL_EVENT = '__routerTurnToolEvent'\nconst TOOL_RESULT_LIMIT = 12_000\n\n/** OpenAI-compatible function names allow only `[A-Za-z0-9_-]{1,64}`; app tool names often use dots. */\nexport function routerFunctionName(name: string): string {\n return name.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 64)\n}\n\n/**\n * The lean system prompt for a Router turn: the profile's system prompt, appended prompt and\n * instructions, the turn's knowledge files, then caller context, with tool names rewritten to the function\n * names the model actually calls.\n */\nexport function composeRouterTurnPrompt(\n profile: { prompt?: { systemPrompt?: string; appendSystemPrompt?: string; instructions?: readonly string[] } },\n options: {\n context?: readonly string[]\n toolNames?: readonly string[]\n /** Knowledge files every turn carries; see {@link turnKnowledgeInstructions}. */\n knowledge?: { files: readonly TurnKnowledgeFile[]; readTool?: string }\n } = {},\n): string {\n const knowledge = options.knowledge ? turnKnowledgeInstructions(options.knowledge.files, { readTool: options.knowledge.readTool }) : []\n const text = [profile.prompt?.systemPrompt ?? '', profile.prompt?.appendSystemPrompt ?? '', ...(profile.prompt?.instructions ?? []), ...knowledge,\n replyLanguageInstruction(), ...(options.context ?? [])]\n .filter(part => part.trim()).join('\\n\\n')\n return [...(options.toolNames ?? [])].sort((a, b) => b.length - a.length)\n .reduce((out, name) => out.split(name).join(routerFunctionName(name)), text)\n}\n\nconst joinText = (parts: string[]) => parts.map(part => part.trim()).filter(Boolean).join('\\n\\n')\n\nfunction defaultToolError(error: unknown): { code: string; message: string; status?: number } | null {\n const value = error as { code?: unknown; status?: unknown; message?: unknown; issues?: unknown }\n if (Array.isArray(value?.issues)) {\n return { code: 'invalid_arguments', message: JSON.stringify(value.issues).slice(0, 600) }\n }\n if (typeof value?.code === 'string' && typeof value.message === 'string') {\n return { code: value.code, message: value.message, ...(typeof value.status === 'number' ? { status: value.status } : {}) }\n }\n return null\n}\n\nfunction renderResult(outcome: RouterTurnToolOutcome, limit = TOOL_RESULT_LIMIT): string {\n if (!outcome.ok) return `Error (${outcome.code}): ${outcome.message}`\n let text: string\n try { text = typeof outcome.result === 'string' ? outcome.result : JSON.stringify(outcome.result ?? null) }\n catch { text = String(outcome.result) }\n return text.length > limit ? `${text.slice(0, limit)}…` : text\n}\n\nexport async function runRouterTurn(options: RouterTurnOptions): Promise<RouterTurnOutcome> {\n // A continued turn answers the last user message in its checkpoint.\n const lastUser = options.continueFrom?.messages.filter(message => message.role === 'user').at(-1)?.content\n const currentUserMessage = options.continueFrom ? (typeof lastUser === 'string' ? lastUser : '') : options.userMessage\n const placement = options.placement\n ? placeRouterTurn({ ...options.placement, requestedModel: options.model, toolNames: (options.tools ?? []).map(tool => tool.name) })\n : undefined\n if (placement?.chosen === 'sandbox') {\n return { text: '', servedModel: null, toolCalls: [], usage: { promptTokens: 0, completionTokens: 0, modelCalls: 0 }, toolEvents: [], placement,\n failure: { code: 'placement_sandbox', message: placement.reason } }\n }\n const { apiKey } = options\n if (!apiKey) {\n return { text: '', servedModel: null, toolCalls: [], usage: { promptTokens: 0, completionTokens: 0, modelCalls: 0 }, toolEvents: [],\n ...(placement ? { placement } : {}),\n failure: { code: 'router_unconfigured', message: 'The model credential is not configured.' } }\n }\n const emit = options.onEvent ?? (() => {})\n const { signal } = options\n // A cancelled turn sends nothing more, whatever the injected fetch does with the signal.\n const attempts = Math.max(1, Math.floor(options.retry?.attempts ?? 1))\n const attemptTimeoutMs = options.retry?.timeoutMs\n const baseDelayMs = options.retry?.baseDelayMs ?? 500\n const pause = (ms: number) => new Promise<void>((resolve, reject) => {\n if (signal?.aborted) return reject(signal.reason)\n const timer = setTimeout(resolve, ms)\n signal?.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason) }, { once: true })\n })\n const traceId = options.traceId !== undefined ? turnTraceId(options.traceId) : options.trace?.traceId\n const { trace } = options\n let currentModel = options.model\n let lastModelSpan: TurnSpan | undefined\n // Each HTTP send is its own `model.call` span, so a retried 503 is visible beside the send that answered.\n const sendOnce: typeof fetch | undefined = trace\n ? tracedModelFetch(trace, options.fetchImpl ?? fetch, {\n onSpan: span => { lastModelSpan = span; span.set({ 'gen_ai.request.model': currentModel }) },\n })\n : options.fetchImpl\n const fetchImpl: typeof fetch | undefined = signal || attempts > 1 || attemptTimeoutMs\n ? async (input, init) => {\n for (let attempt = 1; ; attempt++) {\n signal?.throwIfAborted()\n const signals = [signal, attemptTimeoutMs ? AbortSignal.timeout(attemptTimeoutMs) : undefined]\n .filter((value): value is AbortSignal => Boolean(value))\n const attemptSignal = signals.length > 1 ? AbortSignal.any(signals) : signals[0]\n const backoff = () => Math.min(MAX_RETRY_DELAY_MS, baseDelayMs * 2 ** (attempt - 1)) * (0.5 + Math.random() / 2)\n // The wait before a resend is its own span, so the time between two sends is named, not a gap.\n const wait = (ms: number, reason: string) => trace\n ? trace.measure('model.retry_wait', 'other', () => pause(ms), { 'retry.attempt': attempt, 'retry.reason': reason, 'retry.wait_ms': Math.round(ms) })\n : pause(ms)\n let response: Response\n try {\n // A cancelled turn sends nothing more, whatever the injected fetch does with the signal.\n response = await (sendOnce ?? fetch)(input, { ...init, ...(attemptSignal ? { signal: attemptSignal } : {}) })\n } catch (error) {\n if (signal?.aborted || attempt >= attempts) throw error\n await wait(backoff(), 'send_failed')\n continue\n }\n // Nothing has streamed yet, so a resend repeats no word and no tool.\n if (attempt >= attempts || !RETRY_STATUS.has(response.status)) return response\n if (DEAD_CREDENTIAL_CATEGORIES.has(response.headers.get('x-tangle-failure-category') ?? '')) return response\n const retryAfter = Number(response.headers.get('retry-after'))\n await response.body?.cancel().catch(() => {})\n await wait(Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1000, MAX_RETRY_DELAY_MS) : backoff(), `http_${response.status}`)\n }\n }\n : sendOnce\n const extraBody = {\n ...(options.maxTokens ? { max_tokens: options.maxTokens } : {}),\n ...(options.maxCompletionTokens ? { max_completion_tokens: options.maxCompletionTokens } : {}),\n ...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),\n }\n const byFunction = new Map<string, RouterTurnTool>()\n for (const tool of options.tools ?? []) {\n const fn = routerFunctionName(tool.name)\n if (!byFunction.has(fn)) byFunction.set(fn, tool)\n }\n let servedModel: string | null = null\n const reasoningAsked = Boolean(options.reasoningEffort) && options.reasoningEffort !== 'none'\n const useResponses = (model: string) => options.stream !== false &&\n (options.api === 'responses' || ((options.api ?? 'auto') === 'auto' && reasoningAsked && isOpenAIResponsesModel(model)))\n const streamTurnFor = (model: string) => (options.stream === false ? createOpenAICompatCompletionTurn\n : useResponses(model) ? createOpenAIResponsesTurn : createOpenAICompatStreamTurn)({\n baseUrl: options.baseUrl ?? DEFAULT_TANGLE_ROUTER_BASE_URL,\n apiKey,\n model,\n ...(options.temperature !== undefined ? { temperature: options.temperature } : {}),\n tools: [...byFunction.entries()].map(([name, tool]) => ({\n type: 'function', function: { name, description: tool.description, parameters: tool.inputSchema },\n })),\n ...(Object.keys(extraBody).length ? { extraBody } : {}),\n ...(traceId ? { headers: { 'x-tangle-trace-id': traceId } } : {}),\n ...(fetchImpl ? { fetchImpl } : {}),\n onServedModel: served => {\n servedModel = served.servedModel\n if (served.servedModel) lastModelSpan?.set({ 'gen_ai.response.model': served.servedModel })\n },\n })\n const segments: string[] = []\n const usage = { promptTokens: 0, completionTokens: 0, modelCalls: 0 }\n // How much reasoning the turn streamed: the number an app logs to measure reasoning coverage.\n const reasoning = { deltas: 0, chars: 0 }\n const toolCalls: RouterTurnOutcome['toolCalls'] = []\n const toolEvents: RouterTurnToolEvent[] = []\n const started = new Map<string, number>()\n let segmentOpen = false\n let toolsRan = 0\n let checkpoint: LoopMessage[] | null = null\n let toolLimitReached = false\n const deadline = Date.now() + (options.budgetMs ?? DEFAULT_BUDGET_MS)\n const callIdOf = (id: string | undefined, toolName: string) => id ?? `${toolName}-${started.size}`\n\n let placementFailure: RouterTurnFailure | undefined\n const run = async (model: string): Promise<ToolLoopStopReason | 'tool_limit' | 'placement_refused'> => {\n // A safe model retry still cannot change execution tier after this turn began.\n if (placement && options.placement && model !== options.model) {\n const next = placeRouterTurn({ ...options.placement, requestedModel: model,\n toolNames: (options.tools ?? []).map(tool => tool.name) })\n if (next.chosen !== placement.chosen) {\n placementFailure = { code: 'placement_fallback_refused', message: `The fallback model cannot run in this turn's placement: ${next.reason}` }\n return 'placement_refused'\n }\n }\n currentModel = model\n const streamTurn = streamTurnFor(model)\n for await (const step of streamAppToolLoop<LoopEvent>({\n systemPrompt: options.systemPrompt,\n ...(options.continueFrom\n ? { priorMessages: options.continueFrom.messages }\n : { userMessage: options.userMessage, priorMessages: (options.history ?? []) as LoopMessage[] }),\n streamTurn,\n extractText: event => event.type === 'text' ? event.text : '',\n extractToolCall: event => event.type === 'tool_call' ? event.call : null,\n isExecutableTool: name => byFunction.has(name),\n deadlineMs: deadline,\n maxToolTurns: options.maxToolTurns ?? 12,\n labelFor: call => byFunction.get(call.toolName)?.name ?? call.toolName,\n renderResult: (label, outcome) => options.renderToolResult ? options.renderToolResult(label, outcome) : renderResult(outcome, options.toolResultLimit),\n executeToolCall: async (call: LoopToolCall, context) => {\n const tool = byFunction.get(call.toolName)\n if (!tool) return { ok: false, code: 'unknown_tool', message: `No tool named ${call.toolName}` }\n if (signal?.aborted) return { ok: false, code: 'aborted', message: 'The turn was cancelled before this tool ran.' }\n if (options.maxToolCalls !== undefined && toolsRan >= options.maxToolCalls) {\n toolLimitReached = true\n return { ok: false, code: 'tool_limit', message: 'The turn reached its tool-call limit.' }\n }\n toolsRan++\n // A tool's typed event rides the loop's progress channel, so it streams in order before the result.\n const emitEvent = (kind: string, data?: unknown) => context?.progress({ data: { [TOOL_EVENT]: kind, data } })\n const toolSpan = trace?.start('tool.call', 'tool', {\n 'gen_ai.tool.name': tool.name,\n 'tool.call_id': call.toolCallId ?? '',\n 'call.index': nextCallIndex(trace, 'tool.call'),\n })\n try {\n const result = await tool.run(call.args, {\n emit: emitEvent,\n userMessage: currentUserMessage,\n ...(trace && toolSpan ? { trace, span: toolSpan } : {}),\n })\n toolSpan?.end({ 'tool.ok': true })\n return { ok: true, result }\n } catch (error) {\n const mapped = (options.toolError ?? defaultToolError)(error)\n // The span names the failure by its code; a thrown message can carry customer text.\n toolSpan?.end({ 'tool.ok': false }, mapped?.code ?? 'tool_failed')\n if (mapped) return { ok: false, ...mapped }\n options.onToolFailure?.(tool.name, error)\n return { ok: false, code: 'tool_failed', message: 'The tool failed; tell the user what could not be completed.' }\n }\n },\n })) {\n if (step.kind === 'event') {\n const event = step.event\n if (event.type === 'text' && event.text) {\n if (!segmentOpen) { segments.push(''); segmentOpen = true }\n segments[segments.length - 1] += event.text\n emit({ type: 'text', segment: segments.length, delta: event.text })\n } else if (event.type === 'reasoning' && event.text) {\n reasoning.deltas++\n reasoning.chars += event.text.length\n emit({ type: 'reasoning', delta: event.text })\n } else if (event.type === 'usage') {\n usage.promptTokens += event.usage.promptTokens\n usage.completionTokens += event.usage.completionTokens\n usage.modelCalls++\n lastModelSpan?.set({\n 'gen_ai.usage.input_tokens': event.usage.promptTokens,\n 'gen_ai.usage.output_tokens': event.usage.completionTokens,\n })\n emit({ type: 'usage', ...event.usage })\n } else if (event.type === 'tool_call') {\n segmentOpen = false\n const tool = byFunction.get(event.call.toolName)\n const callId = callIdOf(event.call.toolCallId, event.call.toolName)\n started.set(callId, Date.now())\n emit({ type: 'tool', callId, toolName: tool?.name ?? event.call.toolName, status: 'running',\n input: event.call.args, startedAt: started.get(callId)! })\n }\n } else if (step.kind === 'tool_progress') {\n const tool = byFunction.get(step.toolName)\n const marked = step.update.data as { [TOOL_EVENT]?: unknown; data?: unknown } | undefined\n // A tool's own emit carries its kind; plain loop progress (a step count, a message) is kind `progress`.\n const typed = typeof marked?.[TOOL_EVENT] === 'string'\n const event: RouterTurnToolEvent = {\n callId: callIdOf(step.toolCallId, step.toolName), toolName: tool?.name ?? step.toolName,\n kind: typed ? marked![TOOL_EVENT] as string : 'progress',\n data: typed ? marked!.data ?? null : step.update, at: Date.now(),\n }\n if (toolEvents.length < TOOL_EVENT_LIMIT) toolEvents.push(event)\n emit({ type: 'tool_event', ...event })\n } else if (step.kind === 'tool_result') {\n const tool = byFunction.get(step.toolName)\n const callId = callIdOf(step.toolCallId, step.toolName)\n const startedAt = started.get(callId) ?? Date.now()\n const toolName = tool?.name ?? step.toolName\n toolCalls.push({ callId, toolName, ok: step.outcome.ok })\n emit(step.outcome.ok\n ? { type: 'tool', callId, toolName, status: 'completed', output: step.outcome.result, startedAt, endedAt: Date.now() }\n : { type: 'tool', callId, toolName, status: 'error', startedAt, endedAt: Date.now(),\n error: { code: step.outcome.code, message: step.outcome.message } })\n // The refused call never ran; the turn ends here rather than asking the model again.\n if (toolLimitReached) return 'tool_limit'\n } else if (step.kind === 'capped') {\n if (step.stopReason === 'deadline' && step.messages) checkpoint = step.messages\n return step.stopReason\n }\n }\n return 'completed'\n }\n\n const outcome = (failure?: RouterTurnFailure): RouterTurnOutcome => ({\n text: joinText([options.continueFrom?.text ?? '', ...segments]),\n servedModel, toolCalls, usage: { ...usage }, reasoning: { ...reasoning }, toolEvents: [...toolEvents], ...(placement ? { placement } : {}), ...(failure ? { failure } : {}),\n })\n const repeat = options.repeat !== false\n // The safe repeat walks this chain: the next fallback model, or the same model once.\n const repeats = !repeat ? [] : options.fallbackModels?.length ? [...options.fallbackModels] : [options.model]\n let stop: ToolLoopStopReason | 'tool_limit' | 'placement_refused'\n try {\n try {\n stop = await run(options.model)\n } catch (error) {\n // Safe to repeat only while nothing was said and no tool ran.\n const next = repeats.shift()\n if (next === undefined || signal?.aborted || toolsRan > 0 || segments.length > 0) throw error\n stop = await run(next)\n }\n } catch (error) {\n if (signal?.aborted) return outcome({ code: 'turn_aborted', message: 'The turn was cancelled.' })\n const status = (error as { status?: unknown }).status\n // The stream adapter puts the service's own text after the status; keep it, short, for the app's error categories.\n const detail = error instanceof Error ? error.message.match(/\\(HTTP \\d+\\): ([\\s\\S]*)$/)?.[1]?.slice(0, 1000) : undefined\n return outcome(typeof status === 'number'\n ? { code: `model_http_${status}`, status, message: 'The model service did not answer.', ...(detail ? { detail } : {}) }\n : { code: 'model_unavailable', message: 'The model service did not answer.' })\n }\n if (placementFailure) return outcome(placementFailure)\n if (stop === 'deadline' && checkpoint) {\n return { ...outcome(), continuation: { messages: checkpoint,\n hops: (options.continueFrom?.hops ?? 0) + 1,\n text: outcome().text } }\n }\n if (stop !== 'completed') return outcome({ code: `turn_${stop}`, message: 'The reply was stopped before it finished.' })\n // A model can end its turn with neither a word nor a tool call (gemini-2.5-flash did on 8 of\n // 53 \"remember\" turns, 2026-10-11). Nothing was said and nothing ran, so one repeat is safe;\n // an answer still blank is a typed failure, never a successful empty reply.\n if (!segments.some(part => part.trim()) && !options.continueFrom?.text.trim()) {\n const next = repeats.shift()\n if (toolsRan === 0 && next !== undefined) {\n try { stop = await run(next) } catch { stop = 'completed' }\n if (placementFailure) return outcome(placementFailure)\n if (stop !== 'completed') return outcome({ code: `turn_${stop}`, message: 'The reply was stopped before it finished.' })\n }\n if (!segments.some(part => part.trim())) {\n return outcome({ code: 'model_empty', message: 'The model finished without writing a reply.' })\n }\n }\n return outcome()\n}\n","/**\n * A Router turn never fails because of time. `runRouterTurn` stops at its hop\n * budget at a clean point and returns a `continuation`; the host saves it here\n * and runs the next hop in a durable process (a Durable Object alarm or a Queue\n * consumer: 15 minutes of wall time each), posting into the same conversation.\n *\n * Checkpoints live in the app's workspace store under `/turns/<turnId>/`, which\n * a Files view (`/files/`) never lists.\n */\nimport type { RouterTurnContinuation } from './router-turn'\n\n/** Wall time a durable hop may plan for: a Durable Object alarm or Queue consumer gets 15 minutes. */\nexport const DURABLE_HOP_BUDGET_MS = 14 * 60_000\n\n/** Where a turn's checkpoint waits between hops. */\nexport interface TurnCheckpointStore {\n save(turnId: string, continuation: RouterTurnContinuation): Promise<void>\n load(turnId: string): Promise<RouterTurnContinuation | null>\n clear(turnId: string): Promise<void>\n}\n\ninterface CheckpointEntry { content?: string; revision: number }\n\n/** The fields of an agent-runtime `WorkspaceStore` a checkpoint needs; structural so any adapter fits. */\nexport interface CheckpointWorkspaceStore {\n get(workspaceId: string, path: string): Promise<CheckpointEntry | null>\n put(input: { workspaceId: string; path: string; kind: 'file'; ifRevision: number | null; actor: string; content: string }): Promise<unknown>\n delete(input: { workspaceId: string; path: string; actor: string }): Promise<unknown>\n}\n\nexport function turnCheckpointPath(turnId: string): string {\n return `/turns/${encodeURIComponent(turnId)}/checkpoint.json`\n}\n\n/** Turn checkpoints in a workspace store, written as `actor`. */\nexport function workspaceTurnCheckpoints(store: CheckpointWorkspaceStore, workspaceId: string, actor: string): TurnCheckpointStore {\n return {\n async save(turnId, continuation) {\n const path = turnCheckpointPath(turnId)\n const current = await store.get(workspaceId, path)\n await store.put({ workspaceId, path, kind: 'file', ifRevision: current?.revision ?? null, actor,\n content: JSON.stringify(continuation) })\n },\n async load(turnId) {\n const entry = await store.get(workspaceId, turnCheckpointPath(turnId))\n return entry?.content ? JSON.parse(entry.content) as RouterTurnContinuation : null\n },\n async clear(turnId) {\n await store.delete({ workspaceId, path: turnCheckpointPath(turnId), actor })\n },\n }\n}\n"],"mappings":"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","names":["outcome"]}
@@ -1,254 +0,0 @@
1
- import {
2
- validateAnswer
3
- } from "./chunk-JDEGS53O.js";
4
-
5
- // src/intakes-react/components/IntakeInterview.tsx
6
- import { useEffect, useState } from "react";
7
-
8
- // src/intakes-react/components/BrandMark.tsx
9
- import { lazy, Suspense } from "react";
10
- import { jsx } from "react/jsx-runtime";
11
- function MarkSpacer({ size }) {
12
- return /* @__PURE__ */ jsx("span", { "aria-hidden": true, style: { display: "inline-block", width: size, height: size } });
13
- }
14
- var LazyKnot = lazy(async () => {
15
- try {
16
- const mod = await import("./brand/index.js");
17
- return { default: mod.TangleKnot };
18
- } catch {
19
- return { default: MarkSpacer };
20
- }
21
- });
22
- function BrandMark({ size, className }) {
23
- return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(MarkSpacer, { size }), children: /* @__PURE__ */ jsx(LazyKnot, { size, className }) });
24
- }
25
-
26
- // src/intakes-react/components/IntakeInterview.tsx
27
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
28
- function IntakeInterview({
29
- view: initialView,
30
- onAnswer,
31
- onComplete,
32
- onDone,
33
- onNotice
34
- }) {
35
- const [view, setView] = useState(initialView);
36
- const [draft, setDraft] = useState(currentAnswer(initialView));
37
- const [busy, setBusy] = useState(false);
38
- const [doneFired, setDoneFired] = useState(false);
39
- useEffect(() => {
40
- setView(initialView);
41
- setDraft(currentAnswer(initialView));
42
- }, [initialView]);
43
- useEffect(() => {
44
- if (view.completed && !doneFired) {
45
- setDoneFired(true);
46
- onDone?.();
47
- }
48
- }, [view.completed, doneFired, onDone]);
49
- function notify(kind, message) {
50
- onNotice?.({ kind, message });
51
- }
52
- const question = view.nextQuestion;
53
- async function submit() {
54
- if (!question || busy) return;
55
- const validity = validateAnswer(question, draft);
56
- if (!validity.ok) {
57
- notify("error", `Please answer: ${validity.reason}`);
58
- return;
59
- }
60
- setBusy(true);
61
- try {
62
- const next = await onAnswer({ questionId: question.id, value: normalize(question, draft) });
63
- setView(next);
64
- setDraft(currentAnswer(next));
65
- } catch (err) {
66
- notify("error", err instanceof Error ? err.message : "Failed to save answer");
67
- } finally {
68
- setBusy(false);
69
- }
70
- }
71
- async function finish() {
72
- if (busy) return;
73
- setBusy(true);
74
- try {
75
- const next = await onComplete();
76
- setView(next);
77
- notify("success", "All set.");
78
- } catch (err) {
79
- notify("error", err instanceof Error ? err.message : "Failed to finish");
80
- } finally {
81
- setBusy(false);
82
- }
83
- }
84
- return /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-5", children: [
85
- /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-1", children: [
86
- /* @__PURE__ */ jsxs("span", { className: "mb-1 flex items-center gap-2 text-[var(--text-muted)]", children: [
87
- /* @__PURE__ */ jsx2(BrandMark, { size: 20, className: "shrink-0" }),
88
- /* @__PURE__ */ jsx2("span", { className: "text-xs font-semibold uppercase tracking-[0.05em]", children: "Tangle Intake" })
89
- ] }),
90
- /* @__PURE__ */ jsx2("h2", { className: "text-xl font-semibold text-[var(--text-primary)]", children: view.title }),
91
- view.description && /* @__PURE__ */ jsx2("p", { className: "text-sm text-[var(--text-muted)]", children: view.description }),
92
- /* @__PURE__ */ jsx2(ProgressBar, { answered: view.progress.answered, total: view.progress.total })
93
- ] }),
94
- view.completed ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 rounded-lg border border-[hsl(var(--success)/0.4)] bg-[hsl(var(--success)/0.08)] px-3.5 py-3", children: [
95
- /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", className: "h-4 w-4 shrink-0 text-[var(--surface-success-text)]", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx2("polyline", { points: "20 6 9 17 4 12" }) }),
96
- /* @__PURE__ */ jsx2("p", { className: "text-sm text-[var(--text-primary)]", children: "You're all set \u2014 every answer is saved. You can close this panel." })
97
- ] }) : question ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
98
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
99
- /* @__PURE__ */ jsx2("p", { className: "text-sm font-medium text-[var(--text-primary)]", children: question.prompt }),
100
- question.help && /* @__PURE__ */ jsx2("p", { className: "text-xs text-[var(--text-muted)]", children: question.help })
101
- ] }),
102
- /* @__PURE__ */ jsx2(AnswerField, { question, value: draft, onChange: setDraft, onSubmit: () => void submit() }),
103
- /* @__PURE__ */ jsx2("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx2(
104
- "button",
105
- {
106
- type: "button",
107
- onClick: () => void submit(),
108
- disabled: busy || isDraftEmpty(draft),
109
- className: "rounded bg-[var(--brand-primary)] px-4 py-1.5 text-sm text-[hsl(var(--primary-foreground))] disabled:opacity-50",
110
- children: busy ? "Saving\u2026" : "Continue"
111
- }
112
- ) })
113
- ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
114
- /* @__PURE__ */ jsx2("p", { className: "text-sm text-[var(--text-secondary)]", children: "That's everything. Ready to finish?" }),
115
- /* @__PURE__ */ jsx2("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx2(
116
- "button",
117
- {
118
- type: "button",
119
- onClick: () => void finish(),
120
- disabled: busy,
121
- className: "rounded bg-[var(--brand-primary)] px-4 py-1.5 text-sm text-[hsl(var(--primary-foreground))] disabled:opacity-50",
122
- children: busy ? "Finishing\u2026" : "Finish"
123
- }
124
- ) })
125
- ] })
126
- ] });
127
- }
128
- function AnswerField({ question, value, onChange, onSubmit }) {
129
- const inputClass = "rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-3 py-2 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)]";
130
- switch (question.type) {
131
- case "long-text":
132
- return /* @__PURE__ */ jsx2(
133
- "textarea",
134
- {
135
- rows: 4,
136
- "aria-label": question.prompt,
137
- value: typeof value === "string" ? value : "",
138
- onChange: (event) => onChange(event.target.value),
139
- className: inputClass
140
- }
141
- );
142
- case "boolean":
143
- return /* @__PURE__ */ jsx2("div", { className: "flex gap-2", children: [{ v: true, l: "Yes" }, { v: false, l: "No" }].map((opt) => /* @__PURE__ */ jsx2(
144
- "button",
145
- {
146
- type: "button",
147
- onClick: () => onChange(opt.v),
148
- className: `rounded border px-4 py-1.5 text-sm ${value === opt.v ? "border-[var(--brand-primary)] bg-[var(--brand-primary)] text-[hsl(var(--primary-foreground))]" : "border-[var(--border-default)] bg-[var(--bg-input)] text-[var(--text-secondary)]"}`,
149
- children: opt.l
150
- },
151
- opt.l
152
- )) });
153
- case "number":
154
- return /* @__PURE__ */ jsx2(
155
- "input",
156
- {
157
- type: "number",
158
- "aria-label": question.prompt,
159
- value: typeof value === "number" ? value : "",
160
- onChange: (event) => onChange(event.target.value === "" ? null : Number(event.target.value)),
161
- onKeyDown: (event) => {
162
- if (event.key === "Enter") onSubmit();
163
- },
164
- className: inputClass
165
- }
166
- );
167
- case "single-select":
168
- return /* @__PURE__ */ jsx2("div", { className: "flex flex-col gap-1.5", children: (question.options ?? []).map((option) => /* @__PURE__ */ jsxs(
169
- "button",
170
- {
171
- type: "button",
172
- onClick: () => onChange(option.value),
173
- className: `flex items-center gap-2 rounded border px-3 py-2 text-left text-sm ${value === option.value ? "border-[var(--brand-primary)] bg-[color-mix(in_srgb,var(--brand-primary)_10%,transparent)] text-[var(--text-primary)]" : "border-[var(--border-default)] bg-[var(--bg-input)] text-[var(--text-secondary)]"}`,
174
- children: [
175
- value === option.value && /* @__PURE__ */ jsx2(ChoiceCheck, {}),
176
- option.label
177
- ]
178
- },
179
- option.value
180
- )) });
181
- case "multi-select": {
182
- const selected = Array.isArray(value) ? value : [];
183
- return /* @__PURE__ */ jsx2("div", { className: "flex flex-col gap-1.5", children: (question.options ?? []).map((option) => {
184
- const on = selected.includes(option.value);
185
- return /* @__PURE__ */ jsxs(
186
- "button",
187
- {
188
- type: "button",
189
- onClick: () => onChange(on ? selected.filter((v) => v !== option.value) : [...selected, option.value]),
190
- className: `flex items-center gap-2 rounded border px-3 py-2 text-left text-sm ${on ? "border-[var(--brand-primary)] bg-[color-mix(in_srgb,var(--brand-primary)_10%,transparent)] text-[var(--text-primary)]" : "border-[var(--border-default)] bg-[var(--bg-input)] text-[var(--text-secondary)]"}`,
191
- children: [
192
- on && /* @__PURE__ */ jsx2(ChoiceCheck, {}),
193
- option.label
194
- ]
195
- },
196
- option.value
197
- );
198
- }) });
199
- }
200
- default:
201
- return /* @__PURE__ */ jsx2(
202
- "input",
203
- {
204
- type: question.type === "email" ? "email" : question.type === "url" ? "url" : "text",
205
- "aria-label": question.prompt,
206
- value: typeof value === "string" ? value : "",
207
- onChange: (event) => onChange(event.target.value),
208
- onKeyDown: (event) => {
209
- if (event.key === "Enter") onSubmit();
210
- },
211
- className: inputClass
212
- }
213
- );
214
- }
215
- }
216
- function ChoiceCheck() {
217
- return /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 24 24", className: "h-3.5 w-3.5 shrink-0 text-[var(--brand-primary)]", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx2("polyline", { points: "20 6 9 17 4 12" }) });
218
- }
219
- function ProgressBar({ answered, total }) {
220
- const pct = total > 0 ? Math.round(answered / total * 100) : 0;
221
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
222
- /* @__PURE__ */ jsx2("div", { className: "h-1.5 flex-1 overflow-hidden rounded-full bg-[var(--bg-input)]", children: /* @__PURE__ */ jsx2("div", { className: "h-full rounded-full bg-[var(--brand-primary)] transition-all", style: { width: `${pct}%` } }) }),
223
- /* @__PURE__ */ jsxs("span", { className: "text-xs text-[var(--text-muted)]", children: [
224
- answered,
225
- "/",
226
- total
227
- ] })
228
- ] });
229
- }
230
- function currentAnswer(view) {
231
- const id = view.nextQuestion?.id;
232
- if (!id) return null;
233
- return view.answers[id] ?? defaultDraft(view.nextQuestion);
234
- }
235
- function defaultDraft(question) {
236
- return question.type === "multi-select" ? [] : null;
237
- }
238
- function isDraftEmpty(value) {
239
- if (value === null || value === void 0) return true;
240
- if (typeof value === "string") return value.trim() === "";
241
- if (Array.isArray(value)) return value.length === 0;
242
- return false;
243
- }
244
- function normalize(question, value) {
245
- if (typeof value === "string" && (question.type === "text" || question.type === "long-text" || question.type === "url" || question.type === "email")) {
246
- return value.trim();
247
- }
248
- return value;
249
- }
250
-
251
- export {
252
- IntakeInterview
253
- };
254
- //# sourceMappingURL=chunk-I2CA5N7V.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/intakes-react/components/IntakeInterview.tsx","../src/intakes-react/components/BrandMark.tsx"],"sourcesContent":["/**\n * Agent-led intake interview: asks one question at a time, advancing as each\n * answer is saved. Fully callback-driven — the host supplies the loaded `view`\n * and the async `onAnswer` / `onComplete` callbacks (backed by `./intakes/api`),\n * so this imports no app router, fetch client, or toast. Styled with the\n * shipped Tangle Quiet tokens (`var(--*)`).\n *\n * The server owns traversal: each `onAnswer` resolves with the next view (next\n * question + progress), so the component never re-derives the graph — it renders\n * `view.nextQuestion`, and when that is null and the view is completable it\n * shows the finish action. Local validation only gates the submit button; the\n * authoritative validation runs in the store.\n */\n\nimport { useEffect, useState } from 'react'\nimport type { IntakeAnswerValue, IntakeQuestion } from '../../intakes/model'\nimport { validateAnswer } from '../../intakes/model'\nimport type { IntakeInterviewProps, IntakeView } from '../contracts'\nimport { BrandMark } from './BrandMark'\n\nexport function IntakeInterview({\n view: initialView,\n onAnswer,\n onComplete,\n onDone,\n onNotice,\n}: IntakeInterviewProps) {\n const [view, setView] = useState<IntakeView>(initialView)\n const [draft, setDraft] = useState<IntakeAnswerValue>(currentAnswer(initialView))\n const [busy, setBusy] = useState(false)\n const [doneFired, setDoneFired] = useState(false)\n\n useEffect(() => {\n setView(initialView)\n setDraft(currentAnswer(initialView))\n }, [initialView])\n\n useEffect(() => {\n if (view.completed && !doneFired) {\n setDoneFired(true)\n onDone?.()\n }\n }, [view.completed, doneFired, onDone])\n\n function notify(kind: 'success' | 'error', message: string) {\n onNotice?.({ kind, message })\n }\n\n const question = view.nextQuestion\n\n async function submit() {\n if (!question || busy) return\n const validity = validateAnswer(question, draft)\n if (!validity.ok) {\n notify('error', `Please answer: ${validity.reason}`)\n return\n }\n setBusy(true)\n try {\n const next = await onAnswer({ questionId: question.id, value: normalize(question, draft) })\n setView(next)\n setDraft(currentAnswer(next))\n } catch (err) {\n notify('error', err instanceof Error ? err.message : 'Failed to save answer')\n } finally {\n setBusy(false)\n }\n }\n\n async function finish() {\n if (busy) return\n setBusy(true)\n try {\n const next = await onComplete()\n setView(next)\n notify('success', 'All set.')\n } catch (err) {\n notify('error', err instanceof Error ? err.message : 'Failed to finish')\n } finally {\n setBusy(false)\n }\n }\n\n return (\n <section className=\"flex flex-col gap-5\">\n <header className=\"flex flex-col gap-1\">\n <span className=\"mb-1 flex items-center gap-2 text-[var(--text-muted)]\">\n <BrandMark size={20} className=\"shrink-0\" />\n <span className=\"text-xs font-semibold uppercase tracking-[0.05em]\">Tangle Intake</span>\n </span>\n <h2 className=\"text-xl font-semibold text-[var(--text-primary)]\">{view.title}</h2>\n {view.description && <p className=\"text-sm text-[var(--text-muted)]\">{view.description}</p>}\n <ProgressBar answered={view.progress.answered} total={view.progress.total} />\n </header>\n\n {view.completed ? (\n <div className=\"flex items-center gap-2.5 rounded-lg border border-[hsl(var(--success)/0.4)] bg-[hsl(var(--success)/0.08)] px-3.5 py-3\">\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4 shrink-0 text-[var(--surface-success-text)]\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n <p className=\"text-sm text-[var(--text-primary)]\">You're all set — every answer is saved. You can close this panel.</p>\n </div>\n ) : question ? (\n <div className=\"flex flex-col gap-3\">\n <div className=\"flex flex-col gap-1\">\n <p className=\"text-sm font-medium text-[var(--text-primary)]\">{question.prompt}</p>\n {question.help && <p className=\"text-xs text-[var(--text-muted)]\">{question.help}</p>}\n </div>\n\n <AnswerField question={question} value={draft} onChange={setDraft} onSubmit={() => void submit()} />\n\n <div className=\"flex justify-end\">\n <button\n type=\"button\"\n onClick={() => void submit()}\n disabled={busy || isDraftEmpty(draft)}\n className=\"rounded bg-[var(--brand-primary)] px-4 py-1.5 text-sm text-[hsl(var(--primary-foreground))] disabled:opacity-50\"\n >\n {busy ? 'Saving…' : 'Continue'}\n </button>\n </div>\n </div>\n ) : (\n <div className=\"flex flex-col gap-3\">\n <p className=\"text-sm text-[var(--text-secondary)]\">That's everything. Ready to finish?</p>\n <div className=\"flex justify-end\">\n <button\n type=\"button\"\n onClick={() => void finish()}\n disabled={busy}\n className=\"rounded bg-[var(--brand-primary)] px-4 py-1.5 text-sm text-[hsl(var(--primary-foreground))] disabled:opacity-50\"\n >\n {busy ? 'Finishing…' : 'Finish'}\n </button>\n </div>\n </div>\n )}\n </section>\n )\n}\n\ninterface AnswerFieldProps {\n question: IntakeQuestion\n value: IntakeAnswerValue\n onChange(value: IntakeAnswerValue): void\n onSubmit(): void\n}\n\nfunction AnswerField({ question, value, onChange, onSubmit }: AnswerFieldProps) {\n const inputClass =\n 'rounded border border-[var(--border-default)] bg-[var(--bg-input)] px-3 py-2 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)]'\n\n switch (question.type) {\n case 'long-text':\n return (\n <textarea\n rows={4}\n aria-label={question.prompt}\n value={typeof value === 'string' ? value : ''}\n onChange={(event) => onChange(event.target.value)}\n className={inputClass}\n />\n )\n case 'boolean':\n return (\n <div className=\"flex gap-2\">\n {[{ v: true, l: 'Yes' }, { v: false, l: 'No' }].map((opt) => (\n <button\n key={opt.l}\n type=\"button\"\n onClick={() => onChange(opt.v)}\n className={`rounded border px-4 py-1.5 text-sm ${\n value === opt.v\n ? 'border-[var(--brand-primary)] bg-[var(--brand-primary)] text-[hsl(var(--primary-foreground))]'\n : 'border-[var(--border-default)] bg-[var(--bg-input)] text-[var(--text-secondary)]'\n }`}\n >\n {opt.l}\n </button>\n ))}\n </div>\n )\n case 'number':\n return (\n <input\n type=\"number\"\n aria-label={question.prompt}\n value={typeof value === 'number' ? value : ''}\n onChange={(event) => onChange(event.target.value === '' ? null : Number(event.target.value))}\n onKeyDown={(event) => { if (event.key === 'Enter') onSubmit() }}\n className={inputClass}\n />\n )\n case 'single-select':\n return (\n <div className=\"flex flex-col gap-1.5\">\n {(question.options ?? []).map((option) => (\n <button\n key={option.value}\n type=\"button\"\n onClick={() => onChange(option.value)}\n className={`flex items-center gap-2 rounded border px-3 py-2 text-left text-sm ${\n value === option.value\n ? 'border-[var(--brand-primary)] bg-[color-mix(in_srgb,var(--brand-primary)_10%,transparent)] text-[var(--text-primary)]'\n : 'border-[var(--border-default)] bg-[var(--bg-input)] text-[var(--text-secondary)]'\n }`}\n >\n {value === option.value && <ChoiceCheck />}\n {option.label}\n </button>\n ))}\n </div>\n )\n case 'multi-select': {\n const selected = Array.isArray(value) ? value : []\n return (\n <div className=\"flex flex-col gap-1.5\">\n {(question.options ?? []).map((option) => {\n const on = selected.includes(option.value)\n return (\n <button\n key={option.value}\n type=\"button\"\n onClick={() =>\n onChange(on ? selected.filter((v) => v !== option.value) : [...selected, option.value])\n }\n className={`flex items-center gap-2 rounded border px-3 py-2 text-left text-sm ${\n on\n ? 'border-[var(--brand-primary)] bg-[color-mix(in_srgb,var(--brand-primary)_10%,transparent)] text-[var(--text-primary)]'\n : 'border-[var(--border-default)] bg-[var(--bg-input)] text-[var(--text-secondary)]'\n }`}\n >\n {on && <ChoiceCheck />}\n {option.label}\n </button>\n )\n })}\n </div>\n )\n }\n default:\n return (\n <input\n type={question.type === 'email' ? 'email' : question.type === 'url' ? 'url' : 'text'}\n aria-label={question.prompt}\n value={typeof value === 'string' ? value : ''}\n onChange={(event) => onChange(event.target.value)}\n onKeyDown={(event) => { if (event.key === 'Enter') onSubmit() }}\n className={inputClass}\n />\n )\n }\n}\n\n/** Leading check for a selected choice option (single/multi-select). */\nfunction ChoiceCheck() {\n return (\n <svg viewBox=\"0 0 24 24\" className=\"h-3.5 w-3.5 shrink-0 text-[var(--brand-primary)]\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n )\n}\n\nfunction ProgressBar({ answered, total }: { answered: number; total: number }) {\n const pct = total > 0 ? Math.round((answered / total) * 100) : 0\n return (\n <div className=\"flex items-center gap-2\">\n <div className=\"h-1.5 flex-1 overflow-hidden rounded-full bg-[var(--bg-input)]\">\n <div className=\"h-full rounded-full bg-[var(--brand-primary)] transition-all\" style={{ width: `${pct}%` }} />\n </div>\n <span className=\"text-xs text-[var(--text-muted)]\">{answered}/{total}</span>\n </div>\n )\n}\n\nfunction currentAnswer(view: IntakeView): IntakeAnswerValue {\n const id = view.nextQuestion?.id\n if (!id) return null\n return view.answers[id] ?? defaultDraft(view.nextQuestion!)\n}\n\nfunction defaultDraft(question: IntakeQuestion): IntakeAnswerValue {\n return question.type === 'multi-select' ? [] : null\n}\n\n/** No answer yet — null, empty text, or an empty multi-select. `false` and `0`\n * are real answers and never gate Continue. */\nfunction isDraftEmpty(value: IntakeAnswerValue): boolean {\n if (value === null || value === undefined) return true\n if (typeof value === 'string') return value.trim() === ''\n if (Array.isArray(value)) return value.length === 0\n return false\n}\n\n/** Trim text answers before submit; pass others through unchanged. */\nfunction normalize(question: IntakeQuestion, value: IntakeAnswerValue): IntakeAnswerValue {\n if (typeof value === 'string' && (question.type === 'text' || question.type === 'long-text' || question.type === 'url' || question.type === 'email')) {\n return value.trim()\n }\n return value\n}\n","/**\n * Load the canonical Tangle knot only when an intakes surface renders it.\n * `/brand` imports the optional `@tangle-network/brand` peer, so a failed load\n * renders a fixed-size spacer instead of crashing the panel.\n */\n\nimport { lazy, Suspense } from 'react'\nimport type { ComponentType } from 'react'\n\nexport interface BrandMarkProps {\n size: number\n className?: string\n}\n\n/** Preserve the mark's footprint while its optional package loads or is absent. */\nfunction MarkSpacer({ size }: { size: number }) {\n return <span aria-hidden style={{ display: 'inline-block', width: size, height: size }} />\n}\n\nconst LazyKnot = lazy(async () => {\n try {\n const mod = await import('../../brand')\n return { default: mod.TangleKnot as ComponentType<BrandMarkProps> }\n } catch {\n return { default: MarkSpacer as ComponentType<BrandMarkProps> }\n }\n})\n\nexport function BrandMark({ size, className }: BrandMarkProps) {\n return (\n <Suspense fallback={<MarkSpacer size={size} />}>\n <LazyKnot size={size} className={className} />\n </Suspense>\n )\n}\n"],"mappings":"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","names":["jsx"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/intakes/model.ts"],"sourcesContent":["/**\n * Pure intake MODEL — the question-graph and the answer/completion algebra a\n * one-question-at-a-time interview runs on. Zero dependencies: no drizzle, no\n * env, no react, no I/O. The DB layer (`./intakes/drizzle`), the handlers\n * (`./intakes/api`) and the React surface (`./intakes-react`) all build on\n * these functions; this leaf imports nothing back, so a consumer can pull just\n * the intake math.\n *\n * An intake is an ordered list of questions, optionally branching: a question\n * can declare `next` as a function of the answers so far, so the next prompt\n * depends on what was said (e.g. \"do you have a website?\" → no → skip the URL\n * question). Traversal is pure and deterministic — `nextQuestion(graph, answers)`\n * folds the answers into the single question to ask next (or null = done), and\n * `isComplete` is true exactly when every REQUIRED reachable question has a\n * valid answer.\n *\n * Answers are a flat `Record<questionId, value>`. Each question declares an\n * answer `type` that `validateAnswer` checks against — the same validation the\n * UI runs before advancing and the store runs before persisting, so an invalid\n * answer can never enter the payload.\n */\n\n/** The kinds of answer a question accepts. */\nexport type IntakeAnswerType =\n | 'text'\n | 'long-text'\n | 'single-select'\n | 'multi-select'\n | 'boolean'\n | 'number'\n | 'url'\n | 'email'\n\n/** A selectable option for single/multi-select questions. */\nexport interface IntakeOption {\n value: string\n label: string\n}\n\n/** A flat map of answers keyed by question id. */\nexport type IntakeAnswers = Record<string, IntakeAnswerValue>\n\n/** Any value an answer can hold; the type is validated per-question. */\nexport type IntakeAnswerValue = string | string[] | number | boolean | null\n\n/**\n * One question in the graph. `next` (optional) makes the graph branch: given\n * the answers so far it returns the id of the question to ask next, or null to\n * end the interview early. With no `next`, traversal falls through to the next\n * question in declaration order.\n */\nexport interface IntakeQuestion {\n id: string\n /** The prompt the interviewer asks. */\n prompt: string\n type: IntakeAnswerType\n /** Required questions gate completion; optional ones may be skipped. */\n required?: boolean\n /** Help text shown under the prompt. */\n help?: string\n /** Options for single/multi-select questions. */\n options?: IntakeOption[]\n /** Min length (text) or min value (number); inclusive. */\n min?: number\n /** Max length (text) or max value (number); inclusive. */\n max?: number\n /**\n * Branch override: given the answers so far, the id of the next question\n * (or null to end early). Omit for linear flow (next in declaration order).\n */\n next?(answers: IntakeAnswers): string | null\n}\n\n/** The intake definition: an ordered, addressable set of questions. */\nexport interface IntakeGraph {\n /** Stable id for the intake definition (e.g. 'user-onboarding-v1'). */\n id: string\n /** Human title shown at the top of the interview. */\n title: string\n /** Optional one-line description. */\n description?: string\n /** Questions in declaration (default traversal) order. */\n questions: IntakeQuestion[]\n}\n\n/** The reason an answer failed validation. */\nexport type AnswerRejectionReason =\n | 'required'\n | 'wrong-type'\n | 'too-short'\n | 'too-long'\n | 'too-small'\n | 'too-large'\n | 'not-an-option'\n | 'invalid-url'\n | 'invalid-email'\n | 'unknown-question'\n\n/** Describe validation outcome of an answer including success status and optional rejection reason */\nexport interface AnswerValidationResult {\n ok: boolean\n reason?: AnswerRejectionReason\n}\n\nconst URL_PATTERN = /^https?:\\/\\/[^\\s/$.?#].[^\\s]*$/i\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** Look up a question by id, or null if the graph has none. */\nexport function getQuestion(graph: IntakeGraph, questionId: string): IntakeQuestion | null {\n return graph.questions.find((q) => q.id === questionId) ?? null\n}\n\n/** True when an answer value is present (not null/undefined/empty). */\nexport function hasAnswer(value: IntakeAnswerValue | undefined): boolean {\n if (value == null) return false\n if (typeof value === 'string') return value.trim().length > 0\n if (Array.isArray(value)) return value.length > 0\n return true\n}\n\n/**\n * Validate one answer against its question. Pure: no I/O, no graph mutation.\n * An empty value is rejected as `required` only when the question is required;\n * an empty value for an optional question is OK (the user skipped it).\n */\nexport function validateAnswer(\n question: IntakeQuestion,\n value: IntakeAnswerValue | undefined,\n): AnswerValidationResult {\n if (!hasAnswer(value)) {\n return question.required ? { ok: false, reason: 'required' } : { ok: true }\n }\n\n switch (question.type) {\n case 'text':\n case 'long-text': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n const len = value.trim().length\n if (question.min != null && len < question.min) return { ok: false, reason: 'too-short' }\n if (question.max != null && len > question.max) return { ok: false, reason: 'too-long' }\n return { ok: true }\n }\n case 'url': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n return URL_PATTERN.test(value.trim()) ? { ok: true } : { ok: false, reason: 'invalid-url' }\n }\n case 'email': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n return EMAIL_PATTERN.test(value.trim()) ? { ok: true } : { ok: false, reason: 'invalid-email' }\n }\n case 'number': {\n if (typeof value !== 'number' || !Number.isFinite(value)) return { ok: false, reason: 'wrong-type' }\n if (question.min != null && value < question.min) return { ok: false, reason: 'too-small' }\n if (question.max != null && value > question.max) return { ok: false, reason: 'too-large' }\n return { ok: true }\n }\n case 'boolean': {\n return typeof value === 'boolean' ? { ok: true } : { ok: false, reason: 'wrong-type' }\n }\n case 'single-select': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n return optionExists(question, value) ? { ok: true } : { ok: false, reason: 'not-an-option' }\n }\n case 'multi-select': {\n if (!Array.isArray(value)) return { ok: false, reason: 'wrong-type' }\n if (!value.every((v) => typeof v === 'string' && optionExists(question, v))) {\n return { ok: false, reason: 'not-an-option' }\n }\n if (question.min != null && value.length < question.min) return { ok: false, reason: 'too-short' }\n if (question.max != null && value.length > question.max) return { ok: false, reason: 'too-long' }\n return { ok: true }\n }\n default:\n return { ok: false, reason: 'wrong-type' }\n }\n}\n\n/**\n * The single question to ask next given the answers so far, or null when the\n * interview is done. Walks the graph from the first question, following each\n * question's `next` branch (or declaration order) and stopping at the first\n * reachable question that has no valid answer yet. Deterministic and pure.\n *\n * A `next` that points at a missing id, or returns null, ends the walk — so a\n * malformed graph terminates rather than looping. A visited-set guards against\n * a cyclic `next`.\n */\nexport function nextQuestion(graph: IntakeGraph, answers: IntakeAnswers): IntakeQuestion | null {\n if (graph.questions.length === 0) return null\n const visited = new Set<string>()\n let current: IntakeQuestion | null = graph.questions[0] ?? null\n\n while (current) {\n if (visited.has(current.id)) return null\n visited.add(current.id)\n\n const answer = answers[current.id]\n const validity = validateAnswer(current, answer)\n if (!validity.ok || (current.required && !hasAnswer(answer))) {\n return current\n }\n\n current = advance(graph, current, answers)\n }\n\n return null\n}\n\n/**\n * True when every REQUIRED question reachable under the current answers has a\n * valid answer. Pure: it replays the same traversal `nextQuestion` uses and is\n * complete exactly when that traversal has no question left to ask.\n */\nexport function isComplete(graph: IntakeGraph, answers: IntakeAnswers): boolean {\n return nextQuestion(graph, answers) === null\n}\n\n/**\n * Progress as answered-vs-total over the REACHABLE required questions under the\n * current answers — what a progress bar renders. Optional questions don't count\n * toward the denominator (they never block completion).\n */\nexport function intakeProgress(graph: IntakeGraph, answers: IntakeAnswers): { answered: number; total: number } {\n const reachable = reachableQuestions(graph, answers)\n const required = reachable.filter((q) => q.required)\n const answered = required.filter((q) => validateAnswer(q, answers[q.id]).ok && hasAnswer(answers[q.id])).length\n return { answered, total: required.length }\n}\n\n/** The questions reachable under the current answers, in traversal order. */\nexport function reachableQuestions(graph: IntakeGraph, answers: IntakeAnswers): IntakeQuestion[] {\n if (graph.questions.length === 0) return []\n const visited = new Set<string>()\n const out: IntakeQuestion[] = []\n let current: IntakeQuestion | null = graph.questions[0] ?? null\n\n while (current) {\n if (visited.has(current.id)) break\n visited.add(current.id)\n out.push(current)\n current = advance(graph, current, answers)\n }\n return out\n}\n\nfunction advance(graph: IntakeGraph, current: IntakeQuestion, answers: IntakeAnswers): IntakeQuestion | null {\n if (current.next) {\n const nextId = current.next(answers)\n return nextId == null ? null : getQuestion(graph, nextId)\n }\n const index = graph.questions.indexOf(current)\n return graph.questions[index + 1] ?? null\n}\n\nfunction optionExists(question: IntakeQuestion, value: string): boolean {\n return (question.options ?? []).some((option) => option.value === value)\n}\n"],"mappings":";AAwGA,IAAM,cAAc;AACpB,IAAM,gBAAgB;AAGf,SAAS,YAAY,OAAoB,YAA2C;AACzF,SAAO,MAAM,UAAU,KAAK,CAAC,MAAM,EAAE,OAAO,UAAU,KAAK;AAC7D;AAGO,SAAS,UAAU,OAA+C;AACvE,MAAI,SAAS,KAAM,QAAO;AAC1B,MAAI,OAAO,UAAU,SAAU,QAAO,MAAM,KAAK,EAAE,SAAS;AAC5D,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,SAAS;AAChD,SAAO;AACT;AAOO,SAAS,eACd,UACA,OACwB;AACxB,MAAI,CAAC,UAAU,KAAK,GAAG;AACrB,WAAO,SAAS,WAAW,EAAE,IAAI,OAAO,QAAQ,WAAW,IAAI,EAAE,IAAI,KAAK;AAAA,EAC5E;AAEA,UAAQ,SAAS,MAAM;AAAA,IACrB,KAAK;AAAA,IACL,KAAK,aAAa;AAChB,UAAI,OAAO,UAAU,SAAU,QAAO,EAAE,IAAI,OAAO,QAAQ,aAAa;AACxE,YAAM,MAAM,MAAM,KAAK,EAAE;AACzB,UAAI,SAAS,OAAO,QAAQ,MAAM,SAAS,IAAK,QAAO,EAAE,IAAI,OAAO,QAAQ,YAAY;AACxF,UAAI,SAAS,OAAO,QAAQ,MAAM,SAAS,IAAK,QAAO,EAAE,IAAI,OAAO,QAAQ,WAAW;AACvF,aAAO,EAAE,IAAI,KAAK;AAAA,IACpB;AAAA,IACA,KAAK,OAAO;AACV,UAAI,OAAO,UAAU,SAAU,QAAO,EAAE,IAAI,OAAO,QAAQ,aAAa;AACxE,aAAO,YAAY,KAAK,MAAM,KAAK,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,EAAE,IAAI,OAAO,QAAQ,cAAc;AAAA,IAC5F;AAAA,IACA,KAAK,SAAS;AACZ,UAAI,OAAO,UAAU,SAAU,QAAO,EAAE,IAAI,OAAO,QAAQ,aAAa;AACxE,aAAO,cAAc,KAAK,MAAM,KAAK,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,EAAE,IAAI,OAAO,QAAQ,gBAAgB;AAAA,IAChG;AAAA,IACA,KAAK,UAAU;AACb,UAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO,EAAE,IAAI,OAAO,QAAQ,aAAa;AACnG,UAAI,SAAS,OAAO,QAAQ,QAAQ,SAAS,IAAK,QAAO,EAAE,IAAI,OAAO,QAAQ,YAAY;AAC1F,UAAI,SAAS,OAAO,QAAQ,QAAQ,SAAS,IAAK,QAAO,EAAE,IAAI,OAAO,QAAQ,YAAY;AAC1F,aAAO,EAAE,IAAI,KAAK;AAAA,IACpB;AAAA,IACA,KAAK,WAAW;AACd,aAAO,OAAO,UAAU,YAAY,EAAE,IAAI,KAAK,IAAI,EAAE,IAAI,OAAO,QAAQ,aAAa;AAAA,IACvF;AAAA,IACA,KAAK,iBAAiB;AACpB,UAAI,OAAO,UAAU,SAAU,QAAO,EAAE,IAAI,OAAO,QAAQ,aAAa;AACxE,aAAO,aAAa,UAAU,KAAK,IAAI,EAAE,IAAI,KAAK,IAAI,EAAE,IAAI,OAAO,QAAQ,gBAAgB;AAAA,IAC7F;AAAA,IACA,KAAK,gBAAgB;AACnB,UAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO,EAAE,IAAI,OAAO,QAAQ,aAAa;AACpE,UAAI,CAAC,MAAM,MAAM,CAAC,MAAM,OAAO,MAAM,YAAY,aAAa,UAAU,CAAC,CAAC,GAAG;AAC3E,eAAO,EAAE,IAAI,OAAO,QAAQ,gBAAgB;AAAA,MAC9C;AACA,UAAI,SAAS,OAAO,QAAQ,MAAM,SAAS,SAAS,IAAK,QAAO,EAAE,IAAI,OAAO,QAAQ,YAAY;AACjG,UAAI,SAAS,OAAO,QAAQ,MAAM,SAAS,SAAS,IAAK,QAAO,EAAE,IAAI,OAAO,QAAQ,WAAW;AAChG,aAAO,EAAE,IAAI,KAAK;AAAA,IACpB;AAAA,IACA;AACE,aAAO,EAAE,IAAI,OAAO,QAAQ,aAAa;AAAA,EAC7C;AACF;AAYO,SAAS,aAAa,OAAoB,SAA+C;AAC9F,MAAI,MAAM,UAAU,WAAW,EAAG,QAAO;AACzC,QAAM,UAAU,oBAAI,IAAY;AAChC,MAAI,UAAiC,MAAM,UAAU,CAAC,KAAK;AAE3D,SAAO,SAAS;AACd,QAAI,QAAQ,IAAI,QAAQ,EAAE,EAAG,QAAO;AACpC,YAAQ,IAAI,QAAQ,EAAE;AAEtB,UAAM,SAAS,QAAQ,QAAQ,EAAE;AACjC,UAAM,WAAW,eAAe,SAAS,MAAM;AAC/C,QAAI,CAAC,SAAS,MAAO,QAAQ,YAAY,CAAC,UAAU,MAAM,GAAI;AAC5D,aAAO;AAAA,IACT;AAEA,cAAU,QAAQ,OAAO,SAAS,OAAO;AAAA,EAC3C;AAEA,SAAO;AACT;AAOO,SAAS,WAAW,OAAoB,SAAiC;AAC9E,SAAO,aAAa,OAAO,OAAO,MAAM;AAC1C;AAOO,SAAS,eAAe,OAAoB,SAA6D;AAC9G,QAAM,YAAY,mBAAmB,OAAO,OAAO;AACnD,QAAM,WAAW,UAAU,OAAO,CAAC,MAAM,EAAE,QAAQ;AACnD,QAAM,WAAW,SAAS,OAAO,CAAC,MAAM,eAAe,GAAG,QAAQ,EAAE,EAAE,CAAC,EAAE,MAAM,UAAU,QAAQ,EAAE,EAAE,CAAC,CAAC,EAAE;AACzG,SAAO,EAAE,UAAU,OAAO,SAAS,OAAO;AAC5C;AAGO,SAAS,mBAAmB,OAAoB,SAA0C;AAC/F,MAAI,MAAM,UAAU,WAAW,EAAG,QAAO,CAAC;AAC1C,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,MAAwB,CAAC;AAC/B,MAAI,UAAiC,MAAM,UAAU,CAAC,KAAK;AAE3D,SAAO,SAAS;AACd,QAAI,QAAQ,IAAI,QAAQ,EAAE,EAAG;AAC7B,YAAQ,IAAI,QAAQ,EAAE;AACtB,QAAI,KAAK,OAAO;AAChB,cAAU,QAAQ,OAAO,SAAS,OAAO;AAAA,EAC3C;AACA,SAAO;AACT;AAEA,SAAS,QAAQ,OAAoB,SAAyB,SAA+C;AAC3G,MAAI,QAAQ,MAAM;AAChB,UAAM,SAAS,QAAQ,KAAK,OAAO;AACnC,WAAO,UAAU,OAAO,OAAO,YAAY,OAAO,MAAM;AAAA,EAC1D;AACA,QAAM,QAAQ,MAAM,UAAU,QAAQ,OAAO;AAC7C,SAAO,MAAM,UAAU,QAAQ,CAAC,KAAK;AACvC;AAEA,SAAS,aAAa,UAA0B,OAAwB;AACtE,UAAQ,SAAS,WAAW,CAAC,GAAG,KAAK,CAAC,WAAW,OAAO,UAAU,KAAK;AACzE;","names":[]}