@tangle-network/agent-app 0.44.2 → 0.44.3

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.
@@ -5,7 +5,7 @@ import { ChatTurnIdentity, ChatTurnProducer } from '@tangle-network/agent-runtim
5
5
  import { InteractionAnswerRoute, InteractionAnswerRouteOptions } from '../interactions/index.js';
6
6
  import { PersistedChatMessageForTurn } from '../stream/index.js';
7
7
  import { T as TurnEventStore } from '../turn-buffer-BcPfhr1_.js';
8
- import { M as ModelFailoverAttempt } from '../failover-H0x12kY3.js';
8
+ import { M as ModelFailoverAttempt } from '../failover-D-3UXXTb.js';
9
9
  export { D as DEFAULT_STALE_TURN_LOCK_GRACE_MS, a as DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS, R as ReconcileStaleTurnLockOptions, b as ReconcileStaleTurnLockResult, S as StaleTurnLockSandboxProbeResult, c as StaleTurnLockSessionProbeResult, r as reconcileStaleTurnLock } from '../stale-turn-lock-DucQzvXu.js';
10
10
  import { J as JsonRecord } from '../stream-normalizer-DnuqkZvw.js';
11
11
  import { SandboxExecChannel, PromptInputPart } from '../sandbox/index.js';
@@ -87,7 +87,7 @@ import {
87
87
  buildModelChain,
88
88
  isUpstreamUnavailable,
89
89
  runWithModelFailover
90
- } from "../chunk-KFTRTOT2.js";
90
+ } from "../chunk-CHLOH4DG.js";
91
91
  import {
92
92
  flattenHistory,
93
93
  readSandboxBinaryBytes,
@@ -9,6 +9,7 @@ var UPSTREAM_UNAVAILABLE_CODES = [
9
9
  "service_unavailable"
10
10
  ];
11
11
  var UPSTREAM_UNAVAILABLE_STATUSES = [429, 500, 502, 503, 504];
12
+ var REQUEST_ERROR_STATUSES = [400, 401, 403, 405, 413, 422];
12
13
  var UPSTREAM_UNAVAILABLE_MESSAGES = [
13
14
  "bad gateway",
14
15
  "service unavailable",
@@ -19,8 +20,33 @@ var UPSTREAM_UNAVAILABLE_MESSAGES = [
19
20
  "quota exceeded",
20
21
  "rate limit",
21
22
  "overloaded",
22
- "temporarily unavailable"
23
+ "temporarily unavailable",
24
+ // Model-SCOPED unavailability. A different model in the chain can still
25
+ // serve, so this belongs here and not with the client errors. Measured on a
26
+ // real box 2026-07-27: the sandbox now reports an unservable model as a
27
+ // terminal `error` whose data is `{"message":"Session error:
28
+ // {\"error\":{\"name\":\"UnknownError\",\"data\":{\"message\":\"Model not
29
+ // found: openai-compat/zai/glm-4.7.\"}}}"}` — no `code`, no numeric status,
30
+ // and none of the fragments above. The shipped live product-path proof went
31
+ // red on exactly this: `usedFallback:false`, `failed:true`, an error row to
32
+ // the customer where a fallback was available and would have answered.
33
+ "model not found",
34
+ "is not currently available"
23
35
  ];
36
+ var HTTP_STATUS_HINT_PATTERNS = [
37
+ /\berror\s+code:?\s*([1-5]\d{2})\b/i,
38
+ /\bhttp(?:\/[\d.]+)?[\s:]\s*([1-5]\d{2})\b/i,
39
+ /\bstatus(?:\s*code)?[\s:=]\s*([1-5]\d{2})\b/i,
40
+ /\b(?:returned|responded(?:\s+with)?)\s+([1-5]\d{2})\b/i,
41
+ /\b([1-5]\d{2})\s+(?:bad\s+gateway|service\s+unavailable|gateway\s+time-?out|internal\s+server\s+error|too\s+many\s+requests)\b/i
42
+ ];
43
+ function readHttpStatusHint(text) {
44
+ for (const pattern of HTTP_STATUS_HINT_PATTERNS) {
45
+ const match = pattern.exec(text);
46
+ if (match?.[1]) return Number(match[1]);
47
+ }
48
+ return void 0;
49
+ }
24
50
  function readString(source, key) {
25
51
  const value = source[key];
26
52
  return typeof value === "string" && value.trim().length > 0 ? value : void 0;
@@ -39,6 +65,11 @@ function isUpstreamUnavailable(signal) {
39
65
  }
40
66
  const message = readString(record, "message") ?? readString(record, "error") ?? (nestedRecord ? readString(nestedRecord, "message") : void 0);
41
67
  if (!message) return false;
68
+ const hinted = readHttpStatusHint(message);
69
+ if (hinted !== void 0) {
70
+ if (UPSTREAM_UNAVAILABLE_STATUSES.includes(hinted)) return true;
71
+ if (REQUEST_ERROR_STATUSES.includes(hinted)) return false;
72
+ }
42
73
  const lowered = message.toLowerCase();
43
74
  return UPSTREAM_UNAVAILABLE_MESSAGES.some((fragment) => lowered.includes(fragment));
44
75
  }
@@ -103,9 +134,10 @@ function buildModelChain(preferred, fallbacks) {
103
134
  export {
104
135
  UPSTREAM_UNAVAILABLE_CODES,
105
136
  UPSTREAM_UNAVAILABLE_STATUSES,
137
+ readHttpStatusHint,
106
138
  isUpstreamUnavailable,
107
139
  ModelFailoverExhaustedError,
108
140
  runWithModelFailover,
109
141
  buildModelChain
110
142
  };
111
- //# sourceMappingURL=chunk-KFTRTOT2.js.map
143
+ //# sourceMappingURL=chunk-CHLOH4DG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/model-resolution/failover.ts"],"sourcesContent":["/**\n * Model failover — the answer to \"one upstream hit a quota wall and the customer\n * got a Bad Gateway even though the router had abundant healthy capacity\".\n *\n * This is deliberately NOT a health probe. Probing before every turn buys a\n * round-trip of latency on the happy path and still races the outage (a model\n * healthy at probe time can 502 a second later). Failover is REACTIVE: run the\n * preferred model, and on an upstream-outage signal move to the next model in\n * the chain. Zero added latency when the preferred model works.\n *\n * Two facts drive the design, both measured against a live box during the\n * 2026-07-25 Anthropic/DeepSeek outage:\n *\n * 1. An outage is NOT always a thrown error. The sandbox resolves with a\n * payload — `{ success: false, errorCode: 'provider_inference_unavailable' }`\n * — so a classifier that only inspects `catch` misses the customer-visible\n * case entirely. `isUpstreamUnavailable` inspects resolved values too.\n * 2. Catalog membership is NOT liveness. The router's `/v1/models` still listed\n * every dead model during the outage, so `validateChatModelId` admitted\n * `claude-sonnet-4-6` while every call to it returned 502. Nothing upstream\n * of the actual call can be trusted to tell you a model is serving.\n *\n * Substrate-free: the caller injects `run`, so this composes with a sandbox\n * turn, a router chat completion, or a test fake without importing any of them.\n */\n\n/**\n * Error codes that mean \"this model's upstream is unavailable — a different\n * model may still work\". Deliberately excludes codes that would fail identically\n * on every model (bad request, auth, content filter): retrying those down a\n * chain burns latency and money to reach the same failure.\n */\nexport const UPSTREAM_UNAVAILABLE_CODES: readonly string[] = [\n 'provider_inference_unavailable',\n 'upstream_unavailable',\n 'insufficient_quota',\n 'model_not_available',\n 'server_error',\n 'bad_gateway',\n 'service_unavailable',\n]\n\n/** HTTP statuses that indicate an upstream capacity/availability problem. */\nexport const UPSTREAM_UNAVAILABLE_STATUSES: readonly number[] = [429, 500, 502, 503, 504]\n\n/**\n * Statuses that mean the REQUEST is wrong, so every model in the chain would\n * fail on it identically. Listed explicitly rather than as \"any 4xx\" because\n * 404 is NOT one of them: a model the endpoint cannot find is model-scoped, and\n * the next model in the chain may well be there. 429 is absent for the same\n * reason in reverse — it is a capacity signal and lives in the list above.\n */\nconst REQUEST_ERROR_STATUSES: readonly number[] = [400, 401, 403, 405, 413, 422]\n\n/**\n * Message fragments emitted by real upstreams during this class of outage.\n * Matched case-insensitively as a last resort, after code and status.\n */\nconst UPSTREAM_UNAVAILABLE_MESSAGES: readonly string[] = [\n 'bad gateway',\n 'service unavailable',\n 'inference temporarily unavailable',\n 'provider inference is unavailable',\n 'insufficient balance',\n 'usage limits',\n 'quota exceeded',\n 'rate limit',\n 'overloaded',\n 'temporarily unavailable',\n // Model-SCOPED unavailability. A different model in the chain can still\n // serve, so this belongs here and not with the client errors. Measured on a\n // real box 2026-07-27: the sandbox now reports an unservable model as a\n // terminal `error` whose data is `{\"message\":\"Session error:\n // {\\\"error\\\":{\\\"name\\\":\\\"UnknownError\\\",\\\"data\\\":{\\\"message\\\":\\\"Model not\n // found: openai-compat/zai/glm-4.7.\\\"}}}\"}` — no `code`, no numeric status,\n // and none of the fragments above. The shipped live product-path proof went\n // red on exactly this: `usedFallback:false`, `failed:true`, an error row to\n // the customer where a fallback was available and would have answered.\n 'model not found',\n 'is not currently available',\n]\n\n/**\n * Textual forms an HTTP status arrives in when NO numeric field carries it.\n *\n * The case that forced this is the Cloudflare EDGE 502. Captured verbatim from\n * `router.tangle.tools` on 2026-07-27 17:48:12 GMT (cf-ray\n * `a21d793899f6cef3-DEN`): `HTTP/2 502`, `content-type: text/plain;\n * charset=UTF-8`, a 16-byte body reading `error code: 502\\n`, `server:\n * cloudflare`, and **zero `x-tangle-*` headers** where a healthy 200 from the\n * same endpoint carries nine of them. Zero means the origin never executed —\n * so the router's own upstream failover, which lives inside the origin,\n * structurally cannot fire. Only a client OUTSIDE the edge can rescue that\n * turn, which is why this classifier has to recognize a failure shape that\n * nobody upstream will ever label for it.\n *\n * Every pattern below matches a string a real producer emits:\n * - `error code: 502` — Cloudflare's edge body (the capture above).\n * - `(HTTP 502)` — `src/runtime/openai-stream.ts` wrapping a non-ok response.\n * - `returned 502` — `src/runtime/model-catalog.ts`.\n * - `failed with status 502` — `src/turn-stream/adapters.ts`.\n * - `502 Bad Gateway` — an HTTP/1.1 status line. HTTP/2 carries no reason\n * phrase at all, which is precisely why matching the phrase alone (the\n * `'bad gateway'` fragment below) read the capture as a non-outage.\n */\nconst HTTP_STATUS_HINT_PATTERNS: readonly RegExp[] = [\n /\\berror\\s+code:?\\s*([1-5]\\d{2})\\b/i,\n /\\bhttp(?:\\/[\\d.]+)?[\\s:]\\s*([1-5]\\d{2})\\b/i,\n /\\bstatus(?:\\s*code)?[\\s:=]\\s*([1-5]\\d{2})\\b/i,\n /\\b(?:returned|responded(?:\\s+with)?)\\s+([1-5]\\d{2})\\b/i,\n /\\b([1-5]\\d{2})\\s+(?:bad\\s+gateway|service\\s+unavailable|gateway\\s+time-?out|internal\\s+server\\s+error|too\\s+many\\s+requests)\\b/i,\n]\n\n/**\n * The HTTP status a message states in prose, or `undefined` when it states\n * none. Deliberately NOT \"any three digits in the string\" — an unanchored digit\n * scan would read a token count or a duration as a status.\n */\nexport function readHttpStatusHint(text: string): number | undefined {\n for (const pattern of HTTP_STATUS_HINT_PATTERNS) {\n const match = pattern.exec(text)\n if (match?.[1]) return Number(match[1])\n }\n return undefined\n}\n\nfunction readString(source: Record<string, unknown>, key: string): string | undefined {\n const value = source[key]\n return typeof value === 'string' && value.trim().length > 0 ? value : undefined\n}\n\n/**\n * True when `signal` — a thrown error OR a resolved result payload — indicates\n * the model's upstream is unavailable and another model is worth trying.\n *\n * Checked in order of decreasing confidence: explicit code, HTTP status, then\n * message text. A resolved payload only counts as a failure when it carries an\n * explicit failure marker (`success: false`, or an `error`/`errorCode` field) —\n * a successful result is never misread as an outage.\n */\nexport function isUpstreamUnavailable(signal: unknown): boolean {\n if (signal === null || typeof signal !== 'object') return false\n const record = signal as Record<string, unknown>\n\n // A resolved payload that explicitly reports success is never an outage.\n if (record.success === true) return false\n\n const nested = record.error\n const nestedRecord = nested !== null && typeof nested === 'object' ? (nested as Record<string, unknown>) : undefined\n\n const code =\n readString(record, 'errorCode') ??\n readString(record, 'code') ??\n (nestedRecord ? (readString(nestedRecord, 'code') ?? readString(nestedRecord, 'type')) : undefined)\n if (code && UPSTREAM_UNAVAILABLE_CODES.includes(code)) return true\n\n for (const key of ['status', 'statusCode', 'httpStatus']) {\n const value = record[key]\n if (typeof value === 'number' && UPSTREAM_UNAVAILABLE_STATUSES.includes(value)) return true\n }\n\n const message =\n readString(record, 'message') ??\n readString(record, 'error') ??\n (nestedRecord ? readString(nestedRecord, 'message') : undefined)\n if (!message) return false\n\n // A status carried as PROSE, checked before the fragment list because it is\n // the stronger signal and it cuts BOTH ways. An edge 502 names no exception\n // type and sets no numeric field, so without this the shipped classifier read\n // `error code: 502` as a non-outage and handed the customer a Bad Gateway\n // instead of trying the next model — the exact failure the router cannot\n // rescue from inside its own origin.\n const hinted = readHttpStatusHint(message)\n if (hinted !== undefined) {\n if (UPSTREAM_UNAVAILABLE_STATUSES.includes(hinted)) return true\n // An explicit REQUEST-error status is decisive the OTHER way. A 400\n // carrying a validation message must surface rather than walk the chain,\n // even when its body happens to contain a word from the fragment list —\n // the router's own `Function tools with reasoning_effort are not supported`\n // 400 is a request-shaping bug that fails identically on every model.\n if (REQUEST_ERROR_STATUSES.includes(hinted)) return false\n }\n\n const lowered = message.toLowerCase()\n return UPSTREAM_UNAVAILABLE_MESSAGES.some((fragment) => lowered.includes(fragment))\n}\n\n/** One model tried, and how it went. */\nexport interface ModelFailoverAttempt {\n model: string\n ok: boolean\n /** Why this model was abandoned. Absent when `ok`. */\n reason?: string\n}\n\n/** The outcome of a failover run: the value plus the full attempt trail. */\nexport interface ModelFailoverResult<T> {\n value: T\n /** The model that actually produced `value`. */\n model: string\n attempts: ModelFailoverAttempt[]\n /** True when the preferred (first) model did not serve the request. */\n usedFallback: boolean\n}\n\n/** Every model in the chain failed; carries the trail for logging. */\nexport class ModelFailoverExhaustedError extends Error {\n readonly attempts: ModelFailoverAttempt[]\n constructor(attempts: ModelFailoverAttempt[]) {\n const trail = attempts.map((a) => `${a.model}: ${a.reason ?? 'failed'}`).join(' | ')\n super(`All ${attempts.length} model(s) failed. ${trail}`)\n this.name = 'ModelFailoverExhaustedError'\n this.attempts = attempts\n }\n}\n\n/** Inputs to {@link runWithModelFailover}. */\nexport interface RunWithModelFailoverInput<T> {\n /** Preferred model first, then fallbacks in descending preference. */\n models: readonly string[]\n /** Executes one turn with the given model. */\n run: (model: string) => Promise<T>\n /**\n * Classifies a resolved result as an upstream outage. Defaults to\n * {@link isUpstreamUnavailable}, which understands the sandbox's\n * `{ success: false, errorCode }` payload.\n */\n isUnavailableResult?: (result: T) => boolean\n /** Classifies a thrown error. Defaults to {@link isUpstreamUnavailable}. */\n isUnavailableError?: (error: unknown) => boolean\n /** Observability hook fired each time a model is abandoned. */\n onFallback?: (attempt: ModelFailoverAttempt, nextModel: string) => void\n}\n\nfunction describe(signal: unknown): string {\n if (signal instanceof Error) return signal.message\n if (signal !== null && typeof signal === 'object') {\n const record = signal as Record<string, unknown>\n const message = readString(record, 'error') ?? readString(record, 'message') ?? readString(record, 'errorCode')\n if (message) return message\n }\n return String(signal)\n}\n\n/**\n * Run `run` against the first model in `models` that does not report an upstream\n * outage, falling through the chain in order.\n *\n * A non-outage failure (bad request, auth, content filter) is re-thrown\n * immediately rather than retried down the chain — those fail identically on\n * every model, so walking the chain would only multiply latency and spend.\n *\n * @throws ModelFailoverExhaustedError when every model reports an outage.\n */\nexport async function runWithModelFailover<T>(\n input: RunWithModelFailoverInput<T>,\n): Promise<ModelFailoverResult<T>> {\n const models = input.models.map((m) => m.trim()).filter((m) => m.length > 0)\n if (models.length === 0) throw new Error('runWithModelFailover requires at least one model')\n\n const isUnavailableResult = input.isUnavailableResult ?? ((r: T) => isUpstreamUnavailable(r))\n const isUnavailableError = input.isUnavailableError ?? isUpstreamUnavailable\n const attempts: ModelFailoverAttempt[] = []\n\n for (let index = 0; index < models.length; index += 1) {\n const model = models[index]!\n let result: T\n try {\n result = await input.run(model)\n } catch (error) {\n if (!isUnavailableError(error)) throw error\n const attempt: ModelFailoverAttempt = { model, ok: false, reason: describe(error) }\n attempts.push(attempt)\n const next = models[index + 1]\n if (next) input.onFallback?.(attempt, next)\n continue\n }\n\n if (isUnavailableResult(result)) {\n const attempt: ModelFailoverAttempt = { model, ok: false, reason: describe(result) }\n attempts.push(attempt)\n const next = models[index + 1]\n if (next) input.onFallback?.(attempt, next)\n continue\n }\n\n attempts.push({ model, ok: true })\n return { value: result, model, attempts, usedFallback: index > 0 }\n }\n\n throw new ModelFailoverExhaustedError(attempts)\n}\n\n/**\n * Build a failover chain: the preferred model first, then `fallbacks`, with\n * duplicates removed so a model is never retried twice in one turn.\n */\nexport function buildModelChain(preferred: string, fallbacks: readonly string[]): string[] {\n const chain: string[] = []\n for (const model of [preferred, ...fallbacks]) {\n const cleaned = typeof model === 'string' ? model.trim() : ''\n if (cleaned.length > 0 && !chain.includes(cleaned)) chain.push(cleaned)\n }\n return chain\n}\n"],"mappings":"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","names":[]}
@@ -32,6 +32,12 @@
32
32
  declare const UPSTREAM_UNAVAILABLE_CODES: readonly string[];
33
33
  /** HTTP statuses that indicate an upstream capacity/availability problem. */
34
34
  declare const UPSTREAM_UNAVAILABLE_STATUSES: readonly number[];
35
+ /**
36
+ * The HTTP status a message states in prose, or `undefined` when it states
37
+ * none. Deliberately NOT "any three digits in the string" — an unanchored digit
38
+ * scan would read a token count or a duration as a status.
39
+ */
40
+ declare function readHttpStatusHint(text: string): number | undefined;
35
41
  /**
36
42
  * True when `signal` — a thrown error OR a resolved result payload — indicates
37
43
  * the model's upstream is unavailable and another model is worth trying.
@@ -97,4 +103,4 @@ declare function runWithModelFailover<T>(input: RunWithModelFailoverInput<T>): P
97
103
  */
98
104
  declare function buildModelChain(preferred: string, fallbacks: readonly string[]): string[];
99
105
 
100
- export { type ModelFailoverAttempt as M, type RunWithModelFailoverInput as R, UPSTREAM_UNAVAILABLE_CODES as U, ModelFailoverExhaustedError as a, type ModelFailoverResult as b, UPSTREAM_UNAVAILABLE_STATUSES as c, buildModelChain as d, isUpstreamUnavailable as i, runWithModelFailover as r };
106
+ export { type ModelFailoverAttempt as M, type RunWithModelFailoverInput as R, UPSTREAM_UNAVAILABLE_CODES as U, ModelFailoverExhaustedError as a, type ModelFailoverResult as b, UPSTREAM_UNAVAILABLE_STATUSES as c, buildModelChain as d, runWithModelFailover as e, isUpstreamUnavailable as i, readHttpStatusHint as r };
@@ -1,4 +1,4 @@
1
- export { M as ModelFailoverAttempt, a as ModelFailoverExhaustedError, b as ModelFailoverResult, R as RunWithModelFailoverInput, U as UPSTREAM_UNAVAILABLE_CODES, c as UPSTREAM_UNAVAILABLE_STATUSES, d as buildModelChain, i as isUpstreamUnavailable, r as runWithModelFailover } from '../failover-H0x12kY3.js';
1
+ export { M as ModelFailoverAttempt, a as ModelFailoverExhaustedError, b as ModelFailoverResult, R as RunWithModelFailoverInput, U as UPSTREAM_UNAVAILABLE_CODES, c as UPSTREAM_UNAVAILABLE_STATUSES, d as buildModelChain, i as isUpstreamUnavailable, r as readHttpStatusHint, e as runWithModelFailover } from '../failover-D-3UXXTb.js';
2
2
 
3
3
  /**
4
4
  * Canonical chat-model resolution — identical across every agent app.
@@ -4,8 +4,9 @@ import {
4
4
  UPSTREAM_UNAVAILABLE_STATUSES,
5
5
  buildModelChain,
6
6
  isUpstreamUnavailable,
7
+ readHttpStatusHint,
7
8
  runWithModelFailover
8
- } from "../chunk-KFTRTOT2.js";
9
+ } from "../chunk-CHLOH4DG.js";
9
10
 
10
11
  // src/model-resolution/index.ts
11
12
  function canonicalModelId(model) {
@@ -84,6 +85,7 @@ export {
84
85
  cleanModelId,
85
86
  isUpstreamUnavailable,
86
87
  isWellFormedModelId,
88
+ readHttpStatusHint,
87
89
  resolveChatModel,
88
90
  runWithModelFailover,
89
91
  validateChatModelId
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/model-resolution/index.ts"],"sourcesContent":["/**\n * Canonical chat-model resolution — identical across every agent app.\n *\n * The ONLY per-app inputs are DATA, never logic: the default model, the\n * allowlist, the env value the deployment set, and the catalog-fetch loader.\n * The logic is one precedence ladder + one fail-closed validator that every\n * product uses the same way — there is no per-product variant, no env-var name\n * baked in, and no backend dimension (router-vs-sandbox is the harness/dispatch\n * concern, not model resolution; a sandbox's provider default lives in the\n * sandbox subpath).\n *\n * - resolveChatModel: request > workspace > env > default. The product reads its\n * own deploy env var and passes the VALUE as `envModel`; the shell knows no\n * env-var names. Source is canonical: 'request' | 'workspace' | 'env' | 'default'.\n * - validateChatModelId: fail-closed. Admit an id that is in the allowlist, or\n * equals the operator-set env model, or is served by the live router catalog\n * (exact, or a bare id resolved to its canonical id when the suffix is unique).\n */\n\n/** The router /v1/models entry shape this module reads. Minimal on purpose. */\nexport interface ModelInfo {\n id: string\n name?: string\n _provider?: string\n provider?: string\n}\n\n/** Canonical (provider-prefixed) id for a catalog entry: pass through an id that\n * already carries a provider, else prefix the entry's provider when present. */\nfunction canonicalModelId(model: ModelInfo): string {\n if (model.id.includes('/')) return model.id\n const provider = model._provider ?? model.provider\n return provider ? `${provider}/${model.id}` : model.id\n}\n\n/** Define possible origins for the chat model configuration values */\nexport type ChatModelSource = 'request' | 'workspace' | 'env' | 'default'\n\n/** Resolve a chat model with its identifier and source information */\nexport interface ResolvedChatModel {\n model: string\n source: ChatModelSource\n}\n\n/** Represent successful chat model validation with a true status and a validated string value */\nexport interface ChatModelValidationSuccess {\n succeeded: true\n value: string\n}\n\n/** Describe a failed chat model validation result with an error message */\nexport interface ChatModelValidationFailure {\n succeeded: false\n error: string\n}\n\n/** Resolve the outcome of validating a chat model as either success or failure */\nexport type ChatModelValidationResult = ChatModelValidationSuccess | ChatModelValidationFailure\n\n/** The catalog-fetch boundary: maps a router base URL to the raw model list. */\nexport type LoadModels = (routerBaseUrl: string) => Promise<ModelInfo[]>\n\n/** Resolve the effective chat model input by prioritizing request, workspace, environment, and default models */\nexport interface ResolveChatModelInput {\n /** Per-request override (highest precedence). */\n requestModel?: string\n /** Persisted workspace-pinned model. */\n workspaceModel?: string\n /** The value the deployment's model env var holds (the product reads its own\n * var name and passes the value — the shell stays env-var-name agnostic). */\n envModel?: string\n /** Final fallback (the product's default, typically profile.model.default). */\n defaultModel: string\n}\n\n/** Resolve the chat-turn model by the one canonical precedence. Blank values are\n * treated as absent. */\nexport function resolveChatModel(input: ResolveChatModelInput): ResolvedChatModel {\n const request = cleanModelId(input.requestModel)\n if (request) return { model: request, source: 'request' }\n const workspace = cleanModelId(input.workspaceModel)\n if (workspace) return { model: workspace, source: 'workspace' }\n const env = cleanModelId(input.envModel)\n if (env) return { model: env, source: 'env' }\n return { model: input.defaultModel, source: 'default' }\n}\n\n/** Define input parameters for validating chat model IDs with optional allowlist and catalog access details */\nexport interface ValidateChatModelIdInput {\n /** Ids accepted without a catalog round-trip (defaults + operator-trusted). */\n allowlist?: Iterable<string>\n /** The operator-set env model value — always admitted (operator-trusted). */\n envModel?: string\n /** Catalog loader; required to reach the catalog path. */\n loadModels?: LoadModels\n /** Catalog endpoint base; required to reach the catalog path. */\n routerBaseUrl?: string\n}\n\n/**\n * Fail-closed model-id validation. Accepts an id only when it is well-formed AND\n * (in the allowlist, or equals the operator-set env model, or served by the live\n * catalog). A bare id (no provider prefix) resolves to its canonical id only when\n * the suffix is unique across the catalog — an ambiguous suffix is rejected\n * rather than silently assigned a provider.\n */\nexport async function validateChatModelId(\n modelId: unknown,\n input: ValidateChatModelIdInput,\n): Promise<ChatModelValidationResult> {\n const cleaned = cleanModelId(modelId)\n if (!cleaned) return { succeeded: false, error: 'Model id must be a non-empty string.' }\n if (!isWellFormedModelId(cleaned)) return { succeeded: false, error: `Model id is malformed: ${cleaned}` }\n\n const allowed = new Set(input.allowlist ?? [])\n if (allowed.has(cleaned)) return { succeeded: true, value: cleaned }\n\n // The operator-set env model is trusted without a catalog round-trip.\n if (cleanModelId(input.envModel) === cleaned) return { succeeded: true, value: cleaned }\n\n if (!input.loadModels || typeof input.routerBaseUrl !== 'string' || input.routerBaseUrl.length === 0) {\n return { succeeded: false, error: `Model is not available: ${cleaned}` }\n }\n\n let catalog: ModelInfo[]\n try {\n catalog = await input.loadModels(input.routerBaseUrl)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return { succeeded: false, error: `Could not validate model catalog: ${message}` }\n }\n\n const ids = new Set(catalog.flatMap(catalogIdsForModel))\n if (ids.has(cleaned)) return { succeeded: true, value: cleaned }\n\n if (!cleaned.includes('/')) {\n const canonicalBySuffix = new Map<string, string[]>()\n for (const model of catalog) {\n if (typeof model.id !== 'string' || !model.id.trim()) continue\n const canonical = canonicalModelId(model)\n if (!canonical.includes('/')) continue\n const suffix = canonical.split('/').slice(1).join('/')\n const entries = canonicalBySuffix.get(suffix)\n if (entries) entries.push(canonical)\n else canonicalBySuffix.set(suffix, [canonical])\n }\n const matches = canonicalBySuffix.get(cleaned)\n if (matches && matches.length === 1) return { succeeded: true, value: matches[0]! }\n }\n\n return { succeeded: false, error: `Model is not available: ${cleaned}` }\n}\n\n/** Resolve and return a trimmed string model ID or undefined for invalid or empty input */\nexport function cleanModelId(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\n/** Validate if a model ID string conforms to length and character format requirements */\nexport function isWellFormedModelId(modelId: string): boolean {\n if (modelId.length > 200) return false\n return /^[A-Za-z0-9._/@:-]+$/.test(modelId)\n}\n\n/** Resolve unique catalog IDs associated with a given model including its canonical form if applicable */\nexport function catalogIdsForModel(model: ModelInfo): string[] {\n const ids = new Set<string>()\n if (typeof model.id === 'string' && model.id.trim()) ids.add(model.id.trim())\n if (typeof model.id === 'string' && model.id.trim() && !model.id.includes('/')) {\n const canonical = canonicalModelId(model)\n if (canonical.includes('/')) ids.add(canonical)\n }\n return [...ids]\n}\n\n// Reactive failover for the case validation cannot catch: a model that IS in the\n// catalog but whose upstream is down. Catalog membership is not liveness.\nexport {\n isUpstreamUnavailable,\n runWithModelFailover,\n buildModelChain,\n ModelFailoverExhaustedError,\n UPSTREAM_UNAVAILABLE_CODES,\n UPSTREAM_UNAVAILABLE_STATUSES,\n type ModelFailoverAttempt,\n type ModelFailoverResult,\n type RunWithModelFailoverInput,\n} from './failover'\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/model-resolution/index.ts"],"sourcesContent":["/**\n * Canonical chat-model resolution — identical across every agent app.\n *\n * The ONLY per-app inputs are DATA, never logic: the default model, the\n * allowlist, the env value the deployment set, and the catalog-fetch loader.\n * The logic is one precedence ladder + one fail-closed validator that every\n * product uses the same way — there is no per-product variant, no env-var name\n * baked in, and no backend dimension (router-vs-sandbox is the harness/dispatch\n * concern, not model resolution; a sandbox's provider default lives in the\n * sandbox subpath).\n *\n * - resolveChatModel: request > workspace > env > default. The product reads its\n * own deploy env var and passes the VALUE as `envModel`; the shell knows no\n * env-var names. Source is canonical: 'request' | 'workspace' | 'env' | 'default'.\n * - validateChatModelId: fail-closed. Admit an id that is in the allowlist, or\n * equals the operator-set env model, or is served by the live router catalog\n * (exact, or a bare id resolved to its canonical id when the suffix is unique).\n */\n\n/** The router /v1/models entry shape this module reads. Minimal on purpose. */\nexport interface ModelInfo {\n id: string\n name?: string\n _provider?: string\n provider?: string\n}\n\n/** Canonical (provider-prefixed) id for a catalog entry: pass through an id that\n * already carries a provider, else prefix the entry's provider when present. */\nfunction canonicalModelId(model: ModelInfo): string {\n if (model.id.includes('/')) return model.id\n const provider = model._provider ?? model.provider\n return provider ? `${provider}/${model.id}` : model.id\n}\n\n/** Define possible origins for the chat model configuration values */\nexport type ChatModelSource = 'request' | 'workspace' | 'env' | 'default'\n\n/** Resolve a chat model with its identifier and source information */\nexport interface ResolvedChatModel {\n model: string\n source: ChatModelSource\n}\n\n/** Represent successful chat model validation with a true status and a validated string value */\nexport interface ChatModelValidationSuccess {\n succeeded: true\n value: string\n}\n\n/** Describe a failed chat model validation result with an error message */\nexport interface ChatModelValidationFailure {\n succeeded: false\n error: string\n}\n\n/** Resolve the outcome of validating a chat model as either success or failure */\nexport type ChatModelValidationResult = ChatModelValidationSuccess | ChatModelValidationFailure\n\n/** The catalog-fetch boundary: maps a router base URL to the raw model list. */\nexport type LoadModels = (routerBaseUrl: string) => Promise<ModelInfo[]>\n\n/** Resolve the effective chat model input by prioritizing request, workspace, environment, and default models */\nexport interface ResolveChatModelInput {\n /** Per-request override (highest precedence). */\n requestModel?: string\n /** Persisted workspace-pinned model. */\n workspaceModel?: string\n /** The value the deployment's model env var holds (the product reads its own\n * var name and passes the value — the shell stays env-var-name agnostic). */\n envModel?: string\n /** Final fallback (the product's default, typically profile.model.default). */\n defaultModel: string\n}\n\n/** Resolve the chat-turn model by the one canonical precedence. Blank values are\n * treated as absent. */\nexport function resolveChatModel(input: ResolveChatModelInput): ResolvedChatModel {\n const request = cleanModelId(input.requestModel)\n if (request) return { model: request, source: 'request' }\n const workspace = cleanModelId(input.workspaceModel)\n if (workspace) return { model: workspace, source: 'workspace' }\n const env = cleanModelId(input.envModel)\n if (env) return { model: env, source: 'env' }\n return { model: input.defaultModel, source: 'default' }\n}\n\n/** Define input parameters for validating chat model IDs with optional allowlist and catalog access details */\nexport interface ValidateChatModelIdInput {\n /** Ids accepted without a catalog round-trip (defaults + operator-trusted). */\n allowlist?: Iterable<string>\n /** The operator-set env model value — always admitted (operator-trusted). */\n envModel?: string\n /** Catalog loader; required to reach the catalog path. */\n loadModels?: LoadModels\n /** Catalog endpoint base; required to reach the catalog path. */\n routerBaseUrl?: string\n}\n\n/**\n * Fail-closed model-id validation. Accepts an id only when it is well-formed AND\n * (in the allowlist, or equals the operator-set env model, or served by the live\n * catalog). A bare id (no provider prefix) resolves to its canonical id only when\n * the suffix is unique across the catalog — an ambiguous suffix is rejected\n * rather than silently assigned a provider.\n */\nexport async function validateChatModelId(\n modelId: unknown,\n input: ValidateChatModelIdInput,\n): Promise<ChatModelValidationResult> {\n const cleaned = cleanModelId(modelId)\n if (!cleaned) return { succeeded: false, error: 'Model id must be a non-empty string.' }\n if (!isWellFormedModelId(cleaned)) return { succeeded: false, error: `Model id is malformed: ${cleaned}` }\n\n const allowed = new Set(input.allowlist ?? [])\n if (allowed.has(cleaned)) return { succeeded: true, value: cleaned }\n\n // The operator-set env model is trusted without a catalog round-trip.\n if (cleanModelId(input.envModel) === cleaned) return { succeeded: true, value: cleaned }\n\n if (!input.loadModels || typeof input.routerBaseUrl !== 'string' || input.routerBaseUrl.length === 0) {\n return { succeeded: false, error: `Model is not available: ${cleaned}` }\n }\n\n let catalog: ModelInfo[]\n try {\n catalog = await input.loadModels(input.routerBaseUrl)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return { succeeded: false, error: `Could not validate model catalog: ${message}` }\n }\n\n const ids = new Set(catalog.flatMap(catalogIdsForModel))\n if (ids.has(cleaned)) return { succeeded: true, value: cleaned }\n\n if (!cleaned.includes('/')) {\n const canonicalBySuffix = new Map<string, string[]>()\n for (const model of catalog) {\n if (typeof model.id !== 'string' || !model.id.trim()) continue\n const canonical = canonicalModelId(model)\n if (!canonical.includes('/')) continue\n const suffix = canonical.split('/').slice(1).join('/')\n const entries = canonicalBySuffix.get(suffix)\n if (entries) entries.push(canonical)\n else canonicalBySuffix.set(suffix, [canonical])\n }\n const matches = canonicalBySuffix.get(cleaned)\n if (matches && matches.length === 1) return { succeeded: true, value: matches[0]! }\n }\n\n return { succeeded: false, error: `Model is not available: ${cleaned}` }\n}\n\n/** Resolve and return a trimmed string model ID or undefined for invalid or empty input */\nexport function cleanModelId(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\n/** Validate if a model ID string conforms to length and character format requirements */\nexport function isWellFormedModelId(modelId: string): boolean {\n if (modelId.length > 200) return false\n return /^[A-Za-z0-9._/@:-]+$/.test(modelId)\n}\n\n/** Resolve unique catalog IDs associated with a given model including its canonical form if applicable */\nexport function catalogIdsForModel(model: ModelInfo): string[] {\n const ids = new Set<string>()\n if (typeof model.id === 'string' && model.id.trim()) ids.add(model.id.trim())\n if (typeof model.id === 'string' && model.id.trim() && !model.id.includes('/')) {\n const canonical = canonicalModelId(model)\n if (canonical.includes('/')) ids.add(canonical)\n }\n return [...ids]\n}\n\n// Reactive failover for the case validation cannot catch: a model that IS in the\n// catalog but whose upstream is down. Catalog membership is not liveness.\nexport {\n isUpstreamUnavailable,\n readHttpStatusHint,\n runWithModelFailover,\n buildModelChain,\n ModelFailoverExhaustedError,\n UPSTREAM_UNAVAILABLE_CODES,\n UPSTREAM_UNAVAILABLE_STATUSES,\n type ModelFailoverAttempt,\n type ModelFailoverResult,\n type RunWithModelFailoverInput,\n} from './failover'\n"],"mappings":"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","names":[]}
@@ -84,7 +84,9 @@ async function* streamChatCompletions(doFetch, url, apiKey, body) {
84
84
  });
85
85
  if (!res.ok || !res.body) {
86
86
  const text = res.body ? await res.text().catch(() => "") : "";
87
- throw new Error(`OpenAI-compat stream failed (HTTP ${res.status})${text ? `: ${text.slice(0, 200)}` : ""}`);
87
+ const error = new Error(`OpenAI-compat stream failed (HTTP ${res.status})${text ? `: ${text.slice(0, 200)}` : ""}`);
88
+ Object.assign(error, { status: res.status });
89
+ throw error;
88
90
  }
89
91
  const reader = res.body.getReader();
90
92
  const decoder = new TextDecoder();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/runtime/openai-stream.ts","../../src/runtime/certified-delivery.ts","../../src/runtime/surface-profile.ts","../../src/runtime/loop.ts"],"sourcesContent":["/**\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'\n\n/** Minimal OpenAI Chat Completions streaming chunk (structural — no `openai` dep). */\nexport interface OpenAIStreamChunk {\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 }>\n }\n finish_reason?: string | null\n }>\n /** Final-chunk token accounting (requires `stream_options.include_usage`). */\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}\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 */\nexport async function* toLoopEvents(chunks: AsyncIterable<OpenAIStreamChunk>): AsyncIterable<LoopEvent> {\n const calls = new Map<number, PartialToolCall>()\n for await (const chunk of chunks) {\n // Usage rides the final chunk, which has an empty choices array — handle\n // it before the choice guard.\n if (chunk.usage?.prompt_tokens != null || chunk.usage?.completion_tokens != null) {\n yield {\n type: 'usage',\n usage: {\n promptTokens: chunk.usage.prompt_tokens ?? 0,\n completionTokens: chunk.usage.completion_tokens ?? 0,\n },\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: '' }\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 calls.set(tc.index, cur)\n }\n }\n for (const [, c] of [...calls.entries()].sort((a, b) => a[0] - b[0])) {\n if (!c.name) continue\n yield { type: 'tool_call', call: { toolCallId: c.id, toolName: c.name, args: safeParse(c.args) } satisfies LoopToolCall }\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/** 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}\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 }),\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): AsyncIterable<OpenAIStreamChunk> {\n const res = await doFetch(url, {\n method: 'POST',\n headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', Accept: 'text/event-stream' },\n body: JSON.stringify(body),\n })\n if (!res.ok || !res.body) {\n const text = res.body ? await res.text().catch(() => '') : ''\n throw new Error(`OpenAI-compat stream failed (HTTP ${res.status})${text ? `: ${text.slice(0, 200)}` : ''}`)\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 try {\n yield JSON.parse(data) as OpenAIStreamChunk\n } catch {\n /* skip a partial/garbled SSE frame */\n }\n }\n }\n}\n","/**\n * `createCertifiedDelivery` — the delivery truck for Tangle Intelligence.\n *\n * Pulls a tenant's CERTIFIED `AgentProfile` from the deployed plane\n * (`GET /v1/profiles/:target/composed`) and applies it to the agent's resolved\n * surfaces each turn, so an approved improvement reaches the running agent with\n * NO redeploy. This is the `composeProfile` transform `createAgentRuntime`\n * accepts — opt-in per product, fail-closed, cached + refreshed.\n *\n * Profile-WIDE by design (not prompt-only): the composed profile carries every\n * promoted artifact type keyed by kind. What's folded where:\n * - `prompt-surface` + `skill` → the system prompt (via `composeCertifiedPrompt`).\n * - `tool` artifacts that carry an OpenAI tool definition → `extraTools`\n * (advertised to the model; the matching executor is supplied by the product\n * via `executeOtherTool`). Until a `tool` artifact carries a runnable def,\n * it is surfaced (see `current()`) but not advertised — advertising a tool\n * with no executor would make the model call into a dead end.\n * - `mcp` / `memory` / `rag` artifacts materialize as servers/files and deliver\n * through the SANDBOX-provisioning seam, not this in-process one. The full\n * certified profile is exposed via `current()` so that seam can consume it.\n *\n * Substrate boundary: THIS module imports `@tangle-network/agent-runtime`; the\n * `createAgentRuntime` core does not (it only consumes the generic transform).\n */\n\nimport {\n composeCertifiedPrompt,\n type CertifiedProfile,\n pullCertified,\n} from '@tangle-network/agent-runtime/intelligence'\nimport type { ResolvedAgentProfile } from './agent'\n\nconst defaultRefreshMs = 300_000\n\n/** Define configuration options for delivering certified artifacts to a specified tenant target */\nexport interface CertifiedDeliveryConfig {\n /** The tenant target whose certified artifacts to deliver (the agent id). */\n target: string\n /** Bearer for the plane. Reads `TANGLE_API_KEY` when omitted. */\n apiKey?: string\n /** Plane base URL. Reads `TANGLE_INTELLIGENCE_URL` then the public plane. */\n baseUrl?: string\n /** Min interval between certified-profile pulls. Default 5m. */\n refreshMs?: number\n /** fetch impl (tests / non-global-fetch runtimes). */\n fetchImpl?: typeof fetch\n}\n\n/** Resolve and manage certified profiles with refresh and composition capabilities */\nexport interface CertifiedDelivery {\n /** The `composeProfile` transform to pass to `createAgentRuntime`. Applies the\n * cached certified profile to the base surfaces; refreshes on the cadence. */\n composeProfile(base: ResolvedAgentProfile): Promise<ResolvedAgentProfile>\n /** Force a pull now (ignores the refresh window). Best-effort. */\n refresh(): Promise<void>\n /** The certified profile currently in effect (null = none promoted / pull\n * failed). Lets the sandbox-provisioning seam deliver the file/server\n * artifact types this in-process seam doesn't. */\n current(): CertifiedProfile | null\n}\n\n/**\n * Build a certified-delivery transform for one agent target. Fail-closed: a pull\n * error or 404 keeps the last-known certified profile (or null), and the agent\n * runs on its base surfaces — it never breaks because Intelligence is down.\n */\nexport function createCertifiedDelivery(config: CertifiedDeliveryConfig): CertifiedDelivery {\n const refreshMs = config.refreshMs ?? defaultRefreshMs\n let certified: CertifiedProfile | null = null\n let lastPullAt = 0\n let inflight: Promise<void> | null = null\n\n async function refresh(force = false): Promise<void> {\n if (!force && Date.now() - lastPullAt < refreshMs) return\n if (inflight) return inflight\n inflight = (async () => {\n const outcome = await pullCertified({\n target: config.target,\n apiKey: config.apiKey,\n baseUrl: config.baseUrl,\n fetchImpl: config.fetchImpl,\n })\n lastPullAt = Date.now()\n // Only replace on a real pull; a 404/error keeps the last-known profile.\n if (outcome.succeeded) certified = outcome.value\n })()\n try {\n await inflight\n } finally {\n inflight = null\n }\n }\n\n return {\n async composeProfile(base) {\n await refresh()\n return {\n // prompt-surface + skill fold into the system prompt.\n systemPrompt: composeCertifiedPrompt(base.systemPrompt, certified),\n // Certified `tool` artifacts deliver here once they carry a runnable\n // OpenAI def + the product wires the executor; until then pass through.\n extraTools: base.extraTools,\n }\n },\n refresh: () => refresh(true),\n current: () => certified,\n }\n}\n","/**\n * Surface-scoped profile overlay — the seam letting any product page (a\n * sequence editor, a brief composer, a dataset view) add MCP servers, a prompt\n * addendum, and permission tightening to the workspace agent profile for turns\n * initiated FROM that surface, without the chat orchestrator knowing any\n * surface's specifics. The orchestrator resolves `(kind, ctx)` through a\n * registry the REQUEST HANDLER constructs per request (construction is a Map\n * build — cheap) and merges the result into the base profile it was about to\n * send to the sandbox. Per-request construction is the trust mechanism, not an\n * optimization target: each `build()` closes over server-trusted request state\n * (env bindings, secrets, the AUTHENTICATED user/workspace), which on Workers\n * exists only per request — a startup-built registry would force identity\n * through the untrusted client `ctx`.\n *\n * SECURITY INVARIANT: the surface `kind` and the ids inside `ctx` arrive on\n * the client request and are pure ROUTING data — never trusted content, never\n * identity. Identity comes from the closure (see above). The registered\n * `build()` runs server-side only: it validates the routing ids against the\n * product's access control, then mints its own URLs and capability tokens from\n * server configuration (`buildHttpMcpServer` + `createCapabilityToken` in\n * ../tools). A client can therefore never inject an arbitrary MCP url, header,\n * or token into the agent profile: the overlay's `mcp` values are typed as\n * {@link SurfaceMcpServer} (= the server-built `AppToolMcpServer` entry shape),\n * and only build() constructs them.\n */\n\nimport type { AppToolMcpServer } from '../tools/mcp'\n\n/** Sandbox permission posture values, ranked deny > ask > allow for merging. */\nexport type SurfacePermissionValue = 'allow' | 'ask' | 'deny'\n\n/** The only MCP entry shape an overlay may carry: the server-built bridge\n * entry from ../tools/mcp (transport, url, headers, and capability token all\n * assembled server-side). The alias exists so overlay authors reach for the\n * builders in ../tools rather than hand-rolling `{ url: ctx.url }` shapes\n * that would let request data become a dialable endpoint. */\nexport type SurfaceMcpServer = AppToolMcpServer\n\n/** What one surface contributes to the agent profile for a single turn. */\nexport interface SurfaceOverlay {\n /** MCP servers to mount for this turn, keyed by tool-routing name. Names\n * must not collide with the base profile's — see {@link mergeSurfaceOverlay}. */\n mcp?: Record<string, SurfaceMcpServer>\n /** Appended to the base system-prompt addendum with a blank-line separator. */\n promptAddendum?: string\n /** Per-key posture the surface wants for its turns. Merging is monotone\n * fail-closed: the stricter of base/overlay wins, so a surface can tighten\n * the workspace posture but never relax it. */\n permissions?: Record<string, SurfacePermissionValue>\n}\n\n/**\n * One registered surface kind. `TCtx` is the shape build() expects — a CLAIM\n * about the client payload, not a guarantee: the registry hands build() the\n * request's `ctx` unvalidated, so build() must treat every field as an\n * untrusted id (resolve it through access control that throws on a bad or\n * foreign id) before minting anything from it.\n */\nexport interface SurfaceKindDefinition<TCtx> {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}\n\n/** The variance-erased form a registry accepts (`build` is contravariant in\n * `TCtx`, so every concrete definition is assignable to this). */\nexport type AnySurfaceKind = SurfaceKindDefinition<never>\n\n/**\n * Declare one surface kind. The `kind` string is the client-visible routing\n * key (e.g. `'sequences'`); `build` is the server-side factory that turns a\n * validated ctx into the overlay for one turn.\n */\nexport function defineSurfaceKind<TCtx>(opts: {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}): SurfaceKindDefinition<TCtx> {\n if (typeof opts.kind !== 'string' || opts.kind.length === 0 || /\\s/.test(opts.kind)) {\n throw new Error(`surface kind must be a non-empty string without whitespace (got ${JSON.stringify(opts.kind)})`)\n }\n if (typeof opts.build !== 'function') {\n throw new Error(`surface kind '${opts.kind}' requires a build function`)\n }\n return { kind: opts.kind, build: opts.build }\n}\n\n/** Resolve and build the overlay for a given surface kind within a turn context */\nexport interface SurfaceRegistry {\n /** Build the overlay for one turn. Throws on an unknown kind — an unknown\n * surface is a routing bug (client and server registries drifted), and\n * silently returning an empty overlay would strip the surface's tools from\n * the turn with no signal anywhere. Build errors propagate unwrapped. */\n resolve(kind: string, ctx: unknown): Promise<SurfaceOverlay>\n}\n\n/**\n * Assemble the product's surface registry from its registered kinds. Duplicate\n * kinds throw at construction: two builders behind one routing key would make\n * the mounted toolset depend on registration order.\n */\nexport function createSurfaceRegistry(kinds: readonly AnySurfaceKind[]): SurfaceRegistry {\n const byKind = new Map<string, AnySurfaceKind>()\n for (const definition of kinds) {\n if (byKind.has(definition.kind)) {\n throw new Error(`duplicate surface kind '${definition.kind}' — each kind must be registered exactly once`)\n }\n byKind.set(definition.kind, definition)\n }\n\n return {\n async resolve(kind, ctx) {\n const definition = byKind.get(kind)\n if (!definition) {\n const known = [...byKind.keys()].join(', ') || '(none)'\n throw new Error(\n `unknown surface kind '${kind}' — registered kinds: ${known}. ` +\n 'An unknown surface is a routing bug: register the kind via defineSurfaceKind before clients can reference it.',\n )\n }\n // The trust boundary where static ctx typing ends: the request payload is\n // handed to build() as-is, and build() validates it (see SurfaceKindDefinition).\n const overlay = await definition.build(ctx as never)\n assertSurfaceOverlay(overlay, `surface kind '${kind}'`)\n return overlay\n },\n }\n}\n\n/** Base-profile slice the merge reads/writes. Real callers pass their full\n * profile object; every field outside this slice passes through untouched. */\nexport interface SurfaceMergeBase {\n mcp?: Record<string, unknown>\n systemPromptAddendum?: string\n permissions?: Record<string, SurfacePermissionValue>\n}\n\nconst PERMISSION_SEVERITY: Record<SurfacePermissionValue, number> = { allow: 0, ask: 1, deny: 2 }\n\n/**\n * Merge one surface overlay into a base profile, returning a new object\n * (the base is never mutated; untouched nested records are shared by\n * reference).\n *\n * - `mcp`: overlay servers are added under their own names. A name already\n * present on the base THROWS — a collision is two servers claiming one\n * routing name, and renaming either silently would corrupt tool routing for\n * whichever caller expected the original binding.\n * - `systemPromptAddendum`: the overlay's `promptAddendum` appends after a\n * blank-line separator (no separator when the base has no addendum).\n * - `permissions`: per key the STRICTER value wins (deny > ask > allow). A\n * surface can tighten the base posture for its turns; a base 'deny' survives\n * any overlay.\n */\nexport function mergeSurfaceOverlay<TBase extends SurfaceMergeBase>(\n base: TBase,\n overlay: SurfaceOverlay,\n): TBase & SurfaceMergeBase {\n assertSurfaceOverlay(overlay, 'surface overlay')\n const merged: SurfaceMergeBase = { ...base }\n\n if (overlay.mcp && Object.keys(overlay.mcp).length > 0) {\n const baseMcp = base.mcp ?? {}\n const collisions = Object.keys(overlay.mcp).filter((name) => name in baseMcp)\n if (collisions.length > 0) {\n throw new Error(\n `surface overlay MCP name collision: ${collisions.map((n) => `'${n}'`).join(', ')} already exist on the base profile. ` +\n 'Two servers cannot claim one name — give the surface server a distinct name.',\n )\n }\n merged.mcp = { ...baseMcp, ...overlay.mcp }\n }\n\n if (overlay.promptAddendum !== undefined) {\n merged.systemPromptAddendum = base.systemPromptAddendum\n ? `${base.systemPromptAddendum}\\n\\n${overlay.promptAddendum}`\n : overlay.promptAddendum\n }\n\n if (overlay.permissions && Object.keys(overlay.permissions).length > 0) {\n const permissions: Record<string, SurfacePermissionValue> = { ...(base.permissions ?? {}) }\n for (const [key, value] of Object.entries(overlay.permissions)) {\n const existing = permissions[key]\n permissions[key] =\n existing === undefined || PERMISSION_SEVERITY[value] > PERMISSION_SEVERITY[existing] ? value : existing\n }\n merged.permissions = permissions\n }\n\n // merged began as a shallow copy of base; only the three slice fields were\n // replaced, so the intersection type is the true shape.\n return merged as TBase & SurfaceMergeBase\n}\n\n/** Reject overlays a build() (or hand-rolled caller) malformed, with the exact\n * field named: a relative MCP url, a blank addendum, or an off-vocabulary\n * permission would otherwise surface only as an opaque sandbox failure. */\nfunction assertSurfaceOverlay(overlay: SurfaceOverlay, label: string): void {\n if (overlay.promptAddendum !== undefined) {\n if (typeof overlay.promptAddendum !== 'string' || overlay.promptAddendum.trim().length === 0) {\n throw new Error(`${label}: promptAddendum must be a non-blank string when provided`)\n }\n }\n if (overlay.mcp !== undefined) {\n for (const [name, server] of Object.entries(overlay.mcp)) {\n if (name.trim().length === 0) throw new Error(`${label}: MCP server names must be non-empty`)\n if (server.transport !== 'http') {\n throw new Error(`${label}: MCP server '${name}' must use transport 'http' (got ${JSON.stringify((server as { transport?: unknown }).transport)})`)\n }\n let parsed: URL\n try {\n parsed = new URL(server.url)\n } catch {\n throw new Error(`${label}: MCP server '${name}' url must be an absolute URL (got ${JSON.stringify(server.url)})`)\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`${label}: MCP server '${name}' url must be http(s) (got ${JSON.stringify(server.url)})`)\n }\n }\n }\n if (overlay.permissions !== undefined) {\n for (const [key, value] of Object.entries(overlay.permissions)) {\n if (!(value in PERMISSION_SEVERITY)) {\n throw new Error(`${label}: permission '${key}' must be 'allow' | 'ask' | 'deny' (got ${JSON.stringify(value)})`)\n }\n }\n }\n}\n","/**\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'\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'\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 | { type: 'tool_call'; call: import('@tangle-network/agent-runtime').ToolLoopCall }\n | { type: 'usage'; usage: { promptTokens: number; completionTokens: number } }\n | { type: 'other'; event: unknown }\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/runtime/openai-stream.ts","../../src/runtime/certified-delivery.ts","../../src/runtime/surface-profile.ts","../../src/runtime/loop.ts"],"sourcesContent":["/**\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'\n\n/** Minimal OpenAI Chat Completions streaming chunk (structural — no `openai` dep). */\nexport interface OpenAIStreamChunk {\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 }>\n }\n finish_reason?: string | null\n }>\n /** Final-chunk token accounting (requires `stream_options.include_usage`). */\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}\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 */\nexport async function* toLoopEvents(chunks: AsyncIterable<OpenAIStreamChunk>): AsyncIterable<LoopEvent> {\n const calls = new Map<number, PartialToolCall>()\n for await (const chunk of chunks) {\n // Usage rides the final chunk, which has an empty choices array — handle\n // it before the choice guard.\n if (chunk.usage?.prompt_tokens != null || chunk.usage?.completion_tokens != null) {\n yield {\n type: 'usage',\n usage: {\n promptTokens: chunk.usage.prompt_tokens ?? 0,\n completionTokens: chunk.usage.completion_tokens ?? 0,\n },\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: '' }\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 calls.set(tc.index, cur)\n }\n }\n for (const [, c] of [...calls.entries()].sort((a, b) => a[0] - b[0])) {\n if (!c.name) continue\n yield { type: 'tool_call', call: { toolCallId: c.id, toolName: c.name, args: safeParse(c.args) } satisfies LoopToolCall }\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/** 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}\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 }),\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): AsyncIterable<OpenAIStreamChunk> {\n const res = await doFetch(url, {\n method: 'POST',\n headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', Accept: 'text/event-stream' },\n body: JSON.stringify(body),\n })\n if (!res.ok || !res.body) {\n const text = res.body ? await res.text().catch(() => '') : ''\n const error = new Error(`OpenAI-compat stream failed (HTTP ${res.status})${text ? `: ${text.slice(0, 200)}` : ''}`)\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 throw error\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 try {\n yield JSON.parse(data) as OpenAIStreamChunk\n } catch {\n /* skip a partial/garbled SSE frame */\n }\n }\n }\n}\n","/**\n * `createCertifiedDelivery` — the delivery truck for Tangle Intelligence.\n *\n * Pulls a tenant's CERTIFIED `AgentProfile` from the deployed plane\n * (`GET /v1/profiles/:target/composed`) and applies it to the agent's resolved\n * surfaces each turn, so an approved improvement reaches the running agent with\n * NO redeploy. This is the `composeProfile` transform `createAgentRuntime`\n * accepts — opt-in per product, fail-closed, cached + refreshed.\n *\n * Profile-WIDE by design (not prompt-only): the composed profile carries every\n * promoted artifact type keyed by kind. What's folded where:\n * - `prompt-surface` + `skill` → the system prompt (via `composeCertifiedPrompt`).\n * - `tool` artifacts that carry an OpenAI tool definition → `extraTools`\n * (advertised to the model; the matching executor is supplied by the product\n * via `executeOtherTool`). Until a `tool` artifact carries a runnable def,\n * it is surfaced (see `current()`) but not advertised — advertising a tool\n * with no executor would make the model call into a dead end.\n * - `mcp` / `memory` / `rag` artifacts materialize as servers/files and deliver\n * through the SANDBOX-provisioning seam, not this in-process one. The full\n * certified profile is exposed via `current()` so that seam can consume it.\n *\n * Substrate boundary: THIS module imports `@tangle-network/agent-runtime`; the\n * `createAgentRuntime` core does not (it only consumes the generic transform).\n */\n\nimport {\n composeCertifiedPrompt,\n type CertifiedProfile,\n pullCertified,\n} from '@tangle-network/agent-runtime/intelligence'\nimport type { ResolvedAgentProfile } from './agent'\n\nconst defaultRefreshMs = 300_000\n\n/** Define configuration options for delivering certified artifacts to a specified tenant target */\nexport interface CertifiedDeliveryConfig {\n /** The tenant target whose certified artifacts to deliver (the agent id). */\n target: string\n /** Bearer for the plane. Reads `TANGLE_API_KEY` when omitted. */\n apiKey?: string\n /** Plane base URL. Reads `TANGLE_INTELLIGENCE_URL` then the public plane. */\n baseUrl?: string\n /** Min interval between certified-profile pulls. Default 5m. */\n refreshMs?: number\n /** fetch impl (tests / non-global-fetch runtimes). */\n fetchImpl?: typeof fetch\n}\n\n/** Resolve and manage certified profiles with refresh and composition capabilities */\nexport interface CertifiedDelivery {\n /** The `composeProfile` transform to pass to `createAgentRuntime`. Applies the\n * cached certified profile to the base surfaces; refreshes on the cadence. */\n composeProfile(base: ResolvedAgentProfile): Promise<ResolvedAgentProfile>\n /** Force a pull now (ignores the refresh window). Best-effort. */\n refresh(): Promise<void>\n /** The certified profile currently in effect (null = none promoted / pull\n * failed). Lets the sandbox-provisioning seam deliver the file/server\n * artifact types this in-process seam doesn't. */\n current(): CertifiedProfile | null\n}\n\n/**\n * Build a certified-delivery transform for one agent target. Fail-closed: a pull\n * error or 404 keeps the last-known certified profile (or null), and the agent\n * runs on its base surfaces — it never breaks because Intelligence is down.\n */\nexport function createCertifiedDelivery(config: CertifiedDeliveryConfig): CertifiedDelivery {\n const refreshMs = config.refreshMs ?? defaultRefreshMs\n let certified: CertifiedProfile | null = null\n let lastPullAt = 0\n let inflight: Promise<void> | null = null\n\n async function refresh(force = false): Promise<void> {\n if (!force && Date.now() - lastPullAt < refreshMs) return\n if (inflight) return inflight\n inflight = (async () => {\n const outcome = await pullCertified({\n target: config.target,\n apiKey: config.apiKey,\n baseUrl: config.baseUrl,\n fetchImpl: config.fetchImpl,\n })\n lastPullAt = Date.now()\n // Only replace on a real pull; a 404/error keeps the last-known profile.\n if (outcome.succeeded) certified = outcome.value\n })()\n try {\n await inflight\n } finally {\n inflight = null\n }\n }\n\n return {\n async composeProfile(base) {\n await refresh()\n return {\n // prompt-surface + skill fold into the system prompt.\n systemPrompt: composeCertifiedPrompt(base.systemPrompt, certified),\n // Certified `tool` artifacts deliver here once they carry a runnable\n // OpenAI def + the product wires the executor; until then pass through.\n extraTools: base.extraTools,\n }\n },\n refresh: () => refresh(true),\n current: () => certified,\n }\n}\n","/**\n * Surface-scoped profile overlay — the seam letting any product page (a\n * sequence editor, a brief composer, a dataset view) add MCP servers, a prompt\n * addendum, and permission tightening to the workspace agent profile for turns\n * initiated FROM that surface, without the chat orchestrator knowing any\n * surface's specifics. The orchestrator resolves `(kind, ctx)` through a\n * registry the REQUEST HANDLER constructs per request (construction is a Map\n * build — cheap) and merges the result into the base profile it was about to\n * send to the sandbox. Per-request construction is the trust mechanism, not an\n * optimization target: each `build()` closes over server-trusted request state\n * (env bindings, secrets, the AUTHENTICATED user/workspace), which on Workers\n * exists only per request — a startup-built registry would force identity\n * through the untrusted client `ctx`.\n *\n * SECURITY INVARIANT: the surface `kind` and the ids inside `ctx` arrive on\n * the client request and are pure ROUTING data — never trusted content, never\n * identity. Identity comes from the closure (see above). The registered\n * `build()` runs server-side only: it validates the routing ids against the\n * product's access control, then mints its own URLs and capability tokens from\n * server configuration (`buildHttpMcpServer` + `createCapabilityToken` in\n * ../tools). A client can therefore never inject an arbitrary MCP url, header,\n * or token into the agent profile: the overlay's `mcp` values are typed as\n * {@link SurfaceMcpServer} (= the server-built `AppToolMcpServer` entry shape),\n * and only build() constructs them.\n */\n\nimport type { AppToolMcpServer } from '../tools/mcp'\n\n/** Sandbox permission posture values, ranked deny > ask > allow for merging. */\nexport type SurfacePermissionValue = 'allow' | 'ask' | 'deny'\n\n/** The only MCP entry shape an overlay may carry: the server-built bridge\n * entry from ../tools/mcp (transport, url, headers, and capability token all\n * assembled server-side). The alias exists so overlay authors reach for the\n * builders in ../tools rather than hand-rolling `{ url: ctx.url }` shapes\n * that would let request data become a dialable endpoint. */\nexport type SurfaceMcpServer = AppToolMcpServer\n\n/** What one surface contributes to the agent profile for a single turn. */\nexport interface SurfaceOverlay {\n /** MCP servers to mount for this turn, keyed by tool-routing name. Names\n * must not collide with the base profile's — see {@link mergeSurfaceOverlay}. */\n mcp?: Record<string, SurfaceMcpServer>\n /** Appended to the base system-prompt addendum with a blank-line separator. */\n promptAddendum?: string\n /** Per-key posture the surface wants for its turns. Merging is monotone\n * fail-closed: the stricter of base/overlay wins, so a surface can tighten\n * the workspace posture but never relax it. */\n permissions?: Record<string, SurfacePermissionValue>\n}\n\n/**\n * One registered surface kind. `TCtx` is the shape build() expects — a CLAIM\n * about the client payload, not a guarantee: the registry hands build() the\n * request's `ctx` unvalidated, so build() must treat every field as an\n * untrusted id (resolve it through access control that throws on a bad or\n * foreign id) before minting anything from it.\n */\nexport interface SurfaceKindDefinition<TCtx> {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}\n\n/** The variance-erased form a registry accepts (`build` is contravariant in\n * `TCtx`, so every concrete definition is assignable to this). */\nexport type AnySurfaceKind = SurfaceKindDefinition<never>\n\n/**\n * Declare one surface kind. The `kind` string is the client-visible routing\n * key (e.g. `'sequences'`); `build` is the server-side factory that turns a\n * validated ctx into the overlay for one turn.\n */\nexport function defineSurfaceKind<TCtx>(opts: {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}): SurfaceKindDefinition<TCtx> {\n if (typeof opts.kind !== 'string' || opts.kind.length === 0 || /\\s/.test(opts.kind)) {\n throw new Error(`surface kind must be a non-empty string without whitespace (got ${JSON.stringify(opts.kind)})`)\n }\n if (typeof opts.build !== 'function') {\n throw new Error(`surface kind '${opts.kind}' requires a build function`)\n }\n return { kind: opts.kind, build: opts.build }\n}\n\n/** Resolve and build the overlay for a given surface kind within a turn context */\nexport interface SurfaceRegistry {\n /** Build the overlay for one turn. Throws on an unknown kind — an unknown\n * surface is a routing bug (client and server registries drifted), and\n * silently returning an empty overlay would strip the surface's tools from\n * the turn with no signal anywhere. Build errors propagate unwrapped. */\n resolve(kind: string, ctx: unknown): Promise<SurfaceOverlay>\n}\n\n/**\n * Assemble the product's surface registry from its registered kinds. Duplicate\n * kinds throw at construction: two builders behind one routing key would make\n * the mounted toolset depend on registration order.\n */\nexport function createSurfaceRegistry(kinds: readonly AnySurfaceKind[]): SurfaceRegistry {\n const byKind = new Map<string, AnySurfaceKind>()\n for (const definition of kinds) {\n if (byKind.has(definition.kind)) {\n throw new Error(`duplicate surface kind '${definition.kind}' — each kind must be registered exactly once`)\n }\n byKind.set(definition.kind, definition)\n }\n\n return {\n async resolve(kind, ctx) {\n const definition = byKind.get(kind)\n if (!definition) {\n const known = [...byKind.keys()].join(', ') || '(none)'\n throw new Error(\n `unknown surface kind '${kind}' — registered kinds: ${known}. ` +\n 'An unknown surface is a routing bug: register the kind via defineSurfaceKind before clients can reference it.',\n )\n }\n // The trust boundary where static ctx typing ends: the request payload is\n // handed to build() as-is, and build() validates it (see SurfaceKindDefinition).\n const overlay = await definition.build(ctx as never)\n assertSurfaceOverlay(overlay, `surface kind '${kind}'`)\n return overlay\n },\n }\n}\n\n/** Base-profile slice the merge reads/writes. Real callers pass their full\n * profile object; every field outside this slice passes through untouched. */\nexport interface SurfaceMergeBase {\n mcp?: Record<string, unknown>\n systemPromptAddendum?: string\n permissions?: Record<string, SurfacePermissionValue>\n}\n\nconst PERMISSION_SEVERITY: Record<SurfacePermissionValue, number> = { allow: 0, ask: 1, deny: 2 }\n\n/**\n * Merge one surface overlay into a base profile, returning a new object\n * (the base is never mutated; untouched nested records are shared by\n * reference).\n *\n * - `mcp`: overlay servers are added under their own names. A name already\n * present on the base THROWS — a collision is two servers claiming one\n * routing name, and renaming either silently would corrupt tool routing for\n * whichever caller expected the original binding.\n * - `systemPromptAddendum`: the overlay's `promptAddendum` appends after a\n * blank-line separator (no separator when the base has no addendum).\n * - `permissions`: per key the STRICTER value wins (deny > ask > allow). A\n * surface can tighten the base posture for its turns; a base 'deny' survives\n * any overlay.\n */\nexport function mergeSurfaceOverlay<TBase extends SurfaceMergeBase>(\n base: TBase,\n overlay: SurfaceOverlay,\n): TBase & SurfaceMergeBase {\n assertSurfaceOverlay(overlay, 'surface overlay')\n const merged: SurfaceMergeBase = { ...base }\n\n if (overlay.mcp && Object.keys(overlay.mcp).length > 0) {\n const baseMcp = base.mcp ?? {}\n const collisions = Object.keys(overlay.mcp).filter((name) => name in baseMcp)\n if (collisions.length > 0) {\n throw new Error(\n `surface overlay MCP name collision: ${collisions.map((n) => `'${n}'`).join(', ')} already exist on the base profile. ` +\n 'Two servers cannot claim one name — give the surface server a distinct name.',\n )\n }\n merged.mcp = { ...baseMcp, ...overlay.mcp }\n }\n\n if (overlay.promptAddendum !== undefined) {\n merged.systemPromptAddendum = base.systemPromptAddendum\n ? `${base.systemPromptAddendum}\\n\\n${overlay.promptAddendum}`\n : overlay.promptAddendum\n }\n\n if (overlay.permissions && Object.keys(overlay.permissions).length > 0) {\n const permissions: Record<string, SurfacePermissionValue> = { ...(base.permissions ?? {}) }\n for (const [key, value] of Object.entries(overlay.permissions)) {\n const existing = permissions[key]\n permissions[key] =\n existing === undefined || PERMISSION_SEVERITY[value] > PERMISSION_SEVERITY[existing] ? value : existing\n }\n merged.permissions = permissions\n }\n\n // merged began as a shallow copy of base; only the three slice fields were\n // replaced, so the intersection type is the true shape.\n return merged as TBase & SurfaceMergeBase\n}\n\n/** Reject overlays a build() (or hand-rolled caller) malformed, with the exact\n * field named: a relative MCP url, a blank addendum, or an off-vocabulary\n * permission would otherwise surface only as an opaque sandbox failure. */\nfunction assertSurfaceOverlay(overlay: SurfaceOverlay, label: string): void {\n if (overlay.promptAddendum !== undefined) {\n if (typeof overlay.promptAddendum !== 'string' || overlay.promptAddendum.trim().length === 0) {\n throw new Error(`${label}: promptAddendum must be a non-blank string when provided`)\n }\n }\n if (overlay.mcp !== undefined) {\n for (const [name, server] of Object.entries(overlay.mcp)) {\n if (name.trim().length === 0) throw new Error(`${label}: MCP server names must be non-empty`)\n if (server.transport !== 'http') {\n throw new Error(`${label}: MCP server '${name}' must use transport 'http' (got ${JSON.stringify((server as { transport?: unknown }).transport)})`)\n }\n let parsed: URL\n try {\n parsed = new URL(server.url)\n } catch {\n throw new Error(`${label}: MCP server '${name}' url must be an absolute URL (got ${JSON.stringify(server.url)})`)\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`${label}: MCP server '${name}' url must be http(s) (got ${JSON.stringify(server.url)})`)\n }\n }\n }\n if (overlay.permissions !== undefined) {\n for (const [key, value] of Object.entries(overlay.permissions)) {\n if (!(value in PERMISSION_SEVERITY)) {\n throw new Error(`${label}: permission '${key}' must be 'allow' | 'ask' | 'deny' (got ${JSON.stringify(value)})`)\n }\n }\n }\n}\n","/**\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'\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'\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 | { type: 'tool_call'; call: import('@tangle-network/agent-runtime').ToolLoopCall }\n | { type: 'usage'; usage: { promptTokens: number; completionTokens: number } }\n | { type: 'other'; event: unknown }\n"],"mappings":"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","names":[]}
@@ -207,9 +207,14 @@ declare function workProductTrustInputs<D extends string = string>(records: read
207
207
  /** Max entries per `upsert_evidence`/`flag_exception` call — keeps each call a
208
208
  * small batch the model can correct precisely on a named-index failure. */
209
209
  declare const MAX_WORK_PRODUCT_BATCH = 50;
210
- /** The shell's one generic platform check: every material target has ≥1
211
- * evidence row. Recorded as `QualityCheck{source:'platform'}`. */
210
+ /** Platform check: every material target has ≥1 evidence row. Recorded as
211
+ * `QualityCheck{source:'platform'}`. */
212
212
  declare const EVIDENCE_COVERAGE_CHECK = "evidence_coverage";
213
+ /** Platform check: how many evidence entries carry a quote the shell PROVED
214
+ * occurs in the source it names. Recorded when `readSourceText` is wired, so
215
+ * a reviewer reads the strength of the lineage off the row itself rather than
216
+ * trusting that a quote was checked. */
217
+ declare const QUOTE_VERIFICATION_CHECK = "quote_verification";
213
218
  /** Domain seams for the three work-product tools — every domain word is a
214
219
  * parameter; the shell bakes none. */
215
220
  interface WorkProductToolConfig {
@@ -222,6 +227,20 @@ interface WorkProductToolConfig {
222
227
  * (vault stat / attachment lookup). A dangling ref is a `ToolInputError`
223
228
  * naming the entry index — lineage can never point at nothing. */
224
229
  resolveSourceRef: (ref: string, ctx: AppToolContext) => Promise<boolean>;
230
+ /** Fail-loud QUOTE verification: return the source document's TEXT for a
231
+ * ref so the shell can prove each `locator.quote` occurs in it verbatim.
232
+ * Wiring this turns the gate ON — a quote that does not occur is a
233
+ * `ToolInputError` naming the entry index, so the model re-extracts from
234
+ * the document instead of persisting invented lineage.
235
+ *
236
+ * Return `null` ONLY when the ref genuinely has no extractable text (an
237
+ * image scan, an opaque blob). The gate is fail-CLOSED on `null`: a quote
238
+ * that cannot be checked is refused, because "unverifiable" and "verified"
239
+ * must never look the same to a reviewer. Such an entry is still recordable
240
+ * without `locator.quote` — `claim` carries the assertion.
241
+ *
242
+ * Omit the seam entirely and no quote is checked. */
243
+ readSourceText?: (ref: string, ctx: AppToolContext) => Promise<string | null>;
225
244
  /** The material targets the platform coverage check requires evidence for.
226
245
  * Product-owned vocabulary; omit to skip the coverage gate. */
227
246
  materialTargets?: (artifact: WorkProductArtifact) => string[];
@@ -239,6 +258,41 @@ interface WorkProductToolConfig {
239
258
  * MCP server / HTTP handler / runtime executor. */
240
259
  declare function buildWorkProductTools(config: WorkProductToolConfig): AppToolDefinition[];
241
260
 
261
+ /**
262
+ * Verbatim quote verification — the lineage gate.
263
+ *
264
+ * An evidence `locator.quote` is the reviewer's click target: it must land on
265
+ * text that is actually in the document the entry names. Nothing here is
266
+ * fuzzy. The only latitude is representational — the same characters written
267
+ * differently by a PDF extractor, a text transcription, or a model repeating
268
+ * what it read: Unicode compatibility forms, curly quotes, the several dash
269
+ * codepoints, non-breaking spaces, and run-length whitespace. A quote that
270
+ * still does not occur after that is not a formatting difference; it is text
271
+ * the document does not contain.
272
+ *
273
+ * Deliberately NOT tolerated, because each one re-opens the hole this closes:
274
+ * case (a quote is a quote), token subsets, edit distance, word-overlap
275
+ * scoring, and "the numbers match" heuristics. A fabricated quote and a real
276
+ * one differ by exactly the thing a fuzzy matcher forgives.
277
+ */
278
+ /**
279
+ * Fold a string to the form both sides of a quote comparison are measured in.
280
+ * Composition and presentation only: NFKC, one dash, one apostrophe, one
281
+ * double quote, runs of any whitespace to a single space, trimmed. Case is
282
+ * PRESERVED — lowercasing would let "Box 1" match "box 1", and a citation
283
+ * that cannot reproduce capitalization did not read the document.
284
+ */
285
+ declare function normalizeQuoteText(value: string): string;
286
+ /**
287
+ * Does `quote` occur in `sourceText`? Exact substring first (the common case,
288
+ * and free); the normalized comparison second, for the representational
289
+ * differences above. Nothing else.
290
+ *
291
+ * An empty or whitespace-only quote is NOT a match — it would otherwise be a
292
+ * substring of every document and pass the gate vacuously.
293
+ */
294
+ declare function sourceContainsQuote(sourceText: string, quote: string): boolean;
295
+
242
296
  /**
243
297
  * Framework-neutral work-product review endpoints — the
244
298
  * `createInteractionAnswerRoute` factory pattern: web-standard
@@ -328,4 +382,4 @@ interface WorkProductRoutes {
328
382
  /** Create the work-product review endpoints over the store port and product seams */
329
383
  declare function createWorkProductRoutes(options: WorkProductRoutesOptions): WorkProductRoutes;
330
384
 
331
- export { type CreateWorkProductInput, EVIDENCE_COVERAGE_CHECK, EvidenceEntry, ExceptionEntry, type FinalizeWorkProductProvenanceInput, type InMemoryWorkProductStore, MAX_WORK_PRODUCT_BATCH, QualityCheck, type SubmitWorkProductInput, TrustItem, WorkProductArtifact, WorkProductAuditEvent, type WorkProductAuthorizeArgs, type WorkProductOutcome, WorkProductPersistedPart, WorkProductProvenance, type WorkProductProvenanceBase, WorkProductRecord, type WorkProductRouteAuthorization, type WorkProductRoutes, type WorkProductRoutesOptions, type WorkProductService, type WorkProductServiceOptions, WorkProductStatus, WorkProductStorePort, type WorkProductToolConfig, type WorkProductVerdictBody, type WorkProductVerdictInput, buildWorkProductTools, canTransitionWorkProduct, createInMemoryWorkProductStore, createWorkProductRoutes, createWorkProductService, finalizeWorkProductProvenance, isWorkProductTerminal, stampProvenance, validateWorkProductVerdictBody, workProductTrustInputs };
385
+ export { type CreateWorkProductInput, EVIDENCE_COVERAGE_CHECK, EvidenceEntry, ExceptionEntry, type FinalizeWorkProductProvenanceInput, type InMemoryWorkProductStore, MAX_WORK_PRODUCT_BATCH, QUOTE_VERIFICATION_CHECK, QualityCheck, type SubmitWorkProductInput, TrustItem, WorkProductArtifact, WorkProductAuditEvent, type WorkProductAuthorizeArgs, type WorkProductOutcome, WorkProductPersistedPart, WorkProductProvenance, type WorkProductProvenanceBase, WorkProductRecord, type WorkProductRouteAuthorization, type WorkProductRoutes, type WorkProductRoutesOptions, type WorkProductService, type WorkProductServiceOptions, WorkProductStatus, WorkProductStorePort, type WorkProductToolConfig, type WorkProductVerdictBody, type WorkProductVerdictInput, buildWorkProductTools, canTransitionWorkProduct, createInMemoryWorkProductStore, createWorkProductRoutes, createWorkProductService, finalizeWorkProductProvenance, isWorkProductTerminal, normalizeQuoteText, sourceContainsQuote, stampProvenance, validateWorkProductVerdictBody, workProductTrustInputs };
@@ -387,9 +387,27 @@ function workProductTrustInputs(records, verdictsFor) {
387
387
  return items;
388
388
  }
389
389
 
390
+ // src/work-product/quote.ts
391
+ var WHITESPACE = /[\s\p{Zs}\u2028\u2029\u200b-\u200d\ufeff]+/gu;
392
+ var DASHES = /[\u2010-\u2015\u2212\ufe58\ufe63\uff0d]/gu;
393
+ var SINGLE_QUOTES = /[\u2018\u2019\u201a\u201b\u2032\u2035\u00b4`]/gu;
394
+ var DOUBLE_QUOTES = /[\u201c\u201d\u201e\u201f\u2033\u2036\u00ab\u00bb]/gu;
395
+ function normalizeQuoteText(value) {
396
+ return value.normalize("NFKC").replace(DASHES, "-").replace(SINGLE_QUOTES, "'").replace(DOUBLE_QUOTES, '"').replace(WHITESPACE, " ").trim();
397
+ }
398
+ function sourceContainsQuote(sourceText, quote) {
399
+ const trimmed = quote.trim();
400
+ if (trimmed.length === 0) return false;
401
+ if (sourceText.includes(trimmed)) return true;
402
+ const normalizedQuote = normalizeQuoteText(quote);
403
+ if (normalizedQuote.length === 0) return false;
404
+ return normalizeQuoteText(sourceText).includes(normalizedQuote);
405
+ }
406
+
390
407
  // src/work-product/tools.ts
391
408
  var MAX_WORK_PRODUCT_BATCH = 50;
392
409
  var EVIDENCE_COVERAGE_CHECK = "evidence_coverage";
410
+ var QUOTE_VERIFICATION_CHECK = "quote_verification";
393
411
  async function unwrap(run, code) {
394
412
  let outcome = await run();
395
413
  if (!outcome.succeeded && outcome.conflict) outcome = await run();
@@ -434,6 +452,50 @@ async function resolveDraft(service, config, scopeKey, ctx) {
434
452
  provenance: stampProvenance(config.provenance(ctx), config.now)
435
453
  });
436
454
  }
455
+ async function assertQuotesOccurInSources(config, entries, ctx) {
456
+ const readSourceText = config.readSourceText;
457
+ if (!readSourceText) return;
458
+ const texts = /* @__PURE__ */ new Map();
459
+ for (let index = 0; index < entries.length; index += 1) {
460
+ const entry = entries[index];
461
+ const quote = entry.locator.quote;
462
+ if (quote === void 0 || quote.trim().length === 0) continue;
463
+ if (!texts.has(entry.sourceRef)) texts.set(entry.sourceRef, await readSourceText(entry.sourceRef, ctx));
464
+ const text = texts.get(entry.sourceRef);
465
+ if (text === null || text === void 0) {
466
+ throw new ToolInputError(
467
+ "unverifiable_quote",
468
+ `entries[${index}].locator.quote: "${entry.sourceRef}" has no readable text, so the quote cannot be verified. Record the entry without locator.quote and state the basis in claim.`
469
+ );
470
+ }
471
+ if (!sourceContainsQuote(text, quote)) {
472
+ throw new ToolInputError(
473
+ "quote_not_found",
474
+ `entries[${index}].locator.quote: ${JSON.stringify(quote)} does not occur in "${entry.sourceRef}". Copy the supporting line character-for-character out of that document, or \u2014 if this value is COMPUTED rather than read \u2014 omit locator.quote and state the computation in claim.`
475
+ );
476
+ }
477
+ }
478
+ }
479
+ async function summarizeQuoteVerification(config, evidence, ctx) {
480
+ const readSourceText = config.readSourceText;
481
+ if (!readSourceText) return void 0;
482
+ const texts = /* @__PURE__ */ new Map();
483
+ let verified = 0;
484
+ let withoutQuote = 0;
485
+ const failed = [];
486
+ for (const entry of evidence) {
487
+ const quote = entry.locator.quote;
488
+ if (quote === void 0 || quote.trim().length === 0) {
489
+ withoutQuote += 1;
490
+ continue;
491
+ }
492
+ if (!texts.has(entry.sourceRef)) texts.set(entry.sourceRef, await readSourceText(entry.sourceRef, ctx));
493
+ const text = texts.get(entry.sourceRef);
494
+ if (typeof text === "string" && sourceContainsQuote(text, quote)) verified += 1;
495
+ else failed.push(entry.id);
496
+ }
497
+ return { verified, withoutQuote, failed };
498
+ }
437
499
  function buildWorkProductTools(config) {
438
500
  const service = createWorkProductService({
439
501
  store: config.store,
@@ -461,7 +523,10 @@ function buildWorkProductTools(config) {
461
523
  properties: {
462
524
  page: { type: "number" },
463
525
  range: { type: "string", description: "Free-form location: 'L120-L134' | 'B7' | '\xB64'." },
464
- quote: { type: "string", description: "Verbatim supporting quote." }
526
+ quote: {
527
+ type: "string",
528
+ description: "Verbatim supporting quote \u2014 copied character-for-character out of the source, not retyped, reformatted, or summarised. The platform checks it occurs in the document and REFUSES the entry if it does not. For a value you COMPUTED rather than read, omit this field and state the computation in claim."
529
+ }
465
530
  }
466
531
  },
467
532
  target: { type: "string", description: "Artifact field/claim this evidence supports." },
@@ -492,6 +557,7 @@ function buildWorkProductTools(config) {
492
557
  );
493
558
  }
494
559
  }
560
+ await assertQuotesOccurInSources(config, entries, ctx);
495
561
  const draft = await resolveDraft(service, config, scopeKey, ctx);
496
562
  const record = await unwrap(() => service.upsertEvidence(draft.id, entries), "evidence_rejected");
497
563
  return { workProductId: record.id, version: record.version, evidenceCount: record.evidence.length };
@@ -619,6 +685,24 @@ function buildWorkProductTools(config) {
619
685
  );
620
686
  }
621
687
  const checks = [...agentChecks];
688
+ const quotes = await summarizeQuoteVerification(config, draft.evidence, ctx);
689
+ if (quotes) {
690
+ const quoted = quotes.verified + quotes.failed.length;
691
+ checks.unshift({
692
+ id: QUOTE_VERIFICATION_CHECK,
693
+ name: QUOTE_VERIFICATION_CHECK,
694
+ passed: quotes.failed.length === 0,
695
+ detail: quotes.failed.length === 0 ? `${quotes.verified}/${quoted} quoted evidence entries verified against their source; ${quotes.withoutQuote} recorded without a quote` : `Unverifiable quotes on: ${quotes.failed.join(", ")}`,
696
+ source: "platform"
697
+ });
698
+ if (quotes.failed.length > 0) {
699
+ await unwrap(() => service.recordChecks(draft.id, checks), "checks_rejected");
700
+ throw new ToolInputError(
701
+ "quote_verification_failed",
702
+ `Cannot submit: ${quotes.failed.length} evidence entr${quotes.failed.length === 1 ? "y quotes" : "ies quote"} text that does not occur in the source named (${quotes.failed.join(", ")}). Re-emit each with a quote copied character-for-character from that document, or without locator.quote if the value is computed.`
703
+ );
704
+ }
705
+ }
622
706
  if (config.materialTargets) {
623
707
  const targets = config.materialTargets(artifact);
624
708
  const covered = new Set(draft.evidence.map((entry) => entry.target));
@@ -750,6 +834,7 @@ function createWorkProductRoutes(options) {
750
834
  export {
751
835
  EVIDENCE_COVERAGE_CHECK,
752
836
  MAX_WORK_PRODUCT_BATCH,
837
+ QUOTE_VERIFICATION_CHECK,
753
838
  buildWorkProductTools,
754
839
  canTransitionWorkProduct,
755
840
  createInMemoryWorkProductStore,
@@ -758,6 +843,7 @@ export {
758
843
  finalizeWorkProductProvenance,
759
844
  isWorkProductStatus,
760
845
  isWorkProductTerminal,
846
+ normalizeQuoteText,
761
847
  parseAgentCheckInput,
762
848
  parseArtifactInput,
763
849
  parseEvidenceInput,
@@ -765,6 +851,7 @@ export {
765
851
  parseReviewQueueItem,
766
852
  persistedPartToWorkProduct,
767
853
  projectReviewQueue,
854
+ sourceContainsQuote,
768
855
  stampProvenance,
769
856
  unresolvedBlockingExceptions,
770
857
  validateWorkProductVerdictBody,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/work-product/service.ts","../../src/work-product/provenance.ts","../../src/work-product/tools.ts","../../src/work-product/route.ts"],"sourcesContent":["/**\n * The guarded work-product status machine — the `/missions` service PATTERN\n * (load → validate against a transition table → CAS-write guarded on what was\n * read → audit event) over {@link WorkProductStorePort}. Deliberately NOT a\n * reuse of the mission service: missions' cursor/plan/budget machinery does\n * not apply here, and the six-status review machine is a different contract.\n *\n * Concurrency contract: a scope's draft is driven by a single serialized turn\n * owner, so real contention is rare. The service is the typed guard layer,\n * not a serializer — every mutation re-reads the record and CAS-writes\n * guarded on the `{status, version}` it read. A guard miss surfaces as\n * `{ succeeded: false, conflict: true }` (retryable: re-read and re-apply),\n * never a silent clobber.\n */\n\nimport {\n unresolvedBlockingExceptions,\n type EvidenceEntry,\n type ExceptionEntry,\n type QualityCheck,\n type WorkProductArtifact,\n type WorkProductAuditEvent,\n type WorkProductPatch,\n type WorkProductProvenance,\n type WorkProductRecord,\n type WorkProductStatus,\n type WorkProductStorePort,\n type WorkProductVersionEntry,\n} from './types'\n\n/** Discriminated outcome for guarded operations — `conflict` distinguishes a\n * lost guarded race (retryable) from a logic rejection (illegal edge,\n * missing row — deterministic, never retried). */\nexport type WorkProductOutcome<T> =\n | { succeeded: true; value: T }\n | { succeeded: false; error: string; conflict: boolean }\n\n// Legal status transitions. A target absent from a source's set is rejected\n// by the guarded helpers. `superseded` is terminal.\nconst WORK_PRODUCT_TRANSITIONS: Record<WorkProductStatus, ReadonlySet<WorkProductStatus>> = {\n draft: new Set<WorkProductStatus>(['blocked', 'ready']),\n blocked: new Set<WorkProductStatus>(['draft']),\n ready: new Set<WorkProductStatus>(['changes_requested', 'approved', 'superseded']),\n changes_requested: new Set<WorkProductStatus>(['draft', 'superseded']),\n approved: new Set<WorkProductStatus>(['superseded']),\n superseded: new Set<WorkProductStatus>(),\n}\n\n/** Whether a legal edge exists from `from` to `to` in the review machine. */\nexport function canTransitionWorkProduct(from: WorkProductStatus, to: WorkProductStatus): boolean {\n return WORK_PRODUCT_TRANSITIONS[from].has(to)\n}\n\n/** Statuses a work product can never leave. */\nexport function isWorkProductTerminal(status: WorkProductStatus): boolean {\n return WORK_PRODUCT_TRANSITIONS[status].size === 0\n}\n\n/** Define the input required to create a new draft work product row */\nexport interface CreateWorkProductInput {\n /** Explicit row id — omit to use the service's generator. The MODEL never\n * supplies ids; this is for deterministic server-side creation. */\n id?: string\n workspaceId: string\n threadId: string | null\n scopeKey: string\n /** Version this draft will become; default 1. The tool layer passes\n * `last reviewed version + 1` when a scope is re-engaged after approval. */\n version?: number\n provenance: WorkProductProvenance\n /** Opaque product-column values handed VERBATIM to the store's insert. */\n extras?: Record<string, unknown>\n}\n\n/** Payload for the draft→ready submit transition */\nexport interface SubmitWorkProductInput {\n artifact: WorkProductArtifact\n checks: QualityCheck[]\n provenance: WorkProductProvenance\n /** Frozen snapshot ref of this version's body for the history entry;\n * defaults to `artifact.path`. */\n artifactPath?: string\n}\n\n/** Reviewer verdict payload for the ready→approved / ready→changes_requested transition */\nexport interface WorkProductVerdictInput {\n verdict: 'approve' | 'request_changes'\n reviewedBy: string\n note?: string\n}\n\n/** Guarded mutation surface over the work-product store */\nexport interface WorkProductService {\n create(input: CreateWorkProductInput): Promise<WorkProductRecord>\n get(id: string): Promise<WorkProductRecord | null>\n /** The scope's open accumulator row (`draft`/`blocked`), or null. */\n openDraft(workspaceId: string, scopeKey: string): Promise<WorkProductRecord | null>\n /** The scope's `changes_requested` row awaiting its correction turn, or null. */\n awaitingCorrection(workspaceId: string, scopeKey: string): Promise<WorkProductRecord | null>\n /** The scope's `ready` row awaiting review, or null. */\n awaitingReview(workspaceId: string, scopeKey: string): Promise<WorkProductRecord | null>\n /** `max(version over the scope's approved/superseded rows) + 1` — the\n * version a fresh draft for the scope should carry. */\n nextVersion(workspaceId: string, scopeKey: string): Promise<number>\n /** Re-open a `changes_requested` row as the next draft: version bumps +1\n * and the correction turn accumulates into the same scope row. */\n reopen(id: string): Promise<WorkProductOutcome<WorkProductRecord>>\n /** Merge evidence entries by id (same id replaces — the agent can correct\n * itself mid-turn). Legal only while `draft`/`blocked`. */\n upsertEvidence(id: string, entries: readonly EvidenceEntry[]): Promise<WorkProductOutcome<WorkProductRecord>>\n /** Merge exception entries by id, then reconcile the blocked flag: any\n * unresolved blocking entry parks `draft`→`blocked`; resolving the last\n * one releases `blocked`→`draft`. */\n upsertExceptions(id: string, entries: readonly ExceptionEntry[]): Promise<WorkProductOutcome<WorkProductRecord>>\n /** Persist a checks array without transitioning — how a failed platform\n * gate (e.g. evidence_coverage) stays visible on the still-draft row. */\n recordChecks(id: string, checks: readonly QualityCheck[]): Promise<WorkProductOutcome<WorkProductRecord>>\n /** The terminal agent call: CAS `draft`→`ready` with artifact + checks +\n * provenance, appending the version-history entry. Refuses while an\n * unresolved blocking exception exists. */\n submit(id: string, input: SubmitWorkProductInput): Promise<WorkProductOutcome<WorkProductRecord>>\n /** Reviewer verdict: CAS `ready`→`approved`/`changes_requested` + history\n * entry. Approval also supersedes the scope's prior approved versions so\n * exactly one approved version is current per scope. */\n applyVerdict(id: string, input: WorkProductVerdictInput): Promise<WorkProductOutcome<WorkProductRecord>>\n /** Explicit replacement: CAS to `superseded` (legal from ready /\n * changes_requested / approved). */\n supersede(id: string): Promise<WorkProductOutcome<WorkProductRecord>>\n}\n\n/** Configuration options for creating a work product service */\nexport interface WorkProductServiceOptions {\n store: WorkProductStorePort\n /** Injectable clock (epoch ms). Default `Date.now`. */\n now?: () => number\n /** Row-id generator. Default `crypto.randomUUID()`. */\n generateId?: () => string\n}\n\nfunction rejected<T>(error: string): WorkProductOutcome<T> {\n return { succeeded: false, error, conflict: false }\n}\n\nfunction lostRace<T>(id: string): WorkProductOutcome<T> {\n return { succeeded: false, error: `Work product ${id} changed concurrently`, conflict: true }\n}\n\n/** Merge-by-id upsert: existing order preserved, replaced in place, new\n * entries appended in call order. */\nfunction mergeById<T extends { id: string }>(existing: readonly T[], incoming: readonly T[]): T[] {\n const merged = existing.slice()\n const indexById = new Map(merged.map((entry, index) => [entry.id, index] as const))\n for (const entry of incoming) {\n const at = indexById.get(entry.id)\n if (at === undefined) {\n indexById.set(entry.id, merged.length)\n merged.push(entry)\n } else {\n merged[at] = entry\n }\n }\n return merged\n}\n\n/** Create the guarded work-product service over a store port */\nexport function createWorkProductService(options: WorkProductServiceOptions): WorkProductService {\n const { store } = options\n const now = options.now ?? (() => Date.now())\n const generateId = options.generateId ?? (() => crypto.randomUUID())\n\n async function appendEvent(\n record: WorkProductRecord,\n step: string,\n message: string,\n metadata: Record<string, unknown> = {},\n ): Promise<void> {\n await store.appendEvent({\n workProductId: record.id,\n workspaceId: record.workspaceId,\n step,\n message,\n metadata,\n at: now(),\n })\n }\n\n // Guarded status transition: load → validate the edge → CAS guarded on the\n // {status, version} read → audit event. The loser of a racing transition\n // gets a conflict instead of silently violating the machine.\n async function transition(\n id: string,\n to: WorkProductStatus,\n patch: Omit<WorkProductPatch, 'status'> = {},\n eventMeta: Record<string, unknown> = {},\n ): Promise<WorkProductOutcome<WorkProductRecord>> {\n const record = await store.load(id)\n if (!record) return rejected(`Work product ${id} not found`)\n const from = record.status\n if (isWorkProductTerminal(from)) {\n return rejected(`Work product ${id} is terminal (${from}); cannot transition to ${to}`)\n }\n if (!canTransitionWorkProduct(from, to)) {\n return rejected(`Illegal work-product transition ${from} -> ${to} for ${id}`)\n }\n const updated = await store.update(\n id,\n { status: from, version: record.version },\n { status: to, updatedAt: now(), ...patch },\n )\n if (!updated) return lostRace(id)\n await appendEvent(updated, `wp.${to}`, `Work product ${from} -> ${to}`, { from, to, ...eventMeta })\n return { succeeded: true, value: updated }\n }\n\n // Merge-style guarded write with no status change (evidence / exceptions /\n // checks accumulate on the open row).\n async function guardedMerge(\n id: string,\n legalStatuses: readonly WorkProductStatus[],\n build: (record: WorkProductRecord) => Omit<WorkProductPatch, 'status' | 'version'>,\n event: { step: string; message: (record: WorkProductRecord) => string; metadata?: (record: WorkProductRecord) => Record<string, unknown> },\n ): Promise<WorkProductOutcome<WorkProductRecord>> {\n const record = await store.load(id)\n if (!record) return rejected(`Work product ${id} not found`)\n if (!legalStatuses.includes(record.status)) {\n return rejected(`Work product ${id} is ${record.status}; expected ${legalStatuses.join('/')}`)\n }\n const updated = await store.update(\n id,\n { status: record.status, version: record.version },\n { updatedAt: now(), ...build(record) },\n )\n if (!updated) return lostRace(id)\n await appendEvent(updated, event.step, event.message(updated), event.metadata?.(updated) ?? {})\n return { succeeded: true, value: updated }\n }\n\n const create: WorkProductService['create'] = async (input) => {\n const at = now()\n const record = await store.insert(\n {\n id: input.id ?? generateId(),\n workspaceId: input.workspaceId,\n threadId: input.threadId,\n scopeKey: input.scopeKey,\n status: 'draft',\n version: input.version ?? 1,\n artifact: null,\n evidence: [],\n exceptions: [],\n checks: [],\n provenance: input.provenance,\n history: [],\n createdAt: at,\n updatedAt: at,\n },\n input.extras,\n )\n await appendEvent(record, 'wp.created', `Work product draft v${record.version} created for ${record.scopeKey}`, {\n scopeKey: record.scopeKey,\n version: record.version,\n threadId: record.threadId,\n })\n return record\n }\n\n async function findByScopeAndStatus(\n workspaceId: string,\n scopeKey: string,\n status: WorkProductStatus,\n ): Promise<WorkProductRecord | null> {\n const rows = await store.listByWorkspace(workspaceId, { status: [status] })\n return rows.find((row) => row.scopeKey === scopeKey) ?? null\n }\n\n const nextVersion: WorkProductService['nextVersion'] = async (workspaceId, scopeKey) => {\n const reviewed = await store.listByWorkspace(workspaceId, { status: ['approved', 'superseded'] })\n const versions = reviewed.filter((row) => row.scopeKey === scopeKey).map((row) => row.version)\n return versions.length === 0 ? 1 : Math.max(...versions) + 1\n }\n\n const reopen: WorkProductService['reopen'] = async (id) => {\n const record = await store.load(id)\n if (!record) return rejected(`Work product ${id} not found`)\n if (record.status !== 'changes_requested') {\n return rejected(`Work product ${id} is ${record.status}; only changes_requested reopens`)\n }\n const updated = await store.update(\n id,\n { status: 'changes_requested', version: record.version },\n { status: 'draft', version: record.version + 1, updatedAt: now() },\n )\n if (!updated) return lostRace(id)\n await appendEvent(updated, 'wp.reopened', `Correction draft v${updated.version} opened`, {\n from: record.version,\n to: updated.version,\n })\n return { succeeded: true, value: updated }\n }\n\n const upsertEvidence: WorkProductService['upsertEvidence'] = (id, entries) =>\n guardedMerge(\n id,\n ['draft', 'blocked'],\n (record) => ({ evidence: mergeById(record.evidence, entries) }),\n {\n step: 'wp.evidence',\n message: (record) => `Evidence upserted (${entries.length} entries, ${record.evidence.length} total)`,\n metadata: () => ({ upserted: entries.map((entry) => entry.id) }),\n },\n )\n\n const upsertExceptions: WorkProductService['upsertExceptions'] = async (id, entries) => {\n const merged = await guardedMerge(\n id,\n ['draft', 'blocked'],\n (record) => ({ exceptions: mergeById(record.exceptions, entries) }),\n {\n step: 'wp.exception',\n message: (record) =>\n `Exceptions upserted (${entries.length} entries, ${unresolvedBlockingExceptions(record.exceptions).length} blocking unresolved)`,\n metadata: () => ({ upserted: entries.map((entry) => entry.id) }),\n },\n )\n if (!merged.succeeded) return merged\n // Reconcile the blocked flag AFTER the merge commits: an unresolved\n // blocking entry parks the draft; resolving the last one releases it.\n const record = merged.value\n const blocking = unresolvedBlockingExceptions(record.exceptions).length\n if (record.status === 'draft' && blocking > 0) {\n return transition(id, 'blocked', {}, { blocking })\n }\n if (record.status === 'blocked' && blocking === 0) {\n return transition(id, 'draft', {}, { blocking })\n }\n return merged\n }\n\n const recordChecks: WorkProductService['recordChecks'] = (id, checks) =>\n guardedMerge(\n id,\n ['draft', 'blocked'],\n () => ({ checks: checks.slice() }),\n {\n step: 'wp.checks',\n message: () => `Checks recorded (${checks.length}, ${checks.filter((check) => !check.passed).length} failed)`,\n metadata: () => ({ failed: checks.filter((check) => !check.passed).map((check) => check.name) }),\n },\n )\n\n const submit: WorkProductService['submit'] = async (id, input) => {\n const record = await store.load(id)\n if (!record) return rejected(`Work product ${id} not found`)\n if (record.status !== 'draft') {\n return rejected(`Work product ${id} is ${record.status}; only a draft submits`)\n }\n const blocking = unresolvedBlockingExceptions(record.exceptions)\n if (blocking.length > 0) {\n return rejected(\n `Work product ${id} has ${blocking.length} unresolved blocking exception(s): ${blocking.map((entry) => entry.id).join(', ')}`,\n )\n }\n const artifactPath = input.artifactPath ?? input.artifact.path\n const entry: WorkProductVersionEntry = {\n version: record.version,\n status: 'ready',\n provenance: input.provenance,\n ...(artifactPath === undefined ? {} : { artifactPath }),\n at: now(),\n }\n return transition(\n id,\n 'ready',\n {\n artifact: input.artifact,\n checks: input.checks.slice(),\n provenance: input.provenance,\n history: [...record.history, entry],\n },\n { version: record.version, failedChecks: input.checks.filter((check) => !check.passed).length },\n )\n }\n\n const applyVerdict: WorkProductService['applyVerdict'] = async (id, input) => {\n const record = await store.load(id)\n if (!record) return rejected(`Work product ${id} not found`)\n if (record.status !== 'ready') {\n return rejected(`Work product ${id} is ${record.status}; a verdict applies only to ready`)\n }\n const to: WorkProductStatus = input.verdict === 'approve' ? 'approved' : 'changes_requested'\n const entry: WorkProductVersionEntry = {\n version: record.version,\n status: to,\n provenance: record.provenance,\n ...(record.artifact?.path === undefined ? {} : { artifactPath: record.artifact.path }),\n reviewedBy: input.reviewedBy,\n ...(input.note === undefined ? {} : { reviewNote: input.note }),\n at: now(),\n }\n const outcome = await transition(\n id,\n to,\n { history: [...record.history, entry] },\n { verdict: input.verdict, reviewedBy: input.reviewedBy },\n )\n if (!outcome.succeeded || to !== 'approved') return outcome\n // Exactly one approved version per scope: supersede prior approved rows.\n const priorApproved = await store.listByWorkspace(record.workspaceId, { status: ['approved'] })\n for (const prior of priorApproved) {\n if (prior.id === id || prior.scopeKey !== record.scopeKey) continue\n await transition(prior.id, 'superseded', {}, { supersededBy: id })\n }\n return outcome\n }\n\n return {\n create,\n get: (id) => store.load(id),\n openDraft: (workspaceId, scopeKey) => store.findDraft(workspaceId, scopeKey),\n awaitingCorrection: (workspaceId, scopeKey) => findByScopeAndStatus(workspaceId, scopeKey, 'changes_requested'),\n awaitingReview: (workspaceId, scopeKey) => findByScopeAndStatus(workspaceId, scopeKey, 'ready'),\n nextVersion,\n reopen,\n upsertEvidence,\n upsertExceptions,\n recordChecks,\n submit,\n applyVerdict,\n supersede: (id) => transition(id, 'superseded'),\n }\n}\n\n// ── in-memory store ──────────────────────────────────────────────────────────\n\n/** In-memory store surface with audit trail access and unguarded direct writes for tests */\nexport interface InMemoryWorkProductStore extends WorkProductStorePort {\n /** The full audit trail, append order. */\n events(): WorkProductAuditEvent[]\n /** Unguarded direct write — simulates a concurrent owner in tests. */\n put(record: WorkProductRecord): void\n}\n\n/**\n * In-memory {@link WorkProductStorePort} — the portable backend for tests and\n * reference assemblies. Records are deep-copied on every boundary so callers\n * can never mutate stored state around the guards.\n */\nexport function createInMemoryWorkProductStore(): InMemoryWorkProductStore {\n const rows = new Map<string, WorkProductRecord>()\n const events: WorkProductAuditEvent[] = []\n\n return {\n async load(id) {\n const record = rows.get(id)\n return record ? structuredClone(record) : null\n },\n async findDraft(workspaceId, scopeKey) {\n for (const record of rows.values()) {\n if (\n record.workspaceId === workspaceId &&\n record.scopeKey === scopeKey &&\n (record.status === 'draft' || record.status === 'blocked')\n ) {\n return structuredClone(record)\n }\n }\n return null\n },\n async listByWorkspace(workspaceId, opts) {\n const out: WorkProductRecord[] = []\n for (const record of rows.values()) {\n if (record.workspaceId !== workspaceId) continue\n if (opts?.status && !opts.status.includes(record.status)) continue\n out.push(structuredClone(record))\n }\n return out\n },\n async insert(record) {\n if (rows.has(record.id)) throw new Error(`Work product ${record.id} already exists`)\n rows.set(record.id, structuredClone(record))\n return structuredClone(record)\n },\n async update(id, guard, patch) {\n const current = rows.get(id)\n if (!current) return null\n if (guard.status !== undefined && current.status !== guard.status) return null\n if (guard.version !== undefined && current.version !== guard.version) return null\n const next: WorkProductRecord = { ...current }\n if (patch.status !== undefined) next.status = patch.status\n if (patch.version !== undefined) next.version = patch.version\n if (patch.artifact !== undefined) next.artifact = patch.artifact\n if (patch.evidence !== undefined) next.evidence = patch.evidence\n if (patch.exceptions !== undefined) next.exceptions = patch.exceptions\n if (patch.checks !== undefined) next.checks = patch.checks\n if (patch.provenance !== undefined) next.provenance = patch.provenance\n if (patch.history !== undefined) next.history = patch.history\n if (patch.updatedAt !== undefined) next.updatedAt = patch.updatedAt\n rows.set(id, structuredClone(next))\n return structuredClone(next)\n },\n async appendEvent(event) {\n events.push(structuredClone(event))\n },\n events() {\n return events.map((event) => structuredClone(event))\n },\n put(record) {\n rows.set(record.id, structuredClone(record))\n },\n }\n}\n","/**\n * Provenance stamping — the backtest spine. Every version of every work\n * product carries `profileHash + runId + servingModels`, stamped at the two\n * honest moments:\n *\n * 1. At dispatch, the product route composes the turn's profile and computes\n * agent-eval's `agentProfileHash(profile)` — the SAME hash that keys the\n * app's scorecard cells — and closes it (plus runId/sessionId) into the\n * `provenance` seam `buildWorkProductTools` receives, so the model can\n * neither omit nor forge it.\n * 2. At turn completion, the route's existing lifecycle seam calls\n * {@link finalizeWorkProductProvenance}: serving model and cost come from\n * the usage receipt, because only the completed turn knows what actually\n * served. `servingModels` is honestly EMPTY until then.\n *\n * The trust bridge ({@link workProductTrustInputs}) maps judge-sourced\n * quality verdicts into `/eval-campaign`'s `trustVerdicts()` input with\n * type-only imports — products call `trustVerdicts` from\n * `@tangle-network/agent-app/eval-campaign` (which they already import for\n * their ensemble loop). Deliberately NOT a value re-export here: it would put\n * agent-eval's runtime on `/work-product`'s import path and drag the eval\n * engine into every product worker bundle.\n */\n\nimport type { JudgeVerdict } from '@tangle-network/agent-eval'\nimport type { TrustItem } from '../eval-campaign/trust-gate'\nimport type {\n WorkProductProvenance,\n WorkProductRecord,\n WorkProductStorePort,\n} from './types'\n\nexport type { TrustItem }\n\n/** The dispatch-time provenance closure's output: everything the route knows\n * before the turn completes. */\nexport type WorkProductProvenanceBase = Omit<WorkProductProvenance, 'servingModels' | 'producedAt'>\n\n/** Stamp a full provenance from the dispatch-time base. `servingModels` starts\n * empty (honestly absent — never guessed) until the completion back-fill. */\nexport function stampProvenance(base: WorkProductProvenanceBase, now: () => number = Date.now): WorkProductProvenance {\n return { ...base, servingModels: [], producedAt: now() }\n}\n\n/** Completion receipt for one run, from the turn's usage/lifecycle seam. */\nexport interface FinalizeWorkProductProvenanceInput {\n workspaceId: string\n /** The chat turnId / mission-step run id the records were stamped with. */\n runId: string\n /** What actually served, from the usage receipt / serving-model header. */\n servingModels: readonly string[]\n costUsd?: number\n logger?: Pick<Console, 'warn'>\n}\n\n/**\n * Back-fill `servingModels`/`costUsd` onto every record (and its history\n * entries) stamped with `runId` — wire it into the chat route's existing\n * `lifecycle.onTurnComplete` seam; no new hook. Returns the updated records.\n * A CAS miss on one record is logged and skipped (the next completion or a\n * re-read re-applies); it never throws mid-fleet.\n */\nexport async function finalizeWorkProductProvenance(\n store: WorkProductStorePort,\n input: FinalizeWorkProductProvenanceInput,\n): Promise<WorkProductRecord[]> {\n const rows = await store.listByWorkspace(input.workspaceId)\n const updated: WorkProductRecord[] = []\n for (const record of rows) {\n const recordMatches = record.provenance.runId === input.runId\n const historyMatches = record.history.some((entry) => entry.provenance.runId === input.runId)\n if (!recordMatches && !historyMatches) continue\n const finalize = (provenance: WorkProductProvenance): WorkProductProvenance => ({\n ...provenance,\n servingModels: [...input.servingModels],\n ...(input.costUsd === undefined ? {} : { costUsd: input.costUsd }),\n })\n const next = await store.update(\n record.id,\n { status: record.status, version: record.version },\n {\n ...(recordMatches ? { provenance: finalize(record.provenance) } : {}),\n history: record.history.map((entry) =>\n entry.provenance.runId === input.runId ? { ...entry, provenance: finalize(entry.provenance) } : entry,\n ),\n },\n )\n if (!next) {\n input.logger?.warn(`[work-product] provenance back-fill lost a race on ${record.id}; skipped`)\n continue\n }\n await store.appendEvent({\n workProductId: record.id,\n workspaceId: record.workspaceId,\n step: 'wp.provenance',\n message: `Serving models back-filled for run ${input.runId}`,\n metadata: { runId: input.runId, servingModels: [...input.servingModels], costUsd: input.costUsd ?? null },\n at: Date.now(),\n })\n updated.push(next)\n }\n return updated\n}\n\n/**\n * Trust-gate bridge: one `TrustItem` per work product whose production\n * quality was scored by the product's eval-campaign ensemble. `verdictsFor`\n * returns the per-judge raw verdicts the product retained for a record (the\n * same verdicts `aggregateJudgeVerdicts` reduces); records without verdicts\n * are omitted. Feed the result to `/eval-campaign`'s `trustVerdicts()` —\n * an untrusted verdict renders as \"quality: unverified\", never a naked\n * number. Zero statistics code here: pure mapping.\n */\nexport function workProductTrustInputs<D extends string = string>(\n records: readonly WorkProductRecord[],\n verdictsFor: (record: WorkProductRecord) => readonly JudgeVerdict<D>[] | undefined,\n): TrustItem<D>[] {\n const items: TrustItem<D>[] = []\n for (const record of records) {\n const verdicts = verdictsFor(record)\n if (!verdicts || verdicts.length === 0) continue\n items.push({ itemId: record.id, verdicts })\n }\n return items\n}\n","/**\n * The agent-facing work-product side channel — three registry `customTools`\n * built on `/tools`' `defineAppTool`, dispatched through `dispatchAppTool`'s\n * single validation/outcome path (a thrown `ToolInputError` → correctable 4xx\n * back to the model; any other throw → internal error; a call never silently\n * succeeds without its effect). Deliberately NOT new `/tools` built-ins:\n * extending `AppToolHandlers` would break every existing consumer, and the\n * registry seam exists precisely for a product tool family like this.\n *\n * Partial-emission contract across a long turn: the draft ROW is the\n * accumulator. Evidence and exceptions stream in as found via small batched\n * calls; the artifact arrives once at the end. The agent addresses everything\n * by `scopeKey` — the server mints row ids, the model never invents them, and\n * identity (userId/workspaceId/threadId) rides the trusted `AppToolContext`\n * from headers, never model args. A turn that dies mid-emission leaves a\n * consistent draft the next turn resumes by the same scopeKey.\n */\n\nimport { ToolInputError } from '../tools/errors'\nimport { defineAppTool, type AppToolDefinition } from '../tools/registry'\nimport type { AppToolContext } from '../tools/types'\nimport { createWorkProductService, type WorkProductOutcome, type WorkProductService } from './service'\nimport { stampProvenance, type WorkProductProvenanceBase } from './provenance'\nimport {\n parseAgentCheckInput,\n parseArtifactInput,\n parseEvidenceInput,\n parseExceptionInput,\n unresolvedBlockingExceptions,\n type EvidenceEntry,\n type ExceptionEntry,\n type QualityCheck,\n type WorkProductArtifact,\n type WorkProductRecord,\n type WorkProductStorePort,\n} from './types'\n\n/** Max entries per `upsert_evidence`/`flag_exception` call — keeps each call a\n * small batch the model can correct precisely on a named-index failure. */\nexport const MAX_WORK_PRODUCT_BATCH = 50\n\n/** The shell's one generic platform check: every material target has ≥1\n * evidence row. Recorded as `QualityCheck{source:'platform'}`. */\nexport const EVIDENCE_COVERAGE_CHECK = 'evidence_coverage'\n\n/** Domain seams for the three work-product tools — every domain word is a\n * parameter; the shell bakes none. */\nexport interface WorkProductToolConfig {\n store: WorkProductStorePort\n /** PARAMETER — accepted `artifact.kind` values, validated on submit. */\n artifactKinds: readonly string[]\n /** PARAMETER — accepted exception `kind` values. */\n exceptionKinds: readonly string[]\n /** Fail-loud source check: resolve an evidence `sourceRef` to existence\n * (vault stat / attachment lookup). A dangling ref is a `ToolInputError`\n * naming the entry index — lineage can never point at nothing. */\n resolveSourceRef: (ref: string, ctx: AppToolContext) => Promise<boolean>\n /** The material targets the platform coverage check requires evidence for.\n * Product-owned vocabulary; omit to skip the coverage gate. */\n materialTargets?: (artifact: WorkProductArtifact) => string[]\n /** Per-turn provenance closure the ROUTE supplies (profileHash + runId are\n * known at dispatch; trusted, never read from model args). */\n provenance: (ctx: AppToolContext) => WorkProductProvenanceBase\n /** Called on the draft→ready commit so the route persists the transcript\n * anchor part and the queue projection updates. */\n onReady?: (record: WorkProductRecord, ctx: AppToolContext) => void | Promise<void>\n /** Injectable clock / id generator (tests, deterministic ids). */\n now?: () => number\n generateId?: () => string\n}\n\n/** Unwrap a guarded outcome, retrying ONCE on a lost race (the single\n * serialized turn owner makes real contention rare; one re-read-and-retry\n * absorbs the benign case). A deterministic rejection maps to a correctable\n * `ToolInputError` so the model learns exactly why. */\nasync function unwrap<T>(\n run: () => Promise<WorkProductOutcome<T>>,\n code: string,\n): Promise<T> {\n let outcome = await run()\n if (!outcome.succeeded && outcome.conflict) outcome = await run()\n if (!outcome.succeeded) throw new ToolInputError(code, outcome.error, outcome.conflict ? 409 : 400)\n return outcome.value\n}\n\nfunction requireScopeKey(args: Record<string, unknown>): string {\n const scopeKey = typeof args.scopeKey === 'string' ? args.scopeKey.trim() : ''\n if (!scopeKey) throw new ToolInputError('missing_scope_key', 'scopeKey is required — the engagement key this work product belongs to.')\n return scopeKey\n}\n\nfunction requireBatch(args: Record<string, unknown>, field: string): unknown[] {\n const raw = args[field]\n if (!Array.isArray(raw) || raw.length === 0) {\n throw new ToolInputError('missing_entries', `${field} must be a non-empty array.`)\n }\n if (raw.length > MAX_WORK_PRODUCT_BATCH) {\n throw new ToolInputError('batch_too_large', `${field} accepts at most ${MAX_WORK_PRODUCT_BATCH} entries per call — send smaller batches.`)\n }\n return raw\n}\n\n/**\n * Resolve the scope's open draft row, creating or reopening as needed:\n * an open `draft`/`blocked` row resumes; a `changes_requested` row reopens as\n * the correction draft (version +1); a `ready` row refuses — the package is\n * awaiting review and must receive a verdict first; otherwise a fresh draft\n * is created at `last reviewed version + 1` with dispatch-stamped provenance.\n */\nasync function resolveDraft(\n service: WorkProductService,\n config: WorkProductToolConfig,\n scopeKey: string,\n ctx: AppToolContext,\n): Promise<WorkProductRecord> {\n const open = await service.openDraft(ctx.workspaceId, scopeKey)\n if (open) return open\n const awaitingReview = await service.awaitingReview(ctx.workspaceId, scopeKey)\n if (awaitingReview) {\n throw new ToolInputError(\n 'awaiting_review',\n `Work product for ${scopeKey} (v${awaitingReview.version}) is awaiting review — no further emission until a reviewer verdict.`,\n 409,\n )\n }\n const awaitingCorrection = await service.awaitingCorrection(ctx.workspaceId, scopeKey)\n if (awaitingCorrection) {\n return unwrap(() => service.reopen(awaitingCorrection.id), 'reopen_failed')\n }\n return service.create({\n workspaceId: ctx.workspaceId,\n threadId: ctx.threadId,\n scopeKey,\n version: await service.nextVersion(ctx.workspaceId, scopeKey),\n provenance: stampProvenance(config.provenance(ctx), config.now),\n })\n}\n\n/** Build the three work-product tools for `customTools` registration on the\n * MCP server / HTTP handler / runtime executor. */\nexport function buildWorkProductTools(config: WorkProductToolConfig): AppToolDefinition[] {\n const service = createWorkProductService({\n store: config.store,\n ...(config.now ? { now: config.now } : {}),\n ...(config.generateId ? { generateId: config.generateId } : {}),\n })\n\n const upsertEvidence = defineAppTool({\n name: 'upsert_evidence',\n description:\n 'Record source→field lineage for the current work product, incrementally as you find it. Each entry links a source document (sourceRef + locator) to one artifact target and states the claim it supports. Re-emitting an entry id replaces that entry. Address the work product by scopeKey; the first call creates the draft.',\n parameters: {\n type: 'object',\n properties: {\n scopeKey: { type: 'string', description: 'Engagement key for this work product.' },\n entries: {\n type: 'array',\n minItems: 1,\n maxItems: MAX_WORK_PRODUCT_BATCH,\n items: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Stable entry id — re-emit to replace.' },\n sourceRef: { type: 'string', description: 'Vault path / attachment id of the SOURCE document.' },\n locator: {\n type: 'object',\n properties: {\n page: { type: 'number' },\n range: { type: 'string', description: \"Free-form location: 'L120-L134' | 'B7' | '¶4'.\" },\n quote: { type: 'string', description: 'Verbatim supporting quote.' },\n },\n },\n target: { type: 'string', description: 'Artifact field/claim this evidence supports.' },\n claim: { type: 'string', description: 'The value/assertion at the target.' },\n confidence: { type: 'number', minimum: 0, maximum: 1 },\n },\n required: ['id', 'sourceRef', 'target', 'claim'],\n },\n },\n },\n required: ['scopeKey', 'entries'],\n },\n async execute(args: Record<string, unknown>, ctx: AppToolContext) {\n const scopeKey = requireScopeKey(args)\n const raw = requireBatch(args, 'entries')\n const entries: EvidenceEntry[] = []\n for (let index = 0; index < raw.length; index += 1) {\n const parsed = parseEvidenceInput(raw[index], `entries[${index}]`)\n if (!parsed.ok) throw new ToolInputError('invalid_evidence', `${parsed.field}: ${parsed.error}`)\n entries.push(parsed.value)\n }\n // Fail-loud source resolution: lineage can never point at nothing.\n for (let index = 0; index < entries.length; index += 1) {\n const entry = entries[index]!\n if (!(await config.resolveSourceRef(entry.sourceRef, ctx))) {\n throw new ToolInputError(\n 'unknown_source_ref',\n `entries[${index}].sourceRef: \"${entry.sourceRef}\" does not resolve to an existing source document.`,\n )\n }\n }\n const draft = await resolveDraft(service, config, scopeKey, ctx)\n const record = await unwrap(() => service.upsertEvidence(draft.id, entries), 'evidence_rejected')\n return { workProductId: record.id, version: record.version, evidenceCount: record.evidence.length }\n },\n })\n\n const flagException = defineAppTool({\n name: 'flag_exception',\n description:\n 'Flag problems with the current work product (missing documents, inconsistent sources, …). An unresolved blocking exception parks the work product until it is resolved; re-emit the same id with resolved:true to release it. Address by scopeKey.',\n parameters: {\n type: 'object',\n properties: {\n scopeKey: { type: 'string', description: 'Engagement key for this work product.' },\n exceptions: {\n type: 'array',\n minItems: 1,\n maxItems: MAX_WORK_PRODUCT_BATCH,\n items: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Stable entry id — re-emit to replace.' },\n severity: { type: 'string', enum: ['blocking', 'material', 'advisory'] },\n kind: { type: 'string', description: 'Exception kind from the product vocabulary.' },\n message: { type: 'string' },\n targets: { type: 'array', items: { type: 'string' } },\n resolved: { type: 'boolean' },\n resolutionNote: { type: 'string' },\n },\n required: ['id', 'severity', 'kind', 'message'],\n },\n },\n },\n required: ['scopeKey', 'exceptions'],\n },\n async execute(args: Record<string, unknown>, ctx: AppToolContext) {\n const scopeKey = requireScopeKey(args)\n const raw = requireBatch(args, 'exceptions')\n const entries: ExceptionEntry[] = []\n for (let index = 0; index < raw.length; index += 1) {\n const parsed = parseExceptionInput(raw[index], `exceptions[${index}]`)\n if (!parsed.ok) throw new ToolInputError('invalid_exception', `${parsed.field}: ${parsed.error}`)\n if (!config.exceptionKinds.includes(parsed.value.kind)) {\n throw new ToolInputError(\n 'invalid_exception',\n `exceptions[${index}].kind: must be one of: ${config.exceptionKinds.join(', ')}`,\n )\n }\n // An agent resolving its own exception is tagged as such; the reviewer\n // tag is reserved for the verdict route.\n if (parsed.value.resolved && parsed.value.resolvedBy === undefined) parsed.value.resolvedBy = 'agent'\n entries.push(parsed.value)\n }\n const draft = await resolveDraft(service, config, scopeKey, ctx)\n const record = await unwrap(() => service.upsertExceptions(draft.id, entries), 'exception_rejected')\n return {\n workProductId: record.id,\n version: record.version,\n status: record.status,\n unresolvedBlocking: unresolvedBlockingExceptions(record.exceptions).length,\n }\n },\n })\n\n const submitWorkProduct = defineAppTool({\n name: 'submit_work_product',\n description:\n 'Submit the finished work product for professional review — the terminal call after evidence and exceptions are recorded. Refused while a blocking exception is unresolved, or when a material target lacks evidence. Include your own quality checks in `checks`.',\n parameters: {\n type: 'object',\n properties: {\n scopeKey: { type: 'string', description: 'Engagement key for this work product.' },\n artifact: {\n type: 'object',\n properties: {\n kind: { type: 'string', description: 'Artifact kind from the product vocabulary.' },\n title: { type: 'string' },\n path: { type: 'string', description: 'Vault/object-store ref of the rendered document.' },\n content: { type: 'string', description: 'Inline body when small (markdown/JSON).' },\n mediaType: { type: 'string' },\n baseline: {\n type: 'object',\n properties: { path: { type: 'string' }, content: { type: 'string' } },\n description: 'For diff-first artifacts: the source document being redlined.',\n },\n fields: { type: 'object', description: 'Structured field map lineage targets anchor to.' },\n },\n required: ['kind', 'title'],\n },\n checks: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n name: { type: 'string' },\n passed: { type: 'boolean' },\n detail: { type: 'string' },\n },\n required: ['id', 'name', 'passed'],\n },\n description: 'Your own quality self-checks (recorded as agent-sourced).',\n },\n },\n required: ['scopeKey', 'artifact'],\n },\n async execute(args: Record<string, unknown>, ctx: AppToolContext) {\n const scopeKey = requireScopeKey(args)\n const parsedArtifact = parseArtifactInput(args.artifact)\n if (!parsedArtifact.ok) throw new ToolInputError('invalid_artifact', `${parsedArtifact.field}: ${parsedArtifact.error}`)\n const artifact = parsedArtifact.value\n if (!config.artifactKinds.includes(artifact.kind)) {\n throw new ToolInputError('invalid_artifact', `artifact.kind: must be one of: ${config.artifactKinds.join(', ')}`)\n }\n const agentChecks: QualityCheck[] = []\n if (args.checks !== undefined) {\n if (!Array.isArray(args.checks)) throw new ToolInputError('invalid_checks', 'checks must be an array when present.')\n for (let index = 0; index < args.checks.length; index += 1) {\n const parsed = parseAgentCheckInput(args.checks[index], `checks[${index}]`)\n if (!parsed.ok) throw new ToolInputError('invalid_checks', `${parsed.field}: ${parsed.error}`)\n agentChecks.push({ ...parsed.value, source: 'agent' })\n }\n }\n\n const draft = await resolveDraft(service, config, scopeKey, ctx)\n const blocking = unresolvedBlockingExceptions(draft.exceptions)\n if (blocking.length > 0) {\n throw new ToolInputError(\n 'blocking_exceptions_unresolved',\n `Cannot submit: ${blocking.length} unresolved blocking exception(s) (${blocking.map((entry) => entry.id).join(', ')}). Resolve each via flag_exception with resolved:true, or downgrade its severity with a resolutionNote justifying why it does not block.`,\n 409,\n )\n }\n\n // The shell's ONE generic platform check: every material target has ≥1\n // evidence row. A failing coverage check is BOTH recorded on the row\n // (visible to the queue) and rejected fail-loud — a lineage-free package\n // can never reach a reviewer.\n const checks: QualityCheck[] = [...agentChecks]\n if (config.materialTargets) {\n const targets = config.materialTargets(artifact)\n const covered = new Set(draft.evidence.map((entry) => entry.target))\n const missing = targets.filter((target) => !covered.has(target))\n const coverage: QualityCheck = {\n id: EVIDENCE_COVERAGE_CHECK,\n name: EVIDENCE_COVERAGE_CHECK,\n passed: missing.length === 0,\n detail:\n missing.length === 0\n ? `${targets.length}/${targets.length} material targets evidenced`\n : `Missing evidence for: ${missing.join(', ')}`,\n source: 'platform',\n }\n checks.unshift(coverage)\n if (missing.length > 0) {\n await unwrap(() => service.recordChecks(draft.id, checks), 'checks_rejected')\n throw new ToolInputError(\n 'evidence_coverage_failed',\n `Cannot submit: material targets lack evidence: ${missing.join(', ')}. Add upsert_evidence entries targeting each, then resubmit.`,\n )\n }\n }\n\n const provenance = stampProvenance(config.provenance(ctx), config.now)\n const record = await unwrap(\n () => service.submit(draft.id, { artifact, checks, provenance }),\n 'submit_rejected',\n )\n await config.onReady?.(record, ctx)\n return {\n workProductId: record.id,\n version: record.version,\n status: record.status,\n checks: record.checks.map((check) => ({ name: check.name, passed: check.passed, source: check.source })),\n }\n },\n })\n\n return [upsertEvidence, flagException, submitWorkProduct]\n}\n","/**\n * Framework-neutral work-product review endpoints — the\n * `createInteractionAnswerRoute` factory pattern: web-standard\n * `Request`/`Response`, ONE product-supplied `authorize` seam (session auth,\n * workspace access, reviewer identity, rate limits), everything behind it\n * mechanism. Server-only and subpath-only: never reachable from a client\n * bundle (enforced by the browser-safety test).\n *\n * The verdict endpoint is deliberately NOT a second approval broker: agent\n * asks stay on `/interactions`; this is the REVIEWER's plain authorized\n * verdict on a ready package — one human-in-the-loop channel per direction.\n * A `request_changes` note re-enters chat as the correction turn via the\n * `onVerdict` seam; chat remains the driver surface.\n */\n\nimport {\n createWorkProductService,\n type WorkProductService,\n} from './service'\nimport {\n workProductToPersistedPart,\n type WorkProductPersistedPart,\n type WorkProductRecord,\n type WorkProductStorePort,\n} from './types'\n\n/** Reviewer verdict wire body for the POST endpoint. */\nexport type WorkProductVerdictBody =\n | { ok: true; id: string; verdict: 'approve'; note?: string }\n | { ok: true; id: string; verdict: 'request_changes'; note: string }\n | { ok: false; error: string }\n\n/** Validate the verdict POST body: `{ id, verdict, note? }`; a\n * `request_changes` verdict REQUIRES a non-empty note — the note IS the\n * correction turn the agent works from. */\nexport function validateWorkProductVerdictBody(body: Record<string, unknown>): WorkProductVerdictBody {\n const id = typeof body.id === 'string' && body.id.trim() ? body.id.trim() : null\n if (!id) return { ok: false, error: 'Missing work product id' }\n const verdict = body.verdict\n if (verdict !== 'approve' && verdict !== 'request_changes') {\n return { ok: false, error: 'Invalid verdict: expected approve or request_changes' }\n }\n const note = body.note === undefined ? undefined : typeof body.note === 'string' ? body.note.trim() : null\n if (note === null) return { ok: false, error: 'Invalid note: expected a string' }\n if (verdict === 'request_changes') {\n if (!note) return { ok: false, error: 'request_changes requires a note — it becomes the correction instruction in chat' }\n return { ok: true, id, verdict, note }\n }\n return note ? { ok: true, id, verdict, note } : { ok: true, id, verdict }\n}\n\n/** The product seam's verdict for one request: authenticated reviewer +\n * workspace, or a product-authored short-circuit Response (401/403/429…). */\nexport type WorkProductRouteAuthorization =\n | { ok: true; workspaceId: string; reviewedBy: string }\n | { ok: false; response: Response }\n\n/** Auth seam arguments carrying the request, the endpoint intent, and the parsed verdict body */\nexport interface WorkProductAuthorizeArgs {\n request: Request\n intent: 'list' | 'detail' | 'verdict'\n /** The parsed, validated POST body (verdict intent only). */\n body?: Record<string, unknown>\n}\n\n/** Configuration options assembling the review endpoints from the store and product seams */\nexport interface WorkProductRoutesOptions {\n store: WorkProductStorePort\n /** Authenticate + authorize the caller; the ONLY product access step. */\n authorize: (args: WorkProductAuthorizeArgs) => Promise<WorkProductRouteAuthorization>\n /** Post-verdict product seam: post the `request_changes` note into the\n * driving chat thread as the correction turn, notify, etc. Runs after the\n * transition commits; a throw is logged, never unwinds the verdict. */\n onVerdict?: (args: {\n record: WorkProductRecord\n verdict: 'approve' | 'request_changes'\n note?: string\n reviewedBy: string\n }) => void | Promise<void>\n /** Persist/update the transcript anchor part reflecting the new status, so\n * the chat card flips with the verdict. */\n persistAnchorPart?: (part: WorkProductPersistedPart, record: WorkProductRecord) => void | Promise<void>\n /** Future integration point fired on approval (push to a DMS/CRM/export\n * pipeline). A stub seam by design — export itself stays the product's\n * signed object-store download. */\n onExport?: (record: WorkProductRecord) => void | Promise<void>\n logger?: Pick<Console, 'warn' | 'error'>\n now?: () => number\n}\n\n/** Assembled review endpoints returning web-standard Responses */\nexport interface WorkProductRoutes {\n /** GET — the workspace's records for the queue projection. Optional\n * `?status=a,b` filter. */\n list: (request: Request) => Promise<Response>\n /** GET — one record by id (404 when absent or outside the workspace). */\n detail: (request: Request, id: string) => Promise<Response>\n /** POST `{ id, verdict, note? }` — the reviewer verdict: CAS transition +\n * history entry + product seams. 409 when the record is no longer ready. */\n verdict: (request: Request) => Promise<Response>\n}\n\n/** Create the work-product review endpoints over the store port and product seams */\nexport function createWorkProductRoutes(options: WorkProductRoutesOptions): WorkProductRoutes {\n const logger = options.logger ?? console\n const service: WorkProductService = createWorkProductService({\n store: options.store,\n ...(options.now ? { now: options.now } : {}),\n })\n\n async function list(request: Request): Promise<Response> {\n const auth = await options.authorize({ request, intent: 'list' })\n if (!auth.ok) return auth.response\n const url = new URL(request.url)\n const statusParam = url.searchParams.get('status')\n const statuses = statusParam\n ? statusParam.split(',').map((value) => value.trim()).filter(Boolean)\n : null\n const workProducts = await options.store.listByWorkspace(\n auth.workspaceId,\n statuses ? { status: statuses as WorkProductRecord['status'][] } : undefined,\n )\n return Response.json({ workProducts })\n }\n\n async function detail(request: Request, id: string): Promise<Response> {\n const auth = await options.authorize({ request, intent: 'detail' })\n if (!auth.ok) return auth.response\n const record = await options.store.load(id)\n if (!record || record.workspaceId !== auth.workspaceId) {\n return Response.json({ error: 'Work product not found' }, { status: 404 })\n }\n return Response.json({ workProduct: record })\n }\n\n async function verdict(request: Request): Promise<Response> {\n const body = (await request.json().catch(() => null)) as Record<string, unknown> | null\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n return Response.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const validation = validateWorkProductVerdictBody(body)\n if (!validation.ok) return Response.json({ error: validation.error }, { status: 400 })\n\n const auth = await options.authorize({ request, intent: 'verdict', body })\n if (!auth.ok) return auth.response\n\n const existing = await options.store.load(validation.id)\n if (!existing || existing.workspaceId !== auth.workspaceId) {\n return Response.json({ error: 'Work product not found' }, { status: 404 })\n }\n\n const outcome = await service.applyVerdict(validation.id, {\n verdict: validation.verdict,\n reviewedBy: auth.reviewedBy,\n ...(validation.note === undefined ? {} : { note: validation.note }),\n })\n if (!outcome.succeeded) {\n // A lost race and an illegal edge both mean \"the row is no longer the\n // ready version you looked at\" — 409 so the client re-reads.\n return Response.json({ code: 'VERDICT_CONFLICT', error: outcome.error }, { status: 409 })\n }\n const record = outcome.value\n\n // Product seams run AFTER the committed transition; their failures are\n // logged, never unwound — the verdict is durable truth at this point.\n try {\n await options.persistAnchorPart?.(workProductToPersistedPart(record), record)\n } catch (error) {\n logger.error('[work-product] persistAnchorPart failed:', error)\n }\n try {\n await options.onVerdict?.({\n record,\n verdict: validation.verdict,\n ...(validation.note === undefined ? {} : { note: validation.note }),\n reviewedBy: auth.reviewedBy,\n })\n } catch (error) {\n logger.error('[work-product] onVerdict failed:', error)\n }\n if (validation.verdict === 'approve') {\n try {\n await options.onExport?.(record)\n } catch (error) {\n logger.error('[work-product] onExport failed:', error)\n }\n }\n\n return Response.json({ ok: true, workProduct: record })\n }\n\n return { list, detail, verdict }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAuCA,IAAM,2BAAsF;AAAA,EAC1F,OAAO,oBAAI,IAAuB,CAAC,WAAW,OAAO,CAAC;AAAA,EACtD,SAAS,oBAAI,IAAuB,CAAC,OAAO,CAAC;AAAA,EAC7C,OAAO,oBAAI,IAAuB,CAAC,qBAAqB,YAAY,YAAY,CAAC;AAAA,EACjF,mBAAmB,oBAAI,IAAuB,CAAC,SAAS,YAAY,CAAC;AAAA,EACrE,UAAU,oBAAI,IAAuB,CAAC,YAAY,CAAC;AAAA,EACnD,YAAY,oBAAI,IAAuB;AACzC;AAGO,SAAS,yBAAyB,MAAyB,IAAgC;AAChG,SAAO,yBAAyB,IAAI,EAAE,IAAI,EAAE;AAC9C;AAGO,SAAS,sBAAsB,QAAoC;AACxE,SAAO,yBAAyB,MAAM,EAAE,SAAS;AACnD;AAmFA,SAAS,SAAY,OAAsC;AACzD,SAAO,EAAE,WAAW,OAAO,OAAO,UAAU,MAAM;AACpD;AAEA,SAAS,SAAY,IAAmC;AACtD,SAAO,EAAE,WAAW,OAAO,OAAO,gBAAgB,EAAE,yBAAyB,UAAU,KAAK;AAC9F;AAIA,SAAS,UAAoC,UAAwB,UAA6B;AAChG,QAAM,SAAS,SAAS,MAAM;AAC9B,QAAM,YAAY,IAAI,IAAI,OAAO,IAAI,CAAC,OAAO,UAAU,CAAC,MAAM,IAAI,KAAK,CAAU,CAAC;AAClF,aAAW,SAAS,UAAU;AAC5B,UAAM,KAAK,UAAU,IAAI,MAAM,EAAE;AACjC,QAAI,OAAO,QAAW;AACpB,gBAAU,IAAI,MAAM,IAAI,OAAO,MAAM;AACrC,aAAO,KAAK,KAAK;AAAA,IACnB,OAAO;AACL,aAAO,EAAE,IAAI;AAAA,IACf;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,yBAAyB,SAAwD;AAC/F,QAAM,EAAE,MAAM,IAAI;AAClB,QAAM,MAAM,QAAQ,QAAQ,MAAM,KAAK,IAAI;AAC3C,QAAM,aAAa,QAAQ,eAAe,MAAM,OAAO,WAAW;AAElE,iBAAe,YACb,QACA,MACA,SACA,WAAoC,CAAC,GACtB;AACf,UAAM,MAAM,YAAY;AAAA,MACtB,eAAe,OAAO;AAAA,MACtB,aAAa,OAAO;AAAA,MACpB;AAAA,MACA;AAAA,MACA;AAAA,MACA,IAAI,IAAI;AAAA,IACV,CAAC;AAAA,EACH;AAKA,iBAAe,WACb,IACA,IACA,QAA0C,CAAC,GAC3C,YAAqC,CAAC,GACU;AAChD,UAAM,SAAS,MAAM,MAAM,KAAK,EAAE;AAClC,QAAI,CAAC,OAAQ,QAAO,SAAS,gBAAgB,EAAE,YAAY;AAC3D,UAAM,OAAO,OAAO;AACpB,QAAI,sBAAsB,IAAI,GAAG;AAC/B,aAAO,SAAS,gBAAgB,EAAE,iBAAiB,IAAI,2BAA2B,EAAE,EAAE;AAAA,IACxF;AACA,QAAI,CAAC,yBAAyB,MAAM,EAAE,GAAG;AACvC,aAAO,SAAS,mCAAmC,IAAI,OAAO,EAAE,QAAQ,EAAE,EAAE;AAAA,IAC9E;AACA,UAAM,UAAU,MAAM,MAAM;AAAA,MAC1B;AAAA,MACA,EAAE,QAAQ,MAAM,SAAS,OAAO,QAAQ;AAAA,MACxC,EAAE,QAAQ,IAAI,WAAW,IAAI,GAAG,GAAG,MAAM;AAAA,IAC3C;AACA,QAAI,CAAC,QAAS,QAAO,SAAS,EAAE;AAChC,UAAM,YAAY,SAAS,MAAM,EAAE,IAAI,gBAAgB,IAAI,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,GAAG,UAAU,CAAC;AAClG,WAAO,EAAE,WAAW,MAAM,OAAO,QAAQ;AAAA,EAC3C;AAIA,iBAAe,aACb,IACA,eACA,OACA,OACgD;AAChD,UAAM,SAAS,MAAM,MAAM,KAAK,EAAE;AAClC,QAAI,CAAC,OAAQ,QAAO,SAAS,gBAAgB,EAAE,YAAY;AAC3D,QAAI,CAAC,cAAc,SAAS,OAAO,MAAM,GAAG;AAC1C,aAAO,SAAS,gBAAgB,EAAE,OAAO,OAAO,MAAM,cAAc,cAAc,KAAK,GAAG,CAAC,EAAE;AAAA,IAC/F;AACA,UAAM,UAAU,MAAM,MAAM;AAAA,MAC1B;AAAA,MACA,EAAE,QAAQ,OAAO,QAAQ,SAAS,OAAO,QAAQ;AAAA,MACjD,EAAE,WAAW,IAAI,GAAG,GAAG,MAAM,MAAM,EAAE;AAAA,IACvC;AACA,QAAI,CAAC,QAAS,QAAO,SAAS,EAAE;AAChC,UAAM,YAAY,SAAS,MAAM,MAAM,MAAM,QAAQ,OAAO,GAAG,MAAM,WAAW,OAAO,KAAK,CAAC,CAAC;AAC9F,WAAO,EAAE,WAAW,MAAM,OAAO,QAAQ;AAAA,EAC3C;AAEA,QAAM,SAAuC,OAAO,UAAU;AAC5D,UAAM,KAAK,IAAI;AACf,UAAM,SAAS,MAAM,MAAM;AAAA,MACzB;AAAA,QACE,IAAI,MAAM,MAAM,WAAW;AAAA,QAC3B,aAAa,MAAM;AAAA,QACnB,UAAU,MAAM;AAAA,QAChB,UAAU,MAAM;AAAA,QAChB,QAAQ;AAAA,QACR,SAAS,MAAM,WAAW;AAAA,QAC1B,UAAU;AAAA,QACV,UAAU,CAAC;AAAA,QACX,YAAY,CAAC;AAAA,QACb,QAAQ,CAAC;AAAA,QACT,YAAY,MAAM;AAAA,QAClB,SAAS,CAAC;AAAA,QACV,WAAW;AAAA,QACX,WAAW;AAAA,MACb;AAAA,MACA,MAAM;AAAA,IACR;AACA,UAAM,YAAY,QAAQ,cAAc,uBAAuB,OAAO,OAAO,gBAAgB,OAAO,QAAQ,IAAI;AAAA,MAC9G,UAAU,OAAO;AAAA,MACjB,SAAS,OAAO;AAAA,MAChB,UAAU,OAAO;AAAA,IACnB,CAAC;AACD,WAAO;AAAA,EACT;AAEA,iBAAe,qBACb,aACA,UACA,QACmC;AACnC,UAAM,OAAO,MAAM,MAAM,gBAAgB,aAAa,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;AAC1E,WAAO,KAAK,KAAK,CAAC,QAAQ,IAAI,aAAa,QAAQ,KAAK;AAAA,EAC1D;AAEA,QAAM,cAAiD,OAAO,aAAa,aAAa;AACtF,UAAM,WAAW,MAAM,MAAM,gBAAgB,aAAa,EAAE,QAAQ,CAAC,YAAY,YAAY,EAAE,CAAC;AAChG,UAAM,WAAW,SAAS,OAAO,CAAC,QAAQ,IAAI,aAAa,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,OAAO;AAC7F,WAAO,SAAS,WAAW,IAAI,IAAI,KAAK,IAAI,GAAG,QAAQ,IAAI;AAAA,EAC7D;AAEA,QAAM,SAAuC,OAAO,OAAO;AACzD,UAAM,SAAS,MAAM,MAAM,KAAK,EAAE;AAClC,QAAI,CAAC,OAAQ,QAAO,SAAS,gBAAgB,EAAE,YAAY;AAC3D,QAAI,OAAO,WAAW,qBAAqB;AACzC,aAAO,SAAS,gBAAgB,EAAE,OAAO,OAAO,MAAM,kCAAkC;AAAA,IAC1F;AACA,UAAM,UAAU,MAAM,MAAM;AAAA,MAC1B;AAAA,MACA,EAAE,QAAQ,qBAAqB,SAAS,OAAO,QAAQ;AAAA,MACvD,EAAE,QAAQ,SAAS,SAAS,OAAO,UAAU,GAAG,WAAW,IAAI,EAAE;AAAA,IACnE;AACA,QAAI,CAAC,QAAS,QAAO,SAAS,EAAE;AAChC,UAAM,YAAY,SAAS,eAAe,qBAAqB,QAAQ,OAAO,WAAW;AAAA,MACvF,MAAM,OAAO;AAAA,MACb,IAAI,QAAQ;AAAA,IACd,CAAC;AACD,WAAO,EAAE,WAAW,MAAM,OAAO,QAAQ;AAAA,EAC3C;AAEA,QAAM,iBAAuD,CAAC,IAAI,YAChE;AAAA,IACE;AAAA,IACA,CAAC,SAAS,SAAS;AAAA,IACnB,CAAC,YAAY,EAAE,UAAU,UAAU,OAAO,UAAU,OAAO,EAAE;AAAA,IAC7D;AAAA,MACE,MAAM;AAAA,MACN,SAAS,CAAC,WAAW,sBAAsB,QAAQ,MAAM,aAAa,OAAO,SAAS,MAAM;AAAA,MAC5F,UAAU,OAAO,EAAE,UAAU,QAAQ,IAAI,CAAC,UAAU,MAAM,EAAE,EAAE;AAAA,IAChE;AAAA,EACF;AAEF,QAAM,mBAA2D,OAAO,IAAI,YAAY;AACtF,UAAM,SAAS,MAAM;AAAA,MACnB;AAAA,MACA,CAAC,SAAS,SAAS;AAAA,MACnB,CAACA,aAAY,EAAE,YAAY,UAAUA,QAAO,YAAY,OAAO,EAAE;AAAA,MACjE;AAAA,QACE,MAAM;AAAA,QACN,SAAS,CAACA,YACR,wBAAwB,QAAQ,MAAM,aAAa,6BAA6BA,QAAO,UAAU,EAAE,MAAM;AAAA,QAC3G,UAAU,OAAO,EAAE,UAAU,QAAQ,IAAI,CAAC,UAAU,MAAM,EAAE,EAAE;AAAA,MAChE;AAAA,IACF;AACA,QAAI,CAAC,OAAO,UAAW,QAAO;AAG9B,UAAM,SAAS,OAAO;AACtB,UAAM,WAAW,6BAA6B,OAAO,UAAU,EAAE;AACjE,QAAI,OAAO,WAAW,WAAW,WAAW,GAAG;AAC7C,aAAO,WAAW,IAAI,WAAW,CAAC,GAAG,EAAE,SAAS,CAAC;AAAA,IACnD;AACA,QAAI,OAAO,WAAW,aAAa,aAAa,GAAG;AACjD,aAAO,WAAW,IAAI,SAAS,CAAC,GAAG,EAAE,SAAS,CAAC;AAAA,IACjD;AACA,WAAO;AAAA,EACT;AAEA,QAAM,eAAmD,CAAC,IAAI,WAC5D;AAAA,IACE;AAAA,IACA,CAAC,SAAS,SAAS;AAAA,IACnB,OAAO,EAAE,QAAQ,OAAO,MAAM,EAAE;AAAA,IAChC;AAAA,MACE,MAAM;AAAA,MACN,SAAS,MAAM,oBAAoB,OAAO,MAAM,KAAK,OAAO,OAAO,CAAC,UAAU,CAAC,MAAM,MAAM,EAAE,MAAM;AAAA,MACnG,UAAU,OAAO,EAAE,QAAQ,OAAO,OAAO,CAAC,UAAU,CAAC,MAAM,MAAM,EAAE,IAAI,CAAC,UAAU,MAAM,IAAI,EAAE;AAAA,IAChG;AAAA,EACF;AAEF,QAAM,SAAuC,OAAO,IAAI,UAAU;AAChE,UAAM,SAAS,MAAM,MAAM,KAAK,EAAE;AAClC,QAAI,CAAC,OAAQ,QAAO,SAAS,gBAAgB,EAAE,YAAY;AAC3D,QAAI,OAAO,WAAW,SAAS;AAC7B,aAAO,SAAS,gBAAgB,EAAE,OAAO,OAAO,MAAM,wBAAwB;AAAA,IAChF;AACA,UAAM,WAAW,6BAA6B,OAAO,UAAU;AAC/D,QAAI,SAAS,SAAS,GAAG;AACvB,aAAO;AAAA,QACL,gBAAgB,EAAE,QAAQ,SAAS,MAAM,sCAAsC,SAAS,IAAI,CAACC,WAAUA,OAAM,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,MAC7H;AAAA,IACF;AACA,UAAM,eAAe,MAAM,gBAAgB,MAAM,SAAS;AAC1D,UAAM,QAAiC;AAAA,MACrC,SAAS,OAAO;AAAA,MAChB,QAAQ;AAAA,MACR,YAAY,MAAM;AAAA,MAClB,GAAI,iBAAiB,SAAY,CAAC,IAAI,EAAE,aAAa;AAAA,MACrD,IAAI,IAAI;AAAA,IACV;AACA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,QACE,UAAU,MAAM;AAAA,QAChB,QAAQ,MAAM,OAAO,MAAM;AAAA,QAC3B,YAAY,MAAM;AAAA,QAClB,SAAS,CAAC,GAAG,OAAO,SAAS,KAAK;AAAA,MACpC;AAAA,MACA,EAAE,SAAS,OAAO,SAAS,cAAc,MAAM,OAAO,OAAO,CAAC,UAAU,CAAC,MAAM,MAAM,EAAE,OAAO;AAAA,IAChG;AAAA,EACF;AAEA,QAAM,eAAmD,OAAO,IAAI,UAAU;AAC5E,UAAM,SAAS,MAAM,MAAM,KAAK,EAAE;AAClC,QAAI,CAAC,OAAQ,QAAO,SAAS,gBAAgB,EAAE,YAAY;AAC3D,QAAI,OAAO,WAAW,SAAS;AAC7B,aAAO,SAAS,gBAAgB,EAAE,OAAO,OAAO,MAAM,mCAAmC;AAAA,IAC3F;AACA,UAAM,KAAwB,MAAM,YAAY,YAAY,aAAa;AACzE,UAAM,QAAiC;AAAA,MACrC,SAAS,OAAO;AAAA,MAChB,QAAQ;AAAA,MACR,YAAY,OAAO;AAAA,MACnB,GAAI,OAAO,UAAU,SAAS,SAAY,CAAC,IAAI,EAAE,cAAc,OAAO,SAAS,KAAK;AAAA,MACpF,YAAY,MAAM;AAAA,MAClB,GAAI,MAAM,SAAS,SAAY,CAAC,IAAI,EAAE,YAAY,MAAM,KAAK;AAAA,MAC7D,IAAI,IAAI;AAAA,IACV;AACA,UAAM,UAAU,MAAM;AAAA,MACpB;AAAA,MACA;AAAA,MACA,EAAE,SAAS,CAAC,GAAG,OAAO,SAAS,KAAK,EAAE;AAAA,MACtC,EAAE,SAAS,MAAM,SAAS,YAAY,MAAM,WAAW;AAAA,IACzD;AACA,QAAI,CAAC,QAAQ,aAAa,OAAO,WAAY,QAAO;AAEpD,UAAM,gBAAgB,MAAM,MAAM,gBAAgB,OAAO,aAAa,EAAE,QAAQ,CAAC,UAAU,EAAE,CAAC;AAC9F,eAAW,SAAS,eAAe;AACjC,UAAI,MAAM,OAAO,MAAM,MAAM,aAAa,OAAO,SAAU;AAC3D,YAAM,WAAW,MAAM,IAAI,cAAc,CAAC,GAAG,EAAE,cAAc,GAAG,CAAC;AAAA,IACnE;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL;AAAA,IACA,KAAK,CAAC,OAAO,MAAM,KAAK,EAAE;AAAA,IAC1B,WAAW,CAAC,aAAa,aAAa,MAAM,UAAU,aAAa,QAAQ;AAAA,IAC3E,oBAAoB,CAAC,aAAa,aAAa,qBAAqB,aAAa,UAAU,mBAAmB;AAAA,IAC9G,gBAAgB,CAAC,aAAa,aAAa,qBAAqB,aAAa,UAAU,OAAO;AAAA,IAC9F;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,CAAC,OAAO,WAAW,IAAI,YAAY;AAAA,EAChD;AACF;AAiBO,SAAS,iCAA2D;AACzE,QAAM,OAAO,oBAAI,IAA+B;AAChD,QAAM,SAAkC,CAAC;AAEzC,SAAO;AAAA,IACL,MAAM,KAAK,IAAI;AACb,YAAM,SAAS,KAAK,IAAI,EAAE;AAC1B,aAAO,SAAS,gBAAgB,MAAM,IAAI;AAAA,IAC5C;AAAA,IACA,MAAM,UAAU,aAAa,UAAU;AACrC,iBAAW,UAAU,KAAK,OAAO,GAAG;AAClC,YACE,OAAO,gBAAgB,eACvB,OAAO,aAAa,aACnB,OAAO,WAAW,WAAW,OAAO,WAAW,YAChD;AACA,iBAAO,gBAAgB,MAAM;AAAA,QAC/B;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,IACA,MAAM,gBAAgB,aAAa,MAAM;AACvC,YAAM,MAA2B,CAAC;AAClC,iBAAW,UAAU,KAAK,OAAO,GAAG;AAClC,YAAI,OAAO,gBAAgB,YAAa;AACxC,YAAI,MAAM,UAAU,CAAC,KAAK,OAAO,SAAS,OAAO,MAAM,EAAG;AAC1D,YAAI,KAAK,gBAAgB,MAAM,CAAC;AAAA,MAClC;AACA,aAAO;AAAA,IACT;AAAA,IACA,MAAM,OAAO,QAAQ;AACnB,UAAI,KAAK,IAAI,OAAO,EAAE,EAAG,OAAM,IAAI,MAAM,gBAAgB,OAAO,EAAE,iBAAiB;AACnF,WAAK,IAAI,OAAO,IAAI,gBAAgB,MAAM,CAAC;AAC3C,aAAO,gBAAgB,MAAM;AAAA,IAC/B;AAAA,IACA,MAAM,OAAO,IAAI,OAAO,OAAO;AAC7B,YAAM,UAAU,KAAK,IAAI,EAAE;AAC3B,UAAI,CAAC,QAAS,QAAO;AACrB,UAAI,MAAM,WAAW,UAAa,QAAQ,WAAW,MAAM,OAAQ,QAAO;AAC1E,UAAI,MAAM,YAAY,UAAa,QAAQ,YAAY,MAAM,QAAS,QAAO;AAC7E,YAAM,OAA0B,EAAE,GAAG,QAAQ;AAC7C,UAAI,MAAM,WAAW,OAAW,MAAK,SAAS,MAAM;AACpD,UAAI,MAAM,YAAY,OAAW,MAAK,UAAU,MAAM;AACtD,UAAI,MAAM,aAAa,OAAW,MAAK,WAAW,MAAM;AACxD,UAAI,MAAM,aAAa,OAAW,MAAK,WAAW,MAAM;AACxD,UAAI,MAAM,eAAe,OAAW,MAAK,aAAa,MAAM;AAC5D,UAAI,MAAM,WAAW,OAAW,MAAK,SAAS,MAAM;AACpD,UAAI,MAAM,eAAe,OAAW,MAAK,aAAa,MAAM;AAC5D,UAAI,MAAM,YAAY,OAAW,MAAK,UAAU,MAAM;AACtD,UAAI,MAAM,cAAc,OAAW,MAAK,YAAY,MAAM;AAC1D,WAAK,IAAI,IAAI,gBAAgB,IAAI,CAAC;AAClC,aAAO,gBAAgB,IAAI;AAAA,IAC7B;AAAA,IACA,MAAM,YAAY,OAAO;AACvB,aAAO,KAAK,gBAAgB,KAAK,CAAC;AAAA,IACpC;AAAA,IACA,SAAS;AACP,aAAO,OAAO,IAAI,CAAC,UAAU,gBAAgB,KAAK,CAAC;AAAA,IACrD;AAAA,IACA,IAAI,QAAQ;AACV,WAAK,IAAI,OAAO,IAAI,gBAAgB,MAAM,CAAC;AAAA,IAC7C;AAAA,EACF;AACF;;;ACtdO,SAAS,gBAAgB,MAAiC,MAAoB,KAAK,KAA4B;AACpH,SAAO,EAAE,GAAG,MAAM,eAAe,CAAC,GAAG,YAAY,IAAI,EAAE;AACzD;AAoBA,eAAsB,8BACpB,OACA,OAC8B;AAC9B,QAAM,OAAO,MAAM,MAAM,gBAAgB,MAAM,WAAW;AAC1D,QAAM,UAA+B,CAAC;AACtC,aAAW,UAAU,MAAM;AACzB,UAAM,gBAAgB,OAAO,WAAW,UAAU,MAAM;AACxD,UAAM,iBAAiB,OAAO,QAAQ,KAAK,CAAC,UAAU,MAAM,WAAW,UAAU,MAAM,KAAK;AAC5F,QAAI,CAAC,iBAAiB,CAAC,eAAgB;AACvC,UAAM,WAAW,CAAC,gBAA8D;AAAA,MAC9E,GAAG;AAAA,MACH,eAAe,CAAC,GAAG,MAAM,aAAa;AAAA,MACtC,GAAI,MAAM,YAAY,SAAY,CAAC,IAAI,EAAE,SAAS,MAAM,QAAQ;AAAA,IAClE;AACA,UAAM,OAAO,MAAM,MAAM;AAAA,MACvB,OAAO;AAAA,MACP,EAAE,QAAQ,OAAO,QAAQ,SAAS,OAAO,QAAQ;AAAA,MACjD;AAAA,QACE,GAAI,gBAAgB,EAAE,YAAY,SAAS,OAAO,UAAU,EAAE,IAAI,CAAC;AAAA,QACnE,SAAS,OAAO,QAAQ;AAAA,UAAI,CAAC,UAC3B,MAAM,WAAW,UAAU,MAAM,QAAQ,EAAE,GAAG,OAAO,YAAY,SAAS,MAAM,UAAU,EAAE,IAAI;AAAA,QAClG;AAAA,MACF;AAAA,IACF;AACA,QAAI,CAAC,MAAM;AACT,YAAM,QAAQ,KAAK,sDAAsD,OAAO,EAAE,WAAW;AAC7F;AAAA,IACF;AACA,UAAM,MAAM,YAAY;AAAA,MACtB,eAAe,OAAO;AAAA,MACtB,aAAa,OAAO;AAAA,MACpB,MAAM;AAAA,MACN,SAAS,sCAAsC,MAAM,KAAK;AAAA,MAC1D,UAAU,EAAE,OAAO,MAAM,OAAO,eAAe,CAAC,GAAG,MAAM,aAAa,GAAG,SAAS,MAAM,WAAW,KAAK;AAAA,MACxG,IAAI,KAAK,IAAI;AAAA,IACf,CAAC;AACD,YAAQ,KAAK,IAAI;AAAA,EACnB;AACA,SAAO;AACT;AAWO,SAAS,uBACd,SACA,aACgB;AAChB,QAAM,QAAwB,CAAC;AAC/B,aAAW,UAAU,SAAS;AAC5B,UAAM,WAAW,YAAY,MAAM;AACnC,QAAI,CAAC,YAAY,SAAS,WAAW,EAAG;AACxC,UAAM,KAAK,EAAE,QAAQ,OAAO,IAAI,SAAS,CAAC;AAAA,EAC5C;AACA,SAAO;AACT;;;ACrFO,IAAM,yBAAyB;AAI/B,IAAM,0BAA0B;AAgCvC,eAAe,OACb,KACA,MACY;AACZ,MAAI,UAAU,MAAM,IAAI;AACxB,MAAI,CAAC,QAAQ,aAAa,QAAQ,SAAU,WAAU,MAAM,IAAI;AAChE,MAAI,CAAC,QAAQ,UAAW,OAAM,IAAI,eAAe,MAAM,QAAQ,OAAO,QAAQ,WAAW,MAAM,GAAG;AAClG,SAAO,QAAQ;AACjB;AAEA,SAAS,gBAAgB,MAAuC;AAC9D,QAAM,WAAW,OAAO,KAAK,aAAa,WAAW,KAAK,SAAS,KAAK,IAAI;AAC5E,MAAI,CAAC,SAAU,OAAM,IAAI,eAAe,qBAAqB,8EAAyE;AACtI,SAAO;AACT;AAEA,SAAS,aAAa,MAA+B,OAA0B;AAC7E,QAAM,MAAM,KAAK,KAAK;AACtB,MAAI,CAAC,MAAM,QAAQ,GAAG,KAAK,IAAI,WAAW,GAAG;AAC3C,UAAM,IAAI,eAAe,mBAAmB,GAAG,KAAK,6BAA6B;AAAA,EACnF;AACA,MAAI,IAAI,SAAS,wBAAwB;AACvC,UAAM,IAAI,eAAe,mBAAmB,GAAG,KAAK,oBAAoB,sBAAsB,gDAA2C;AAAA,EAC3I;AACA,SAAO;AACT;AASA,eAAe,aACb,SACA,QACA,UACA,KAC4B;AAC5B,QAAM,OAAO,MAAM,QAAQ,UAAU,IAAI,aAAa,QAAQ;AAC9D,MAAI,KAAM,QAAO;AACjB,QAAM,iBAAiB,MAAM,QAAQ,eAAe,IAAI,aAAa,QAAQ;AAC7E,MAAI,gBAAgB;AAClB,UAAM,IAAI;AAAA,MACR;AAAA,MACA,oBAAoB,QAAQ,MAAM,eAAe,OAAO;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AACA,QAAM,qBAAqB,MAAM,QAAQ,mBAAmB,IAAI,aAAa,QAAQ;AACrF,MAAI,oBAAoB;AACtB,WAAO,OAAO,MAAM,QAAQ,OAAO,mBAAmB,EAAE,GAAG,eAAe;AAAA,EAC5E;AACA,SAAO,QAAQ,OAAO;AAAA,IACpB,aAAa,IAAI;AAAA,IACjB,UAAU,IAAI;AAAA,IACd;AAAA,IACA,SAAS,MAAM,QAAQ,YAAY,IAAI,aAAa,QAAQ;AAAA,IAC5D,YAAY,gBAAgB,OAAO,WAAW,GAAG,GAAG,OAAO,GAAG;AAAA,EAChE,CAAC;AACH;AAIO,SAAS,sBAAsB,QAAoD;AACxF,QAAM,UAAU,yBAAyB;AAAA,IACvC,OAAO,OAAO;AAAA,IACd,GAAI,OAAO,MAAM,EAAE,KAAK,OAAO,IAAI,IAAI,CAAC;AAAA,IACxC,GAAI,OAAO,aAAa,EAAE,YAAY,OAAO,WAAW,IAAI,CAAC;AAAA,EAC/D,CAAC;AAED,QAAM,iBAAiB,cAAc;AAAA,IACnC,MAAM;AAAA,IACN,aACE;AAAA,IACF,YAAY;AAAA,MACV,MAAM;AAAA,MACN,YAAY;AAAA,QACV,UAAU,EAAE,MAAM,UAAU,aAAa,wCAAwC;AAAA,QACjF,SAAS;AAAA,UACP,MAAM;AAAA,UACN,UAAU;AAAA,UACV,UAAU;AAAA,UACV,OAAO;AAAA,YACL,MAAM;AAAA,YACN,YAAY;AAAA,cACV,IAAI,EAAE,MAAM,UAAU,aAAa,6CAAwC;AAAA,cAC3E,WAAW,EAAE,MAAM,UAAU,aAAa,qDAAqD;AAAA,cAC/F,SAAS;AAAA,gBACP,MAAM;AAAA,gBACN,YAAY;AAAA,kBACV,MAAM,EAAE,MAAM,SAAS;AAAA,kBACvB,OAAO,EAAE,MAAM,UAAU,aAAa,oDAAiD;AAAA,kBACvF,OAAO,EAAE,MAAM,UAAU,aAAa,6BAA6B;AAAA,gBACrE;AAAA,cACF;AAAA,cACA,QAAQ,EAAE,MAAM,UAAU,aAAa,+CAA+C;AAAA,cACtF,OAAO,EAAE,MAAM,UAAU,aAAa,qCAAqC;AAAA,cAC3E,YAAY,EAAE,MAAM,UAAU,SAAS,GAAG,SAAS,EAAE;AAAA,YACvD;AAAA,YACA,UAAU,CAAC,MAAM,aAAa,UAAU,OAAO;AAAA,UACjD;AAAA,QACF;AAAA,MACF;AAAA,MACA,UAAU,CAAC,YAAY,SAAS;AAAA,IAClC;AAAA,IACA,MAAM,QAAQ,MAA+B,KAAqB;AAChE,YAAM,WAAW,gBAAgB,IAAI;AACrC,YAAM,MAAM,aAAa,MAAM,SAAS;AACxC,YAAM,UAA2B,CAAC;AAClC,eAAS,QAAQ,GAAG,QAAQ,IAAI,QAAQ,SAAS,GAAG;AAClD,cAAM,SAAS,mBAAmB,IAAI,KAAK,GAAG,WAAW,KAAK,GAAG;AACjE,YAAI,CAAC,OAAO,GAAI,OAAM,IAAI,eAAe,oBAAoB,GAAG,OAAO,KAAK,KAAK,OAAO,KAAK,EAAE;AAC/F,gBAAQ,KAAK,OAAO,KAAK;AAAA,MAC3B;AAEA,eAAS,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS,GAAG;AACtD,cAAM,QAAQ,QAAQ,KAAK;AAC3B,YAAI,CAAE,MAAM,OAAO,iBAAiB,MAAM,WAAW,GAAG,GAAI;AAC1D,gBAAM,IAAI;AAAA,YACR;AAAA,YACA,WAAW,KAAK,iBAAiB,MAAM,SAAS;AAAA,UAClD;AAAA,QACF;AAAA,MACF;AACA,YAAM,QAAQ,MAAM,aAAa,SAAS,QAAQ,UAAU,GAAG;AAC/D,YAAM,SAAS,MAAM,OAAO,MAAM,QAAQ,eAAe,MAAM,IAAI,OAAO,GAAG,mBAAmB;AAChG,aAAO,EAAE,eAAe,OAAO,IAAI,SAAS,OAAO,SAAS,eAAe,OAAO,SAAS,OAAO;AAAA,IACpG;AAAA,EACF,CAAC;AAED,QAAM,gBAAgB,cAAc;AAAA,IAClC,MAAM;AAAA,IACN,aACE;AAAA,IACF,YAAY;AAAA,MACV,MAAM;AAAA,MACN,YAAY;AAAA,QACV,UAAU,EAAE,MAAM,UAAU,aAAa,wCAAwC;AAAA,QACjF,YAAY;AAAA,UACV,MAAM;AAAA,UACN,UAAU;AAAA,UACV,UAAU;AAAA,UACV,OAAO;AAAA,YACL,MAAM;AAAA,YACN,YAAY;AAAA,cACV,IAAI,EAAE,MAAM,UAAU,aAAa,6CAAwC;AAAA,cAC3E,UAAU,EAAE,MAAM,UAAU,MAAM,CAAC,YAAY,YAAY,UAAU,EAAE;AAAA,cACvE,MAAM,EAAE,MAAM,UAAU,aAAa,8CAA8C;AAAA,cACnF,SAAS,EAAE,MAAM,SAAS;AAAA,cAC1B,SAAS,EAAE,MAAM,SAAS,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,cACpD,UAAU,EAAE,MAAM,UAAU;AAAA,cAC5B,gBAAgB,EAAE,MAAM,SAAS;AAAA,YACnC;AAAA,YACA,UAAU,CAAC,MAAM,YAAY,QAAQ,SAAS;AAAA,UAChD;AAAA,QACF;AAAA,MACF;AAAA,MACA,UAAU,CAAC,YAAY,YAAY;AAAA,IACrC;AAAA,IACA,MAAM,QAAQ,MAA+B,KAAqB;AAChE,YAAM,WAAW,gBAAgB,IAAI;AACrC,YAAM,MAAM,aAAa,MAAM,YAAY;AAC3C,YAAM,UAA4B,CAAC;AACnC,eAAS,QAAQ,GAAG,QAAQ,IAAI,QAAQ,SAAS,GAAG;AAClD,cAAM,SAAS,oBAAoB,IAAI,KAAK,GAAG,cAAc,KAAK,GAAG;AACrE,YAAI,CAAC,OAAO,GAAI,OAAM,IAAI,eAAe,qBAAqB,GAAG,OAAO,KAAK,KAAK,OAAO,KAAK,EAAE;AAChG,YAAI,CAAC,OAAO,eAAe,SAAS,OAAO,MAAM,IAAI,GAAG;AACtD,gBAAM,IAAI;AAAA,YACR;AAAA,YACA,cAAc,KAAK,2BAA2B,OAAO,eAAe,KAAK,IAAI,CAAC;AAAA,UAChF;AAAA,QACF;AAGA,YAAI,OAAO,MAAM,YAAY,OAAO,MAAM,eAAe,OAAW,QAAO,MAAM,aAAa;AAC9F,gBAAQ,KAAK,OAAO,KAAK;AAAA,MAC3B;AACA,YAAM,QAAQ,MAAM,aAAa,SAAS,QAAQ,UAAU,GAAG;AAC/D,YAAM,SAAS,MAAM,OAAO,MAAM,QAAQ,iBAAiB,MAAM,IAAI,OAAO,GAAG,oBAAoB;AACnG,aAAO;AAAA,QACL,eAAe,OAAO;AAAA,QACtB,SAAS,OAAO;AAAA,QAChB,QAAQ,OAAO;AAAA,QACf,oBAAoB,6BAA6B,OAAO,UAAU,EAAE;AAAA,MACtE;AAAA,IACF;AAAA,EACF,CAAC;AAED,QAAM,oBAAoB,cAAc;AAAA,IACtC,MAAM;AAAA,IACN,aACE;AAAA,IACF,YAAY;AAAA,MACV,MAAM;AAAA,MACN,YAAY;AAAA,QACV,UAAU,EAAE,MAAM,UAAU,aAAa,wCAAwC;AAAA,QACjF,UAAU;AAAA,UACR,MAAM;AAAA,UACN,YAAY;AAAA,YACV,MAAM,EAAE,MAAM,UAAU,aAAa,6CAA6C;AAAA,YAClF,OAAO,EAAE,MAAM,SAAS;AAAA,YACxB,MAAM,EAAE,MAAM,UAAU,aAAa,mDAAmD;AAAA,YACxF,SAAS,EAAE,MAAM,UAAU,aAAa,0CAA0C;AAAA,YAClF,WAAW,EAAE,MAAM,SAAS;AAAA,YAC5B,UAAU;AAAA,cACR,MAAM;AAAA,cACN,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,GAAG,SAAS,EAAE,MAAM,SAAS,EAAE;AAAA,cACpE,aAAa;AAAA,YACf;AAAA,YACA,QAAQ,EAAE,MAAM,UAAU,aAAa,kDAAkD;AAAA,UAC3F;AAAA,UACA,UAAU,CAAC,QAAQ,OAAO;AAAA,QAC5B;AAAA,QACA,QAAQ;AAAA,UACN,MAAM;AAAA,UACN,OAAO;AAAA,YACL,MAAM;AAAA,YACN,YAAY;AAAA,cACV,IAAI,EAAE,MAAM,SAAS;AAAA,cACrB,MAAM,EAAE,MAAM,SAAS;AAAA,cACvB,QAAQ,EAAE,MAAM,UAAU;AAAA,cAC1B,QAAQ,EAAE,MAAM,SAAS;AAAA,YAC3B;AAAA,YACA,UAAU,CAAC,MAAM,QAAQ,QAAQ;AAAA,UACnC;AAAA,UACA,aAAa;AAAA,QACf;AAAA,MACF;AAAA,MACA,UAAU,CAAC,YAAY,UAAU;AAAA,IACnC;AAAA,IACA,MAAM,QAAQ,MAA+B,KAAqB;AAChE,YAAM,WAAW,gBAAgB,IAAI;AACrC,YAAM,iBAAiB,mBAAmB,KAAK,QAAQ;AACvD,UAAI,CAAC,eAAe,GAAI,OAAM,IAAI,eAAe,oBAAoB,GAAG,eAAe,KAAK,KAAK,eAAe,KAAK,EAAE;AACvH,YAAM,WAAW,eAAe;AAChC,UAAI,CAAC,OAAO,cAAc,SAAS,SAAS,IAAI,GAAG;AACjD,cAAM,IAAI,eAAe,oBAAoB,kCAAkC,OAAO,cAAc,KAAK,IAAI,CAAC,EAAE;AAAA,MAClH;AACA,YAAM,cAA8B,CAAC;AACrC,UAAI,KAAK,WAAW,QAAW;AAC7B,YAAI,CAAC,MAAM,QAAQ,KAAK,MAAM,EAAG,OAAM,IAAI,eAAe,kBAAkB,uCAAuC;AACnH,iBAAS,QAAQ,GAAG,QAAQ,KAAK,OAAO,QAAQ,SAAS,GAAG;AAC1D,gBAAM,SAAS,qBAAqB,KAAK,OAAO,KAAK,GAAG,UAAU,KAAK,GAAG;AAC1E,cAAI,CAAC,OAAO,GAAI,OAAM,IAAI,eAAe,kBAAkB,GAAG,OAAO,KAAK,KAAK,OAAO,KAAK,EAAE;AAC7F,sBAAY,KAAK,EAAE,GAAG,OAAO,OAAO,QAAQ,QAAQ,CAAC;AAAA,QACvD;AAAA,MACF;AAEA,YAAM,QAAQ,MAAM,aAAa,SAAS,QAAQ,UAAU,GAAG;AAC/D,YAAM,WAAW,6BAA6B,MAAM,UAAU;AAC9D,UAAI,SAAS,SAAS,GAAG;AACvB,cAAM,IAAI;AAAA,UACR;AAAA,UACA,kBAAkB,SAAS,MAAM,sCAAsC,SAAS,IAAI,CAAC,UAAU,MAAM,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,UACnH;AAAA,QACF;AAAA,MACF;AAMA,YAAM,SAAyB,CAAC,GAAG,WAAW;AAC9C,UAAI,OAAO,iBAAiB;AAC1B,cAAM,UAAU,OAAO,gBAAgB,QAAQ;AAC/C,cAAM,UAAU,IAAI,IAAI,MAAM,SAAS,IAAI,CAAC,UAAU,MAAM,MAAM,CAAC;AACnE,cAAM,UAAU,QAAQ,OAAO,CAAC,WAAW,CAAC,QAAQ,IAAI,MAAM,CAAC;AAC/D,cAAM,WAAyB;AAAA,UAC7B,IAAI;AAAA,UACJ,MAAM;AAAA,UACN,QAAQ,QAAQ,WAAW;AAAA,UAC3B,QACE,QAAQ,WAAW,IACf,GAAG,QAAQ,MAAM,IAAI,QAAQ,MAAM,gCACnC,yBAAyB,QAAQ,KAAK,IAAI,CAAC;AAAA,UACjD,QAAQ;AAAA,QACV;AACA,eAAO,QAAQ,QAAQ;AACvB,YAAI,QAAQ,SAAS,GAAG;AACtB,gBAAM,OAAO,MAAM,QAAQ,aAAa,MAAM,IAAI,MAAM,GAAG,iBAAiB;AAC5E,gBAAM,IAAI;AAAA,YACR;AAAA,YACA,kDAAkD,QAAQ,KAAK,IAAI,CAAC;AAAA,UACtE;AAAA,QACF;AAAA,MACF;AAEA,YAAM,aAAa,gBAAgB,OAAO,WAAW,GAAG,GAAG,OAAO,GAAG;AACrE,YAAM,SAAS,MAAM;AAAA,QACnB,MAAM,QAAQ,OAAO,MAAM,IAAI,EAAE,UAAU,QAAQ,WAAW,CAAC;AAAA,QAC/D;AAAA,MACF;AACA,YAAM,OAAO,UAAU,QAAQ,GAAG;AAClC,aAAO;AAAA,QACL,eAAe,OAAO;AAAA,QACtB,SAAS,OAAO;AAAA,QAChB,QAAQ,OAAO;AAAA,QACf,QAAQ,OAAO,OAAO,IAAI,CAAC,WAAW,EAAE,MAAM,MAAM,MAAM,QAAQ,MAAM,QAAQ,QAAQ,MAAM,OAAO,EAAE;AAAA,MACzG;AAAA,IACF;AAAA,EACF,CAAC;AAED,SAAO,CAAC,gBAAgB,eAAe,iBAAiB;AAC1D;;;ACzVO,SAAS,+BAA+B,MAAuD;AACpG,QAAM,KAAK,OAAO,KAAK,OAAO,YAAY,KAAK,GAAG,KAAK,IAAI,KAAK,GAAG,KAAK,IAAI;AAC5E,MAAI,CAAC,GAAI,QAAO,EAAE,IAAI,OAAO,OAAO,0BAA0B;AAC9D,QAAM,UAAU,KAAK;AACrB,MAAI,YAAY,aAAa,YAAY,mBAAmB;AAC1D,WAAO,EAAE,IAAI,OAAO,OAAO,uDAAuD;AAAA,EACpF;AACA,QAAM,OAAO,KAAK,SAAS,SAAY,SAAY,OAAO,KAAK,SAAS,WAAW,KAAK,KAAK,KAAK,IAAI;AACtG,MAAI,SAAS,KAAM,QAAO,EAAE,IAAI,OAAO,OAAO,kCAAkC;AAChF,MAAI,YAAY,mBAAmB;AACjC,QAAI,CAAC,KAAM,QAAO,EAAE,IAAI,OAAO,OAAO,uFAAkF;AACxH,WAAO,EAAE,IAAI,MAAM,IAAI,SAAS,KAAK;AAAA,EACvC;AACA,SAAO,OAAO,EAAE,IAAI,MAAM,IAAI,SAAS,KAAK,IAAI,EAAE,IAAI,MAAM,IAAI,QAAQ;AAC1E;AAsDO,SAAS,wBAAwB,SAAsD;AAC5F,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,UAA8B,yBAAyB;AAAA,IAC3D,OAAO,QAAQ;AAAA,IACf,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;AAAA,EAC5C,CAAC;AAED,iBAAe,KAAK,SAAqC;AACvD,UAAM,OAAO,MAAM,QAAQ,UAAU,EAAE,SAAS,QAAQ,OAAO,CAAC;AAChE,QAAI,CAAC,KAAK,GAAI,QAAO,KAAK;AAC1B,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,UAAM,cAAc,IAAI,aAAa,IAAI,QAAQ;AACjD,UAAM,WAAW,cACb,YAAY,MAAM,GAAG,EAAE,IAAI,CAAC,UAAU,MAAM,KAAK,CAAC,EAAE,OAAO,OAAO,IAClE;AACJ,UAAM,eAAe,MAAM,QAAQ,MAAM;AAAA,MACvC,KAAK;AAAA,MACL,WAAW,EAAE,QAAQ,SAA0C,IAAI;AAAA,IACrE;AACA,WAAO,SAAS,KAAK,EAAE,aAAa,CAAC;AAAA,EACvC;AAEA,iBAAe,OAAO,SAAkB,IAA+B;AACrE,UAAM,OAAO,MAAM,QAAQ,UAAU,EAAE,SAAS,QAAQ,SAAS,CAAC;AAClE,QAAI,CAAC,KAAK,GAAI,QAAO,KAAK;AAC1B,UAAM,SAAS,MAAM,QAAQ,MAAM,KAAK,EAAE;AAC1C,QAAI,CAAC,UAAU,OAAO,gBAAgB,KAAK,aAAa;AACtD,aAAO,SAAS,KAAK,EAAE,OAAO,yBAAyB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC3E;AACA,WAAO,SAAS,KAAK,EAAE,aAAa,OAAO,CAAC;AAAA,EAC9C;AAEA,iBAAe,QAAQ,SAAqC;AAC1D,UAAM,OAAQ,MAAM,QAAQ,KAAK,EAAE,MAAM,MAAM,IAAI;AACnD,QAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,GAAG;AAC5D,aAAO,SAAS,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACtE;AACA,UAAM,aAAa,+BAA+B,IAAI;AACtD,QAAI,CAAC,WAAW,GAAI,QAAO,SAAS,KAAK,EAAE,OAAO,WAAW,MAAM,GAAG,EAAE,QAAQ,IAAI,CAAC;AAErF,UAAM,OAAO,MAAM,QAAQ,UAAU,EAAE,SAAS,QAAQ,WAAW,KAAK,CAAC;AACzE,QAAI,CAAC,KAAK,GAAI,QAAO,KAAK;AAE1B,UAAM,WAAW,MAAM,QAAQ,MAAM,KAAK,WAAW,EAAE;AACvD,QAAI,CAAC,YAAY,SAAS,gBAAgB,KAAK,aAAa;AAC1D,aAAO,SAAS,KAAK,EAAE,OAAO,yBAAyB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC3E;AAEA,UAAM,UAAU,MAAM,QAAQ,aAAa,WAAW,IAAI;AAAA,MACxD,SAAS,WAAW;AAAA,MACpB,YAAY,KAAK;AAAA,MACjB,GAAI,WAAW,SAAS,SAAY,CAAC,IAAI,EAAE,MAAM,WAAW,KAAK;AAAA,IACnE,CAAC;AACD,QAAI,CAAC,QAAQ,WAAW;AAGtB,aAAO,SAAS,KAAK,EAAE,MAAM,oBAAoB,OAAO,QAAQ,MAAM,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1F;AACA,UAAM,SAAS,QAAQ;AAIvB,QAAI;AACF,YAAM,QAAQ,oBAAoB,2BAA2B,MAAM,GAAG,MAAM;AAAA,IAC9E,SAAS,OAAO;AACd,aAAO,MAAM,4CAA4C,KAAK;AAAA,IAChE;AACA,QAAI;AACF,YAAM,QAAQ,YAAY;AAAA,QACxB;AAAA,QACA,SAAS,WAAW;AAAA,QACpB,GAAI,WAAW,SAAS,SAAY,CAAC,IAAI,EAAE,MAAM,WAAW,KAAK;AAAA,QACjE,YAAY,KAAK;AAAA,MACnB,CAAC;AAAA,IACH,SAAS,OAAO;AACd,aAAO,MAAM,oCAAoC,KAAK;AAAA,IACxD;AACA,QAAI,WAAW,YAAY,WAAW;AACpC,UAAI;AACF,cAAM,QAAQ,WAAW,MAAM;AAAA,MACjC,SAAS,OAAO;AACd,eAAO,MAAM,mCAAmC,KAAK;AAAA,MACvD;AAAA,IACF;AAEA,WAAO,SAAS,KAAK,EAAE,IAAI,MAAM,aAAa,OAAO,CAAC;AAAA,EACxD;AAEA,SAAO,EAAE,MAAM,QAAQ,QAAQ;AACjC;","names":["record","entry"]}
1
+ {"version":3,"sources":["../../src/work-product/service.ts","../../src/work-product/provenance.ts","../../src/work-product/quote.ts","../../src/work-product/tools.ts","../../src/work-product/route.ts"],"sourcesContent":["/**\n * The guarded work-product status machine — the `/missions` service PATTERN\n * (load → validate against a transition table → CAS-write guarded on what was\n * read → audit event) over {@link WorkProductStorePort}. Deliberately NOT a\n * reuse of the mission service: missions' cursor/plan/budget machinery does\n * not apply here, and the six-status review machine is a different contract.\n *\n * Concurrency contract: a scope's draft is driven by a single serialized turn\n * owner, so real contention is rare. The service is the typed guard layer,\n * not a serializer — every mutation re-reads the record and CAS-writes\n * guarded on the `{status, version}` it read. A guard miss surfaces as\n * `{ succeeded: false, conflict: true }` (retryable: re-read and re-apply),\n * never a silent clobber.\n */\n\nimport {\n unresolvedBlockingExceptions,\n type EvidenceEntry,\n type ExceptionEntry,\n type QualityCheck,\n type WorkProductArtifact,\n type WorkProductAuditEvent,\n type WorkProductPatch,\n type WorkProductProvenance,\n type WorkProductRecord,\n type WorkProductStatus,\n type WorkProductStorePort,\n type WorkProductVersionEntry,\n} from './types'\n\n/** Discriminated outcome for guarded operations — `conflict` distinguishes a\n * lost guarded race (retryable) from a logic rejection (illegal edge,\n * missing row — deterministic, never retried). */\nexport type WorkProductOutcome<T> =\n | { succeeded: true; value: T }\n | { succeeded: false; error: string; conflict: boolean }\n\n// Legal status transitions. A target absent from a source's set is rejected\n// by the guarded helpers. `superseded` is terminal.\nconst WORK_PRODUCT_TRANSITIONS: Record<WorkProductStatus, ReadonlySet<WorkProductStatus>> = {\n draft: new Set<WorkProductStatus>(['blocked', 'ready']),\n blocked: new Set<WorkProductStatus>(['draft']),\n ready: new Set<WorkProductStatus>(['changes_requested', 'approved', 'superseded']),\n changes_requested: new Set<WorkProductStatus>(['draft', 'superseded']),\n approved: new Set<WorkProductStatus>(['superseded']),\n superseded: new Set<WorkProductStatus>(),\n}\n\n/** Whether a legal edge exists from `from` to `to` in the review machine. */\nexport function canTransitionWorkProduct(from: WorkProductStatus, to: WorkProductStatus): boolean {\n return WORK_PRODUCT_TRANSITIONS[from].has(to)\n}\n\n/** Statuses a work product can never leave. */\nexport function isWorkProductTerminal(status: WorkProductStatus): boolean {\n return WORK_PRODUCT_TRANSITIONS[status].size === 0\n}\n\n/** Define the input required to create a new draft work product row */\nexport interface CreateWorkProductInput {\n /** Explicit row id — omit to use the service's generator. The MODEL never\n * supplies ids; this is for deterministic server-side creation. */\n id?: string\n workspaceId: string\n threadId: string | null\n scopeKey: string\n /** Version this draft will become; default 1. The tool layer passes\n * `last reviewed version + 1` when a scope is re-engaged after approval. */\n version?: number\n provenance: WorkProductProvenance\n /** Opaque product-column values handed VERBATIM to the store's insert. */\n extras?: Record<string, unknown>\n}\n\n/** Payload for the draft→ready submit transition */\nexport interface SubmitWorkProductInput {\n artifact: WorkProductArtifact\n checks: QualityCheck[]\n provenance: WorkProductProvenance\n /** Frozen snapshot ref of this version's body for the history entry;\n * defaults to `artifact.path`. */\n artifactPath?: string\n}\n\n/** Reviewer verdict payload for the ready→approved / ready→changes_requested transition */\nexport interface WorkProductVerdictInput {\n verdict: 'approve' | 'request_changes'\n reviewedBy: string\n note?: string\n}\n\n/** Guarded mutation surface over the work-product store */\nexport interface WorkProductService {\n create(input: CreateWorkProductInput): Promise<WorkProductRecord>\n get(id: string): Promise<WorkProductRecord | null>\n /** The scope's open accumulator row (`draft`/`blocked`), or null. */\n openDraft(workspaceId: string, scopeKey: string): Promise<WorkProductRecord | null>\n /** The scope's `changes_requested` row awaiting its correction turn, or null. */\n awaitingCorrection(workspaceId: string, scopeKey: string): Promise<WorkProductRecord | null>\n /** The scope's `ready` row awaiting review, or null. */\n awaitingReview(workspaceId: string, scopeKey: string): Promise<WorkProductRecord | null>\n /** `max(version over the scope's approved/superseded rows) + 1` — the\n * version a fresh draft for the scope should carry. */\n nextVersion(workspaceId: string, scopeKey: string): Promise<number>\n /** Re-open a `changes_requested` row as the next draft: version bumps +1\n * and the correction turn accumulates into the same scope row. */\n reopen(id: string): Promise<WorkProductOutcome<WorkProductRecord>>\n /** Merge evidence entries by id (same id replaces — the agent can correct\n * itself mid-turn). Legal only while `draft`/`blocked`. */\n upsertEvidence(id: string, entries: readonly EvidenceEntry[]): Promise<WorkProductOutcome<WorkProductRecord>>\n /** Merge exception entries by id, then reconcile the blocked flag: any\n * unresolved blocking entry parks `draft`→`blocked`; resolving the last\n * one releases `blocked`→`draft`. */\n upsertExceptions(id: string, entries: readonly ExceptionEntry[]): Promise<WorkProductOutcome<WorkProductRecord>>\n /** Persist a checks array without transitioning — how a failed platform\n * gate (e.g. evidence_coverage) stays visible on the still-draft row. */\n recordChecks(id: string, checks: readonly QualityCheck[]): Promise<WorkProductOutcome<WorkProductRecord>>\n /** The terminal agent call: CAS `draft`→`ready` with artifact + checks +\n * provenance, appending the version-history entry. Refuses while an\n * unresolved blocking exception exists. */\n submit(id: string, input: SubmitWorkProductInput): Promise<WorkProductOutcome<WorkProductRecord>>\n /** Reviewer verdict: CAS `ready`→`approved`/`changes_requested` + history\n * entry. Approval also supersedes the scope's prior approved versions so\n * exactly one approved version is current per scope. */\n applyVerdict(id: string, input: WorkProductVerdictInput): Promise<WorkProductOutcome<WorkProductRecord>>\n /** Explicit replacement: CAS to `superseded` (legal from ready /\n * changes_requested / approved). */\n supersede(id: string): Promise<WorkProductOutcome<WorkProductRecord>>\n}\n\n/** Configuration options for creating a work product service */\nexport interface WorkProductServiceOptions {\n store: WorkProductStorePort\n /** Injectable clock (epoch ms). Default `Date.now`. */\n now?: () => number\n /** Row-id generator. Default `crypto.randomUUID()`. */\n generateId?: () => string\n}\n\nfunction rejected<T>(error: string): WorkProductOutcome<T> {\n return { succeeded: false, error, conflict: false }\n}\n\nfunction lostRace<T>(id: string): WorkProductOutcome<T> {\n return { succeeded: false, error: `Work product ${id} changed concurrently`, conflict: true }\n}\n\n/** Merge-by-id upsert: existing order preserved, replaced in place, new\n * entries appended in call order. */\nfunction mergeById<T extends { id: string }>(existing: readonly T[], incoming: readonly T[]): T[] {\n const merged = existing.slice()\n const indexById = new Map(merged.map((entry, index) => [entry.id, index] as const))\n for (const entry of incoming) {\n const at = indexById.get(entry.id)\n if (at === undefined) {\n indexById.set(entry.id, merged.length)\n merged.push(entry)\n } else {\n merged[at] = entry\n }\n }\n return merged\n}\n\n/** Create the guarded work-product service over a store port */\nexport function createWorkProductService(options: WorkProductServiceOptions): WorkProductService {\n const { store } = options\n const now = options.now ?? (() => Date.now())\n const generateId = options.generateId ?? (() => crypto.randomUUID())\n\n async function appendEvent(\n record: WorkProductRecord,\n step: string,\n message: string,\n metadata: Record<string, unknown> = {},\n ): Promise<void> {\n await store.appendEvent({\n workProductId: record.id,\n workspaceId: record.workspaceId,\n step,\n message,\n metadata,\n at: now(),\n })\n }\n\n // Guarded status transition: load → validate the edge → CAS guarded on the\n // {status, version} read → audit event. The loser of a racing transition\n // gets a conflict instead of silently violating the machine.\n async function transition(\n id: string,\n to: WorkProductStatus,\n patch: Omit<WorkProductPatch, 'status'> = {},\n eventMeta: Record<string, unknown> = {},\n ): Promise<WorkProductOutcome<WorkProductRecord>> {\n const record = await store.load(id)\n if (!record) return rejected(`Work product ${id} not found`)\n const from = record.status\n if (isWorkProductTerminal(from)) {\n return rejected(`Work product ${id} is terminal (${from}); cannot transition to ${to}`)\n }\n if (!canTransitionWorkProduct(from, to)) {\n return rejected(`Illegal work-product transition ${from} -> ${to} for ${id}`)\n }\n const updated = await store.update(\n id,\n { status: from, version: record.version },\n { status: to, updatedAt: now(), ...patch },\n )\n if (!updated) return lostRace(id)\n await appendEvent(updated, `wp.${to}`, `Work product ${from} -> ${to}`, { from, to, ...eventMeta })\n return { succeeded: true, value: updated }\n }\n\n // Merge-style guarded write with no status change (evidence / exceptions /\n // checks accumulate on the open row).\n async function guardedMerge(\n id: string,\n legalStatuses: readonly WorkProductStatus[],\n build: (record: WorkProductRecord) => Omit<WorkProductPatch, 'status' | 'version'>,\n event: { step: string; message: (record: WorkProductRecord) => string; metadata?: (record: WorkProductRecord) => Record<string, unknown> },\n ): Promise<WorkProductOutcome<WorkProductRecord>> {\n const record = await store.load(id)\n if (!record) return rejected(`Work product ${id} not found`)\n if (!legalStatuses.includes(record.status)) {\n return rejected(`Work product ${id} is ${record.status}; expected ${legalStatuses.join('/')}`)\n }\n const updated = await store.update(\n id,\n { status: record.status, version: record.version },\n { updatedAt: now(), ...build(record) },\n )\n if (!updated) return lostRace(id)\n await appendEvent(updated, event.step, event.message(updated), event.metadata?.(updated) ?? {})\n return { succeeded: true, value: updated }\n }\n\n const create: WorkProductService['create'] = async (input) => {\n const at = now()\n const record = await store.insert(\n {\n id: input.id ?? generateId(),\n workspaceId: input.workspaceId,\n threadId: input.threadId,\n scopeKey: input.scopeKey,\n status: 'draft',\n version: input.version ?? 1,\n artifact: null,\n evidence: [],\n exceptions: [],\n checks: [],\n provenance: input.provenance,\n history: [],\n createdAt: at,\n updatedAt: at,\n },\n input.extras,\n )\n await appendEvent(record, 'wp.created', `Work product draft v${record.version} created for ${record.scopeKey}`, {\n scopeKey: record.scopeKey,\n version: record.version,\n threadId: record.threadId,\n })\n return record\n }\n\n async function findByScopeAndStatus(\n workspaceId: string,\n scopeKey: string,\n status: WorkProductStatus,\n ): Promise<WorkProductRecord | null> {\n const rows = await store.listByWorkspace(workspaceId, { status: [status] })\n return rows.find((row) => row.scopeKey === scopeKey) ?? null\n }\n\n const nextVersion: WorkProductService['nextVersion'] = async (workspaceId, scopeKey) => {\n const reviewed = await store.listByWorkspace(workspaceId, { status: ['approved', 'superseded'] })\n const versions = reviewed.filter((row) => row.scopeKey === scopeKey).map((row) => row.version)\n return versions.length === 0 ? 1 : Math.max(...versions) + 1\n }\n\n const reopen: WorkProductService['reopen'] = async (id) => {\n const record = await store.load(id)\n if (!record) return rejected(`Work product ${id} not found`)\n if (record.status !== 'changes_requested') {\n return rejected(`Work product ${id} is ${record.status}; only changes_requested reopens`)\n }\n const updated = await store.update(\n id,\n { status: 'changes_requested', version: record.version },\n { status: 'draft', version: record.version + 1, updatedAt: now() },\n )\n if (!updated) return lostRace(id)\n await appendEvent(updated, 'wp.reopened', `Correction draft v${updated.version} opened`, {\n from: record.version,\n to: updated.version,\n })\n return { succeeded: true, value: updated }\n }\n\n const upsertEvidence: WorkProductService['upsertEvidence'] = (id, entries) =>\n guardedMerge(\n id,\n ['draft', 'blocked'],\n (record) => ({ evidence: mergeById(record.evidence, entries) }),\n {\n step: 'wp.evidence',\n message: (record) => `Evidence upserted (${entries.length} entries, ${record.evidence.length} total)`,\n metadata: () => ({ upserted: entries.map((entry) => entry.id) }),\n },\n )\n\n const upsertExceptions: WorkProductService['upsertExceptions'] = async (id, entries) => {\n const merged = await guardedMerge(\n id,\n ['draft', 'blocked'],\n (record) => ({ exceptions: mergeById(record.exceptions, entries) }),\n {\n step: 'wp.exception',\n message: (record) =>\n `Exceptions upserted (${entries.length} entries, ${unresolvedBlockingExceptions(record.exceptions).length} blocking unresolved)`,\n metadata: () => ({ upserted: entries.map((entry) => entry.id) }),\n },\n )\n if (!merged.succeeded) return merged\n // Reconcile the blocked flag AFTER the merge commits: an unresolved\n // blocking entry parks the draft; resolving the last one releases it.\n const record = merged.value\n const blocking = unresolvedBlockingExceptions(record.exceptions).length\n if (record.status === 'draft' && blocking > 0) {\n return transition(id, 'blocked', {}, { blocking })\n }\n if (record.status === 'blocked' && blocking === 0) {\n return transition(id, 'draft', {}, { blocking })\n }\n return merged\n }\n\n const recordChecks: WorkProductService['recordChecks'] = (id, checks) =>\n guardedMerge(\n id,\n ['draft', 'blocked'],\n () => ({ checks: checks.slice() }),\n {\n step: 'wp.checks',\n message: () => `Checks recorded (${checks.length}, ${checks.filter((check) => !check.passed).length} failed)`,\n metadata: () => ({ failed: checks.filter((check) => !check.passed).map((check) => check.name) }),\n },\n )\n\n const submit: WorkProductService['submit'] = async (id, input) => {\n const record = await store.load(id)\n if (!record) return rejected(`Work product ${id} not found`)\n if (record.status !== 'draft') {\n return rejected(`Work product ${id} is ${record.status}; only a draft submits`)\n }\n const blocking = unresolvedBlockingExceptions(record.exceptions)\n if (blocking.length > 0) {\n return rejected(\n `Work product ${id} has ${blocking.length} unresolved blocking exception(s): ${blocking.map((entry) => entry.id).join(', ')}`,\n )\n }\n const artifactPath = input.artifactPath ?? input.artifact.path\n const entry: WorkProductVersionEntry = {\n version: record.version,\n status: 'ready',\n provenance: input.provenance,\n ...(artifactPath === undefined ? {} : { artifactPath }),\n at: now(),\n }\n return transition(\n id,\n 'ready',\n {\n artifact: input.artifact,\n checks: input.checks.slice(),\n provenance: input.provenance,\n history: [...record.history, entry],\n },\n { version: record.version, failedChecks: input.checks.filter((check) => !check.passed).length },\n )\n }\n\n const applyVerdict: WorkProductService['applyVerdict'] = async (id, input) => {\n const record = await store.load(id)\n if (!record) return rejected(`Work product ${id} not found`)\n if (record.status !== 'ready') {\n return rejected(`Work product ${id} is ${record.status}; a verdict applies only to ready`)\n }\n const to: WorkProductStatus = input.verdict === 'approve' ? 'approved' : 'changes_requested'\n const entry: WorkProductVersionEntry = {\n version: record.version,\n status: to,\n provenance: record.provenance,\n ...(record.artifact?.path === undefined ? {} : { artifactPath: record.artifact.path }),\n reviewedBy: input.reviewedBy,\n ...(input.note === undefined ? {} : { reviewNote: input.note }),\n at: now(),\n }\n const outcome = await transition(\n id,\n to,\n { history: [...record.history, entry] },\n { verdict: input.verdict, reviewedBy: input.reviewedBy },\n )\n if (!outcome.succeeded || to !== 'approved') return outcome\n // Exactly one approved version per scope: supersede prior approved rows.\n const priorApproved = await store.listByWorkspace(record.workspaceId, { status: ['approved'] })\n for (const prior of priorApproved) {\n if (prior.id === id || prior.scopeKey !== record.scopeKey) continue\n await transition(prior.id, 'superseded', {}, { supersededBy: id })\n }\n return outcome\n }\n\n return {\n create,\n get: (id) => store.load(id),\n openDraft: (workspaceId, scopeKey) => store.findDraft(workspaceId, scopeKey),\n awaitingCorrection: (workspaceId, scopeKey) => findByScopeAndStatus(workspaceId, scopeKey, 'changes_requested'),\n awaitingReview: (workspaceId, scopeKey) => findByScopeAndStatus(workspaceId, scopeKey, 'ready'),\n nextVersion,\n reopen,\n upsertEvidence,\n upsertExceptions,\n recordChecks,\n submit,\n applyVerdict,\n supersede: (id) => transition(id, 'superseded'),\n }\n}\n\n// ── in-memory store ──────────────────────────────────────────────────────────\n\n/** In-memory store surface with audit trail access and unguarded direct writes for tests */\nexport interface InMemoryWorkProductStore extends WorkProductStorePort {\n /** The full audit trail, append order. */\n events(): WorkProductAuditEvent[]\n /** Unguarded direct write — simulates a concurrent owner in tests. */\n put(record: WorkProductRecord): void\n}\n\n/**\n * In-memory {@link WorkProductStorePort} — the portable backend for tests and\n * reference assemblies. Records are deep-copied on every boundary so callers\n * can never mutate stored state around the guards.\n */\nexport function createInMemoryWorkProductStore(): InMemoryWorkProductStore {\n const rows = new Map<string, WorkProductRecord>()\n const events: WorkProductAuditEvent[] = []\n\n return {\n async load(id) {\n const record = rows.get(id)\n return record ? structuredClone(record) : null\n },\n async findDraft(workspaceId, scopeKey) {\n for (const record of rows.values()) {\n if (\n record.workspaceId === workspaceId &&\n record.scopeKey === scopeKey &&\n (record.status === 'draft' || record.status === 'blocked')\n ) {\n return structuredClone(record)\n }\n }\n return null\n },\n async listByWorkspace(workspaceId, opts) {\n const out: WorkProductRecord[] = []\n for (const record of rows.values()) {\n if (record.workspaceId !== workspaceId) continue\n if (opts?.status && !opts.status.includes(record.status)) continue\n out.push(structuredClone(record))\n }\n return out\n },\n async insert(record) {\n if (rows.has(record.id)) throw new Error(`Work product ${record.id} already exists`)\n rows.set(record.id, structuredClone(record))\n return structuredClone(record)\n },\n async update(id, guard, patch) {\n const current = rows.get(id)\n if (!current) return null\n if (guard.status !== undefined && current.status !== guard.status) return null\n if (guard.version !== undefined && current.version !== guard.version) return null\n const next: WorkProductRecord = { ...current }\n if (patch.status !== undefined) next.status = patch.status\n if (patch.version !== undefined) next.version = patch.version\n if (patch.artifact !== undefined) next.artifact = patch.artifact\n if (patch.evidence !== undefined) next.evidence = patch.evidence\n if (patch.exceptions !== undefined) next.exceptions = patch.exceptions\n if (patch.checks !== undefined) next.checks = patch.checks\n if (patch.provenance !== undefined) next.provenance = patch.provenance\n if (patch.history !== undefined) next.history = patch.history\n if (patch.updatedAt !== undefined) next.updatedAt = patch.updatedAt\n rows.set(id, structuredClone(next))\n return structuredClone(next)\n },\n async appendEvent(event) {\n events.push(structuredClone(event))\n },\n events() {\n return events.map((event) => structuredClone(event))\n },\n put(record) {\n rows.set(record.id, structuredClone(record))\n },\n }\n}\n","/**\n * Provenance stamping — the backtest spine. Every version of every work\n * product carries `profileHash + runId + servingModels`, stamped at the two\n * honest moments:\n *\n * 1. At dispatch, the product route composes the turn's profile and computes\n * agent-eval's `agentProfileHash(profile)` — the SAME hash that keys the\n * app's scorecard cells — and closes it (plus runId/sessionId) into the\n * `provenance` seam `buildWorkProductTools` receives, so the model can\n * neither omit nor forge it.\n * 2. At turn completion, the route's existing lifecycle seam calls\n * {@link finalizeWorkProductProvenance}: serving model and cost come from\n * the usage receipt, because only the completed turn knows what actually\n * served. `servingModels` is honestly EMPTY until then.\n *\n * The trust bridge ({@link workProductTrustInputs}) maps judge-sourced\n * quality verdicts into `/eval-campaign`'s `trustVerdicts()` input with\n * type-only imports — products call `trustVerdicts` from\n * `@tangle-network/agent-app/eval-campaign` (which they already import for\n * their ensemble loop). Deliberately NOT a value re-export here: it would put\n * agent-eval's runtime on `/work-product`'s import path and drag the eval\n * engine into every product worker bundle.\n */\n\nimport type { JudgeVerdict } from '@tangle-network/agent-eval'\nimport type { TrustItem } from '../eval-campaign/trust-gate'\nimport type {\n WorkProductProvenance,\n WorkProductRecord,\n WorkProductStorePort,\n} from './types'\n\nexport type { TrustItem }\n\n/** The dispatch-time provenance closure's output: everything the route knows\n * before the turn completes. */\nexport type WorkProductProvenanceBase = Omit<WorkProductProvenance, 'servingModels' | 'producedAt'>\n\n/** Stamp a full provenance from the dispatch-time base. `servingModels` starts\n * empty (honestly absent — never guessed) until the completion back-fill. */\nexport function stampProvenance(base: WorkProductProvenanceBase, now: () => number = Date.now): WorkProductProvenance {\n return { ...base, servingModels: [], producedAt: now() }\n}\n\n/** Completion receipt for one run, from the turn's usage/lifecycle seam. */\nexport interface FinalizeWorkProductProvenanceInput {\n workspaceId: string\n /** The chat turnId / mission-step run id the records were stamped with. */\n runId: string\n /** What actually served, from the usage receipt / serving-model header. */\n servingModels: readonly string[]\n costUsd?: number\n logger?: Pick<Console, 'warn'>\n}\n\n/**\n * Back-fill `servingModels`/`costUsd` onto every record (and its history\n * entries) stamped with `runId` — wire it into the chat route's existing\n * `lifecycle.onTurnComplete` seam; no new hook. Returns the updated records.\n * A CAS miss on one record is logged and skipped (the next completion or a\n * re-read re-applies); it never throws mid-fleet.\n */\nexport async function finalizeWorkProductProvenance(\n store: WorkProductStorePort,\n input: FinalizeWorkProductProvenanceInput,\n): Promise<WorkProductRecord[]> {\n const rows = await store.listByWorkspace(input.workspaceId)\n const updated: WorkProductRecord[] = []\n for (const record of rows) {\n const recordMatches = record.provenance.runId === input.runId\n const historyMatches = record.history.some((entry) => entry.provenance.runId === input.runId)\n if (!recordMatches && !historyMatches) continue\n const finalize = (provenance: WorkProductProvenance): WorkProductProvenance => ({\n ...provenance,\n servingModels: [...input.servingModels],\n ...(input.costUsd === undefined ? {} : { costUsd: input.costUsd }),\n })\n const next = await store.update(\n record.id,\n { status: record.status, version: record.version },\n {\n ...(recordMatches ? { provenance: finalize(record.provenance) } : {}),\n history: record.history.map((entry) =>\n entry.provenance.runId === input.runId ? { ...entry, provenance: finalize(entry.provenance) } : entry,\n ),\n },\n )\n if (!next) {\n input.logger?.warn(`[work-product] provenance back-fill lost a race on ${record.id}; skipped`)\n continue\n }\n await store.appendEvent({\n workProductId: record.id,\n workspaceId: record.workspaceId,\n step: 'wp.provenance',\n message: `Serving models back-filled for run ${input.runId}`,\n metadata: { runId: input.runId, servingModels: [...input.servingModels], costUsd: input.costUsd ?? null },\n at: Date.now(),\n })\n updated.push(next)\n }\n return updated\n}\n\n/**\n * Trust-gate bridge: one `TrustItem` per work product whose production\n * quality was scored by the product's eval-campaign ensemble. `verdictsFor`\n * returns the per-judge raw verdicts the product retained for a record (the\n * same verdicts `aggregateJudgeVerdicts` reduces); records without verdicts\n * are omitted. Feed the result to `/eval-campaign`'s `trustVerdicts()` —\n * an untrusted verdict renders as \"quality: unverified\", never a naked\n * number. Zero statistics code here: pure mapping.\n */\nexport function workProductTrustInputs<D extends string = string>(\n records: readonly WorkProductRecord[],\n verdictsFor: (record: WorkProductRecord) => readonly JudgeVerdict<D>[] | undefined,\n): TrustItem<D>[] {\n const items: TrustItem<D>[] = []\n for (const record of records) {\n const verdicts = verdictsFor(record)\n if (!verdicts || verdicts.length === 0) continue\n items.push({ itemId: record.id, verdicts })\n }\n return items\n}\n","/**\n * Verbatim quote verification — the lineage gate.\n *\n * An evidence `locator.quote` is the reviewer's click target: it must land on\n * text that is actually in the document the entry names. Nothing here is\n * fuzzy. The only latitude is representational — the same characters written\n * differently by a PDF extractor, a text transcription, or a model repeating\n * what it read: Unicode compatibility forms, curly quotes, the several dash\n * codepoints, non-breaking spaces, and run-length whitespace. A quote that\n * still does not occur after that is not a formatting difference; it is text\n * the document does not contain.\n *\n * Deliberately NOT tolerated, because each one re-opens the hole this closes:\n * case (a quote is a quote), token subsets, edit distance, word-overlap\n * scoring, and \"the numbers match\" heuristics. A fabricated quote and a real\n * one differ by exactly the thing a fuzzy matcher forgives.\n */\n\n/** Whitespace folded to a single ASCII space: the Unicode space separators\n * (`\\p{Zs}`), the line/paragraph separators, and the zero-width characters a\n * PDF text layer leaves behind (ZWSP / ZWNJ / ZWJ / BOM). */\nconst WHITESPACE = /[\\s\\p{Zs}\\u2028\\u2029\\u200b-\\u200d\\ufeff]+/gu\n\n/** Dash-like codepoints that render alike but differ by byte: non-breaking\n * hyphen, figure / en / em / horizontal dash, minus sign, small and fullwidth\n * forms. */\nconst DASHES = /[\\u2010-\\u2015\\u2212\\ufe58\\ufe63\\uff0d]/gu\n\n/** Curly, prime and grave apostrophes folded to ASCII `'`. */\nconst SINGLE_QUOTES = /[\\u2018\\u2019\\u201a\\u201b\\u2032\\u2035\\u00b4`]/gu\n\n/** Curly, low, prime and guillemet double quotes folded to ASCII `\"`. */\nconst DOUBLE_QUOTES = /[\\u201c\\u201d\\u201e\\u201f\\u2033\\u2036\\u00ab\\u00bb]/gu\n\n/**\n * Fold a string to the form both sides of a quote comparison are measured in.\n * Composition and presentation only: NFKC, one dash, one apostrophe, one\n * double quote, runs of any whitespace to a single space, trimmed. Case is\n * PRESERVED — lowercasing would let \"Box 1\" match \"box 1\", and a citation\n * that cannot reproduce capitalization did not read the document.\n */\nexport function normalizeQuoteText(value: string): string {\n return value\n .normalize('NFKC')\n .replace(DASHES, '-')\n .replace(SINGLE_QUOTES, \"'\")\n .replace(DOUBLE_QUOTES, '\"')\n .replace(WHITESPACE, ' ')\n .trim()\n}\n\n/**\n * Does `quote` occur in `sourceText`? Exact substring first (the common case,\n * and free); the normalized comparison second, for the representational\n * differences above. Nothing else.\n *\n * An empty or whitespace-only quote is NOT a match — it would otherwise be a\n * substring of every document and pass the gate vacuously.\n */\nexport function sourceContainsQuote(sourceText: string, quote: string): boolean {\n const trimmed = quote.trim()\n if (trimmed.length === 0) return false\n if (sourceText.includes(trimmed)) return true\n const normalizedQuote = normalizeQuoteText(quote)\n if (normalizedQuote.length === 0) return false\n return normalizeQuoteText(sourceText).includes(normalizedQuote)\n}\n","/**\n * The agent-facing work-product side channel — three registry `customTools`\n * built on `/tools`' `defineAppTool`, dispatched through `dispatchAppTool`'s\n * single validation/outcome path (a thrown `ToolInputError` → correctable 4xx\n * back to the model; any other throw → internal error; a call never silently\n * succeeds without its effect). Deliberately NOT new `/tools` built-ins:\n * extending `AppToolHandlers` would break every existing consumer, and the\n * registry seam exists precisely for a product tool family like this.\n *\n * Partial-emission contract across a long turn: the draft ROW is the\n * accumulator. Evidence and exceptions stream in as found via small batched\n * calls; the artifact arrives once at the end. The agent addresses everything\n * by `scopeKey` — the server mints row ids, the model never invents them, and\n * identity (userId/workspaceId/threadId) rides the trusted `AppToolContext`\n * from headers, never model args. A turn that dies mid-emission leaves a\n * consistent draft the next turn resumes by the same scopeKey.\n */\n\nimport { ToolInputError } from '../tools/errors'\nimport { defineAppTool, type AppToolDefinition } from '../tools/registry'\nimport type { AppToolContext } from '../tools/types'\nimport { createWorkProductService, type WorkProductOutcome, type WorkProductService } from './service'\nimport { stampProvenance, type WorkProductProvenanceBase } from './provenance'\nimport { sourceContainsQuote } from './quote'\nimport {\n parseAgentCheckInput,\n parseArtifactInput,\n parseEvidenceInput,\n parseExceptionInput,\n unresolvedBlockingExceptions,\n type EvidenceEntry,\n type ExceptionEntry,\n type QualityCheck,\n type WorkProductArtifact,\n type WorkProductRecord,\n type WorkProductStorePort,\n} from './types'\n\n/** Max entries per `upsert_evidence`/`flag_exception` call — keeps each call a\n * small batch the model can correct precisely on a named-index failure. */\nexport const MAX_WORK_PRODUCT_BATCH = 50\n\n/** Platform check: every material target has ≥1 evidence row. Recorded as\n * `QualityCheck{source:'platform'}`. */\nexport const EVIDENCE_COVERAGE_CHECK = 'evidence_coverage'\n\n/** Platform check: how many evidence entries carry a quote the shell PROVED\n * occurs in the source it names. Recorded when `readSourceText` is wired, so\n * a reviewer reads the strength of the lineage off the row itself rather than\n * trusting that a quote was checked. */\nexport const QUOTE_VERIFICATION_CHECK = 'quote_verification'\n\n/** Domain seams for the three work-product tools — every domain word is a\n * parameter; the shell bakes none. */\nexport interface WorkProductToolConfig {\n store: WorkProductStorePort\n /** PARAMETER — accepted `artifact.kind` values, validated on submit. */\n artifactKinds: readonly string[]\n /** PARAMETER — accepted exception `kind` values. */\n exceptionKinds: readonly string[]\n /** Fail-loud source check: resolve an evidence `sourceRef` to existence\n * (vault stat / attachment lookup). A dangling ref is a `ToolInputError`\n * naming the entry index — lineage can never point at nothing. */\n resolveSourceRef: (ref: string, ctx: AppToolContext) => Promise<boolean>\n /** Fail-loud QUOTE verification: return the source document's TEXT for a\n * ref so the shell can prove each `locator.quote` occurs in it verbatim.\n * Wiring this turns the gate ON — a quote that does not occur is a\n * `ToolInputError` naming the entry index, so the model re-extracts from\n * the document instead of persisting invented lineage.\n *\n * Return `null` ONLY when the ref genuinely has no extractable text (an\n * image scan, an opaque blob). The gate is fail-CLOSED on `null`: a quote\n * that cannot be checked is refused, because \"unverifiable\" and \"verified\"\n * must never look the same to a reviewer. Such an entry is still recordable\n * without `locator.quote` — `claim` carries the assertion.\n *\n * Omit the seam entirely and no quote is checked. */\n readSourceText?: (ref: string, ctx: AppToolContext) => Promise<string | null>\n /** The material targets the platform coverage check requires evidence for.\n * Product-owned vocabulary; omit to skip the coverage gate. */\n materialTargets?: (artifact: WorkProductArtifact) => string[]\n /** Per-turn provenance closure the ROUTE supplies (profileHash + runId are\n * known at dispatch; trusted, never read from model args). */\n provenance: (ctx: AppToolContext) => WorkProductProvenanceBase\n /** Called on the draft→ready commit so the route persists the transcript\n * anchor part and the queue projection updates. */\n onReady?: (record: WorkProductRecord, ctx: AppToolContext) => void | Promise<void>\n /** Injectable clock / id generator (tests, deterministic ids). */\n now?: () => number\n generateId?: () => string\n}\n\n/** Unwrap a guarded outcome, retrying ONCE on a lost race (the single\n * serialized turn owner makes real contention rare; one re-read-and-retry\n * absorbs the benign case). A deterministic rejection maps to a correctable\n * `ToolInputError` so the model learns exactly why. */\nasync function unwrap<T>(\n run: () => Promise<WorkProductOutcome<T>>,\n code: string,\n): Promise<T> {\n let outcome = await run()\n if (!outcome.succeeded && outcome.conflict) outcome = await run()\n if (!outcome.succeeded) throw new ToolInputError(code, outcome.error, outcome.conflict ? 409 : 400)\n return outcome.value\n}\n\nfunction requireScopeKey(args: Record<string, unknown>): string {\n const scopeKey = typeof args.scopeKey === 'string' ? args.scopeKey.trim() : ''\n if (!scopeKey) throw new ToolInputError('missing_scope_key', 'scopeKey is required — the engagement key this work product belongs to.')\n return scopeKey\n}\n\nfunction requireBatch(args: Record<string, unknown>, field: string): unknown[] {\n const raw = args[field]\n if (!Array.isArray(raw) || raw.length === 0) {\n throw new ToolInputError('missing_entries', `${field} must be a non-empty array.`)\n }\n if (raw.length > MAX_WORK_PRODUCT_BATCH) {\n throw new ToolInputError('batch_too_large', `${field} accepts at most ${MAX_WORK_PRODUCT_BATCH} entries per call — send smaller batches.`)\n }\n return raw\n}\n\n/**\n * Resolve the scope's open draft row, creating or reopening as needed:\n * an open `draft`/`blocked` row resumes; a `changes_requested` row reopens as\n * the correction draft (version +1); a `ready` row refuses — the package is\n * awaiting review and must receive a verdict first; otherwise a fresh draft\n * is created at `last reviewed version + 1` with dispatch-stamped provenance.\n */\nasync function resolveDraft(\n service: WorkProductService,\n config: WorkProductToolConfig,\n scopeKey: string,\n ctx: AppToolContext,\n): Promise<WorkProductRecord> {\n const open = await service.openDraft(ctx.workspaceId, scopeKey)\n if (open) return open\n const awaitingReview = await service.awaitingReview(ctx.workspaceId, scopeKey)\n if (awaitingReview) {\n throw new ToolInputError(\n 'awaiting_review',\n `Work product for ${scopeKey} (v${awaitingReview.version}) is awaiting review — no further emission until a reviewer verdict.`,\n 409,\n )\n }\n const awaitingCorrection = await service.awaitingCorrection(ctx.workspaceId, scopeKey)\n if (awaitingCorrection) {\n return unwrap(() => service.reopen(awaitingCorrection.id), 'reopen_failed')\n }\n return service.create({\n workspaceId: ctx.workspaceId,\n threadId: ctx.threadId,\n scopeKey,\n version: await service.nextVersion(ctx.workspaceId, scopeKey),\n provenance: stampProvenance(config.provenance(ctx), config.now),\n })\n}\n\n/**\n * Prove every supplied `locator.quote` occurs in the source it names, or\n * throw the correctable error that sends the model back to the document.\n *\n * Source texts are read once per distinct `sourceRef` in the batch — a\n * 50-entry batch citing three documents is three reads, not fifty.\n *\n * No-op when `readSourceText` is not wired: the product has not given the\n * shell a way to see its documents, and inventing a weaker check here would\n * report unverified lineage as verified.\n */\nasync function assertQuotesOccurInSources(\n config: WorkProductToolConfig,\n entries: readonly EvidenceEntry[],\n ctx: AppToolContext,\n): Promise<void> {\n const readSourceText = config.readSourceText\n if (!readSourceText) return\n const texts = new Map<string, string | null>()\n for (let index = 0; index < entries.length; index += 1) {\n const entry = entries[index]!\n const quote = entry.locator.quote\n if (quote === undefined || quote.trim().length === 0) continue\n if (!texts.has(entry.sourceRef)) texts.set(entry.sourceRef, await readSourceText(entry.sourceRef, ctx))\n const text = texts.get(entry.sourceRef)\n if (text === null || text === undefined) {\n throw new ToolInputError(\n 'unverifiable_quote',\n `entries[${index}].locator.quote: \"${entry.sourceRef}\" has no readable text, so the quote cannot be verified. Record the entry without locator.quote and state the basis in claim.`,\n )\n }\n if (!sourceContainsQuote(text, quote)) {\n throw new ToolInputError(\n 'quote_not_found',\n `entries[${index}].locator.quote: ${JSON.stringify(quote)} does not occur in \"${entry.sourceRef}\". Copy the supporting line character-for-character out of that document, or — if this value is COMPUTED rather than read — omit locator.quote and state the computation in claim.`,\n )\n }\n }\n}\n\n/** Re-verify every persisted quote at submit time. The upsert gate stops new\n * fabrication; this stops a package whose evidence was written BEFORE the\n * gate existed (or under a since-corrected document) from reaching a\n * reviewer. Entries with no quote are neither verified nor failures — they\n * are lineage with no click target, counted separately so the recorded check\n * states how much of the package is quote-backed. */\nasync function summarizeQuoteVerification(\n config: WorkProductToolConfig,\n evidence: readonly EvidenceEntry[],\n ctx: AppToolContext,\n): Promise<{ verified: number; withoutQuote: number; failed: string[] } | undefined> {\n const readSourceText = config.readSourceText\n if (!readSourceText) return undefined\n const texts = new Map<string, string | null>()\n let verified = 0\n let withoutQuote = 0\n const failed: string[] = []\n for (const entry of evidence) {\n const quote = entry.locator.quote\n if (quote === undefined || quote.trim().length === 0) {\n withoutQuote += 1\n continue\n }\n if (!texts.has(entry.sourceRef)) texts.set(entry.sourceRef, await readSourceText(entry.sourceRef, ctx))\n const text = texts.get(entry.sourceRef)\n if (typeof text === 'string' && sourceContainsQuote(text, quote)) verified += 1\n else failed.push(entry.id)\n }\n return { verified, withoutQuote, failed }\n}\n\n/** Build the three work-product tools for `customTools` registration on the\n * MCP server / HTTP handler / runtime executor. */\nexport function buildWorkProductTools(config: WorkProductToolConfig): AppToolDefinition[] {\n const service = createWorkProductService({\n store: config.store,\n ...(config.now ? { now: config.now } : {}),\n ...(config.generateId ? { generateId: config.generateId } : {}),\n })\n\n const upsertEvidence = defineAppTool({\n name: 'upsert_evidence',\n description:\n 'Record source→field lineage for the current work product, incrementally as you find it. Each entry links a source document (sourceRef + locator) to one artifact target and states the claim it supports. Re-emitting an entry id replaces that entry. Address the work product by scopeKey; the first call creates the draft.',\n parameters: {\n type: 'object',\n properties: {\n scopeKey: { type: 'string', description: 'Engagement key for this work product.' },\n entries: {\n type: 'array',\n minItems: 1,\n maxItems: MAX_WORK_PRODUCT_BATCH,\n items: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Stable entry id — re-emit to replace.' },\n sourceRef: { type: 'string', description: 'Vault path / attachment id of the SOURCE document.' },\n locator: {\n type: 'object',\n properties: {\n page: { type: 'number' },\n range: { type: 'string', description: \"Free-form location: 'L120-L134' | 'B7' | '¶4'.\" },\n quote: {\n type: 'string',\n description:\n 'Verbatim supporting quote — copied character-for-character out of the source, not retyped, reformatted, or summarised. The platform checks it occurs in the document and REFUSES the entry if it does not. For a value you COMPUTED rather than read, omit this field and state the computation in claim.',\n },\n },\n },\n target: { type: 'string', description: 'Artifact field/claim this evidence supports.' },\n claim: { type: 'string', description: 'The value/assertion at the target.' },\n confidence: { type: 'number', minimum: 0, maximum: 1 },\n },\n required: ['id', 'sourceRef', 'target', 'claim'],\n },\n },\n },\n required: ['scopeKey', 'entries'],\n },\n async execute(args: Record<string, unknown>, ctx: AppToolContext) {\n const scopeKey = requireScopeKey(args)\n const raw = requireBatch(args, 'entries')\n const entries: EvidenceEntry[] = []\n for (let index = 0; index < raw.length; index += 1) {\n const parsed = parseEvidenceInput(raw[index], `entries[${index}]`)\n if (!parsed.ok) throw new ToolInputError('invalid_evidence', `${parsed.field}: ${parsed.error}`)\n entries.push(parsed.value)\n }\n // Fail-loud source resolution: lineage can never point at nothing.\n for (let index = 0; index < entries.length; index += 1) {\n const entry = entries[index]!\n if (!(await config.resolveSourceRef(entry.sourceRef, ctx))) {\n throw new ToolInputError(\n 'unknown_source_ref',\n `entries[${index}].sourceRef: \"${entry.sourceRef}\" does not resolve to an existing source document.`,\n )\n }\n }\n // Fail-loud quote verification: a quote must occur in the document the\n // entry names. Rejected BEFORE the draft is resolved, so a batch with a\n // fabricated quote persists nothing — the model corrects and re-sends\n // rather than leaving a half-written row behind.\n await assertQuotesOccurInSources(config, entries, ctx)\n const draft = await resolveDraft(service, config, scopeKey, ctx)\n const record = await unwrap(() => service.upsertEvidence(draft.id, entries), 'evidence_rejected')\n return { workProductId: record.id, version: record.version, evidenceCount: record.evidence.length }\n },\n })\n\n const flagException = defineAppTool({\n name: 'flag_exception',\n description:\n 'Flag problems with the current work product (missing documents, inconsistent sources, …). An unresolved blocking exception parks the work product until it is resolved; re-emit the same id with resolved:true to release it. Address by scopeKey.',\n parameters: {\n type: 'object',\n properties: {\n scopeKey: { type: 'string', description: 'Engagement key for this work product.' },\n exceptions: {\n type: 'array',\n minItems: 1,\n maxItems: MAX_WORK_PRODUCT_BATCH,\n items: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Stable entry id — re-emit to replace.' },\n severity: { type: 'string', enum: ['blocking', 'material', 'advisory'] },\n kind: { type: 'string', description: 'Exception kind from the product vocabulary.' },\n message: { type: 'string' },\n targets: { type: 'array', items: { type: 'string' } },\n resolved: { type: 'boolean' },\n resolutionNote: { type: 'string' },\n },\n required: ['id', 'severity', 'kind', 'message'],\n },\n },\n },\n required: ['scopeKey', 'exceptions'],\n },\n async execute(args: Record<string, unknown>, ctx: AppToolContext) {\n const scopeKey = requireScopeKey(args)\n const raw = requireBatch(args, 'exceptions')\n const entries: ExceptionEntry[] = []\n for (let index = 0; index < raw.length; index += 1) {\n const parsed = parseExceptionInput(raw[index], `exceptions[${index}]`)\n if (!parsed.ok) throw new ToolInputError('invalid_exception', `${parsed.field}: ${parsed.error}`)\n if (!config.exceptionKinds.includes(parsed.value.kind)) {\n throw new ToolInputError(\n 'invalid_exception',\n `exceptions[${index}].kind: must be one of: ${config.exceptionKinds.join(', ')}`,\n )\n }\n // An agent resolving its own exception is tagged as such; the reviewer\n // tag is reserved for the verdict route.\n if (parsed.value.resolved && parsed.value.resolvedBy === undefined) parsed.value.resolvedBy = 'agent'\n entries.push(parsed.value)\n }\n const draft = await resolveDraft(service, config, scopeKey, ctx)\n const record = await unwrap(() => service.upsertExceptions(draft.id, entries), 'exception_rejected')\n return {\n workProductId: record.id,\n version: record.version,\n status: record.status,\n unresolvedBlocking: unresolvedBlockingExceptions(record.exceptions).length,\n }\n },\n })\n\n const submitWorkProduct = defineAppTool({\n name: 'submit_work_product',\n description:\n 'Submit the finished work product for professional review — the terminal call after evidence and exceptions are recorded. Refused while a blocking exception is unresolved, or when a material target lacks evidence. Include your own quality checks in `checks`.',\n parameters: {\n type: 'object',\n properties: {\n scopeKey: { type: 'string', description: 'Engagement key for this work product.' },\n artifact: {\n type: 'object',\n properties: {\n kind: { type: 'string', description: 'Artifact kind from the product vocabulary.' },\n title: { type: 'string' },\n path: { type: 'string', description: 'Vault/object-store ref of the rendered document.' },\n content: { type: 'string', description: 'Inline body when small (markdown/JSON).' },\n mediaType: { type: 'string' },\n baseline: {\n type: 'object',\n properties: { path: { type: 'string' }, content: { type: 'string' } },\n description: 'For diff-first artifacts: the source document being redlined.',\n },\n fields: { type: 'object', description: 'Structured field map lineage targets anchor to.' },\n },\n required: ['kind', 'title'],\n },\n checks: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n id: { type: 'string' },\n name: { type: 'string' },\n passed: { type: 'boolean' },\n detail: { type: 'string' },\n },\n required: ['id', 'name', 'passed'],\n },\n description: 'Your own quality self-checks (recorded as agent-sourced).',\n },\n },\n required: ['scopeKey', 'artifact'],\n },\n async execute(args: Record<string, unknown>, ctx: AppToolContext) {\n const scopeKey = requireScopeKey(args)\n const parsedArtifact = parseArtifactInput(args.artifact)\n if (!parsedArtifact.ok) throw new ToolInputError('invalid_artifact', `${parsedArtifact.field}: ${parsedArtifact.error}`)\n const artifact = parsedArtifact.value\n if (!config.artifactKinds.includes(artifact.kind)) {\n throw new ToolInputError('invalid_artifact', `artifact.kind: must be one of: ${config.artifactKinds.join(', ')}`)\n }\n const agentChecks: QualityCheck[] = []\n if (args.checks !== undefined) {\n if (!Array.isArray(args.checks)) throw new ToolInputError('invalid_checks', 'checks must be an array when present.')\n for (let index = 0; index < args.checks.length; index += 1) {\n const parsed = parseAgentCheckInput(args.checks[index], `checks[${index}]`)\n if (!parsed.ok) throw new ToolInputError('invalid_checks', `${parsed.field}: ${parsed.error}`)\n agentChecks.push({ ...parsed.value, source: 'agent' })\n }\n }\n\n const draft = await resolveDraft(service, config, scopeKey, ctx)\n const blocking = unresolvedBlockingExceptions(draft.exceptions)\n if (blocking.length > 0) {\n throw new ToolInputError(\n 'blocking_exceptions_unresolved',\n `Cannot submit: ${blocking.length} unresolved blocking exception(s) (${blocking.map((entry) => entry.id).join(', ')}). Resolve each via flag_exception with resolved:true, or downgrade its severity with a resolutionNote justifying why it does not block.`,\n 409,\n )\n }\n\n // Platform check one: every persisted quote still occurs in the source\n // it names. The upsert gate stops NEW fabrication; this stops a package\n // whose evidence predates the gate from reaching a reviewer. Recorded on\n // the row AND rejected fail-loud — the same discipline as coverage.\n const checks: QualityCheck[] = [...agentChecks]\n const quotes = await summarizeQuoteVerification(config, draft.evidence, ctx)\n if (quotes) {\n const quoted = quotes.verified + quotes.failed.length\n checks.unshift({\n id: QUOTE_VERIFICATION_CHECK,\n name: QUOTE_VERIFICATION_CHECK,\n passed: quotes.failed.length === 0,\n detail:\n quotes.failed.length === 0\n ? `${quotes.verified}/${quoted} quoted evidence entries verified against their source; ${quotes.withoutQuote} recorded without a quote`\n : `Unverifiable quotes on: ${quotes.failed.join(', ')}`,\n source: 'platform',\n })\n if (quotes.failed.length > 0) {\n await unwrap(() => service.recordChecks(draft.id, checks), 'checks_rejected')\n throw new ToolInputError(\n 'quote_verification_failed',\n `Cannot submit: ${quotes.failed.length} evidence entr${quotes.failed.length === 1 ? 'y quotes' : 'ies quote'} text that does not occur in the source named (${quotes.failed.join(', ')}). Re-emit each with a quote copied character-for-character from that document, or without locator.quote if the value is computed.`,\n )\n }\n }\n\n // Platform check two: every material target has ≥1 evidence row. A\n // failing coverage check is BOTH recorded on the row (visible to the\n // queue) and rejected fail-loud — a lineage-free package can never reach\n // a reviewer.\n if (config.materialTargets) {\n const targets = config.materialTargets(artifact)\n const covered = new Set(draft.evidence.map((entry) => entry.target))\n const missing = targets.filter((target) => !covered.has(target))\n const coverage: QualityCheck = {\n id: EVIDENCE_COVERAGE_CHECK,\n name: EVIDENCE_COVERAGE_CHECK,\n passed: missing.length === 0,\n detail:\n missing.length === 0\n ? `${targets.length}/${targets.length} material targets evidenced`\n : `Missing evidence for: ${missing.join(', ')}`,\n source: 'platform',\n }\n checks.unshift(coverage)\n if (missing.length > 0) {\n await unwrap(() => service.recordChecks(draft.id, checks), 'checks_rejected')\n throw new ToolInputError(\n 'evidence_coverage_failed',\n `Cannot submit: material targets lack evidence: ${missing.join(', ')}. Add upsert_evidence entries targeting each, then resubmit.`,\n )\n }\n }\n\n const provenance = stampProvenance(config.provenance(ctx), config.now)\n const record = await unwrap(\n () => service.submit(draft.id, { artifact, checks, provenance }),\n 'submit_rejected',\n )\n await config.onReady?.(record, ctx)\n return {\n workProductId: record.id,\n version: record.version,\n status: record.status,\n checks: record.checks.map((check) => ({ name: check.name, passed: check.passed, source: check.source })),\n }\n },\n })\n\n return [upsertEvidence, flagException, submitWorkProduct]\n}\n","/**\n * Framework-neutral work-product review endpoints — the\n * `createInteractionAnswerRoute` factory pattern: web-standard\n * `Request`/`Response`, ONE product-supplied `authorize` seam (session auth,\n * workspace access, reviewer identity, rate limits), everything behind it\n * mechanism. Server-only and subpath-only: never reachable from a client\n * bundle (enforced by the browser-safety test).\n *\n * The verdict endpoint is deliberately NOT a second approval broker: agent\n * asks stay on `/interactions`; this is the REVIEWER's plain authorized\n * verdict on a ready package — one human-in-the-loop channel per direction.\n * A `request_changes` note re-enters chat as the correction turn via the\n * `onVerdict` seam; chat remains the driver surface.\n */\n\nimport {\n createWorkProductService,\n type WorkProductService,\n} from './service'\nimport {\n workProductToPersistedPart,\n type WorkProductPersistedPart,\n type WorkProductRecord,\n type WorkProductStorePort,\n} from './types'\n\n/** Reviewer verdict wire body for the POST endpoint. */\nexport type WorkProductVerdictBody =\n | { ok: true; id: string; verdict: 'approve'; note?: string }\n | { ok: true; id: string; verdict: 'request_changes'; note: string }\n | { ok: false; error: string }\n\n/** Validate the verdict POST body: `{ id, verdict, note? }`; a\n * `request_changes` verdict REQUIRES a non-empty note — the note IS the\n * correction turn the agent works from. */\nexport function validateWorkProductVerdictBody(body: Record<string, unknown>): WorkProductVerdictBody {\n const id = typeof body.id === 'string' && body.id.trim() ? body.id.trim() : null\n if (!id) return { ok: false, error: 'Missing work product id' }\n const verdict = body.verdict\n if (verdict !== 'approve' && verdict !== 'request_changes') {\n return { ok: false, error: 'Invalid verdict: expected approve or request_changes' }\n }\n const note = body.note === undefined ? undefined : typeof body.note === 'string' ? body.note.trim() : null\n if (note === null) return { ok: false, error: 'Invalid note: expected a string' }\n if (verdict === 'request_changes') {\n if (!note) return { ok: false, error: 'request_changes requires a note — it becomes the correction instruction in chat' }\n return { ok: true, id, verdict, note }\n }\n return note ? { ok: true, id, verdict, note } : { ok: true, id, verdict }\n}\n\n/** The product seam's verdict for one request: authenticated reviewer +\n * workspace, or a product-authored short-circuit Response (401/403/429…). */\nexport type WorkProductRouteAuthorization =\n | { ok: true; workspaceId: string; reviewedBy: string }\n | { ok: false; response: Response }\n\n/** Auth seam arguments carrying the request, the endpoint intent, and the parsed verdict body */\nexport interface WorkProductAuthorizeArgs {\n request: Request\n intent: 'list' | 'detail' | 'verdict'\n /** The parsed, validated POST body (verdict intent only). */\n body?: Record<string, unknown>\n}\n\n/** Configuration options assembling the review endpoints from the store and product seams */\nexport interface WorkProductRoutesOptions {\n store: WorkProductStorePort\n /** Authenticate + authorize the caller; the ONLY product access step. */\n authorize: (args: WorkProductAuthorizeArgs) => Promise<WorkProductRouteAuthorization>\n /** Post-verdict product seam: post the `request_changes` note into the\n * driving chat thread as the correction turn, notify, etc. Runs after the\n * transition commits; a throw is logged, never unwinds the verdict. */\n onVerdict?: (args: {\n record: WorkProductRecord\n verdict: 'approve' | 'request_changes'\n note?: string\n reviewedBy: string\n }) => void | Promise<void>\n /** Persist/update the transcript anchor part reflecting the new status, so\n * the chat card flips with the verdict. */\n persistAnchorPart?: (part: WorkProductPersistedPart, record: WorkProductRecord) => void | Promise<void>\n /** Future integration point fired on approval (push to a DMS/CRM/export\n * pipeline). A stub seam by design — export itself stays the product's\n * signed object-store download. */\n onExport?: (record: WorkProductRecord) => void | Promise<void>\n logger?: Pick<Console, 'warn' | 'error'>\n now?: () => number\n}\n\n/** Assembled review endpoints returning web-standard Responses */\nexport interface WorkProductRoutes {\n /** GET — the workspace's records for the queue projection. Optional\n * `?status=a,b` filter. */\n list: (request: Request) => Promise<Response>\n /** GET — one record by id (404 when absent or outside the workspace). */\n detail: (request: Request, id: string) => Promise<Response>\n /** POST `{ id, verdict, note? }` — the reviewer verdict: CAS transition +\n * history entry + product seams. 409 when the record is no longer ready. */\n verdict: (request: Request) => Promise<Response>\n}\n\n/** Create the work-product review endpoints over the store port and product seams */\nexport function createWorkProductRoutes(options: WorkProductRoutesOptions): WorkProductRoutes {\n const logger = options.logger ?? console\n const service: WorkProductService = createWorkProductService({\n store: options.store,\n ...(options.now ? { now: options.now } : {}),\n })\n\n async function list(request: Request): Promise<Response> {\n const auth = await options.authorize({ request, intent: 'list' })\n if (!auth.ok) return auth.response\n const url = new URL(request.url)\n const statusParam = url.searchParams.get('status')\n const statuses = statusParam\n ? statusParam.split(',').map((value) => value.trim()).filter(Boolean)\n : null\n const workProducts = await options.store.listByWorkspace(\n auth.workspaceId,\n statuses ? { status: statuses as WorkProductRecord['status'][] } : undefined,\n )\n return Response.json({ workProducts })\n }\n\n async function detail(request: Request, id: string): Promise<Response> {\n const auth = await options.authorize({ request, intent: 'detail' })\n if (!auth.ok) return auth.response\n const record = await options.store.load(id)\n if (!record || record.workspaceId !== auth.workspaceId) {\n return Response.json({ error: 'Work product not found' }, { status: 404 })\n }\n return Response.json({ workProduct: record })\n }\n\n async function verdict(request: Request): Promise<Response> {\n const body = (await request.json().catch(() => null)) as Record<string, unknown> | null\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n return Response.json({ error: 'Invalid JSON body' }, { status: 400 })\n }\n const validation = validateWorkProductVerdictBody(body)\n if (!validation.ok) return Response.json({ error: validation.error }, { status: 400 })\n\n const auth = await options.authorize({ request, intent: 'verdict', body })\n if (!auth.ok) return auth.response\n\n const existing = await options.store.load(validation.id)\n if (!existing || existing.workspaceId !== auth.workspaceId) {\n return Response.json({ error: 'Work product not found' }, { status: 404 })\n }\n\n const outcome = await service.applyVerdict(validation.id, {\n verdict: validation.verdict,\n reviewedBy: auth.reviewedBy,\n ...(validation.note === undefined ? {} : { note: validation.note }),\n })\n if (!outcome.succeeded) {\n // A lost race and an illegal edge both mean \"the row is no longer the\n // ready version you looked at\" — 409 so the client re-reads.\n return Response.json({ code: 'VERDICT_CONFLICT', error: outcome.error }, { status: 409 })\n }\n const record = outcome.value\n\n // Product seams run AFTER the committed transition; their failures are\n // logged, never unwound — the verdict is durable truth at this point.\n try {\n await options.persistAnchorPart?.(workProductToPersistedPart(record), record)\n } catch (error) {\n logger.error('[work-product] persistAnchorPart failed:', error)\n }\n try {\n await options.onVerdict?.({\n record,\n verdict: validation.verdict,\n ...(validation.note === undefined ? {} : { note: validation.note }),\n reviewedBy: auth.reviewedBy,\n })\n } catch (error) {\n logger.error('[work-product] onVerdict failed:', error)\n }\n if (validation.verdict === 'approve') {\n try {\n await options.onExport?.(record)\n } catch (error) {\n logger.error('[work-product] onExport failed:', error)\n }\n }\n\n return Response.json({ ok: true, workProduct: record })\n }\n\n return { list, detail, verdict }\n}\n"],"mappings":"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","names":["record","entry"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.44.2",
3
+ "version": "0.44.3",
4
4
  "packageManager": "pnpm@10.33.4",
5
5
  "description": "Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent→app tool side channel, integration-hub client, per-workspace billing, and crypto — composed over the Tangle agent substrate through typed seams.",
6
6
  "keywords": [
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/model-resolution/failover.ts"],"sourcesContent":["/**\n * Model failover — the answer to \"one upstream hit a quota wall and the customer\n * got a Bad Gateway even though the router had abundant healthy capacity\".\n *\n * This is deliberately NOT a health probe. Probing before every turn buys a\n * round-trip of latency on the happy path and still races the outage (a model\n * healthy at probe time can 502 a second later). Failover is REACTIVE: run the\n * preferred model, and on an upstream-outage signal move to the next model in\n * the chain. Zero added latency when the preferred model works.\n *\n * Two facts drive the design, both measured against a live box during the\n * 2026-07-25 Anthropic/DeepSeek outage:\n *\n * 1. An outage is NOT always a thrown error. The sandbox resolves with a\n * payload — `{ success: false, errorCode: 'provider_inference_unavailable' }`\n * — so a classifier that only inspects `catch` misses the customer-visible\n * case entirely. `isUpstreamUnavailable` inspects resolved values too.\n * 2. Catalog membership is NOT liveness. The router's `/v1/models` still listed\n * every dead model during the outage, so `validateChatModelId` admitted\n * `claude-sonnet-4-6` while every call to it returned 502. Nothing upstream\n * of the actual call can be trusted to tell you a model is serving.\n *\n * Substrate-free: the caller injects `run`, so this composes with a sandbox\n * turn, a router chat completion, or a test fake without importing any of them.\n */\n\n/**\n * Error codes that mean \"this model's upstream is unavailable — a different\n * model may still work\". Deliberately excludes codes that would fail identically\n * on every model (bad request, auth, content filter): retrying those down a\n * chain burns latency and money to reach the same failure.\n */\nexport const UPSTREAM_UNAVAILABLE_CODES: readonly string[] = [\n 'provider_inference_unavailable',\n 'upstream_unavailable',\n 'insufficient_quota',\n 'model_not_available',\n 'server_error',\n 'bad_gateway',\n 'service_unavailable',\n]\n\n/** HTTP statuses that indicate an upstream capacity/availability problem. */\nexport const UPSTREAM_UNAVAILABLE_STATUSES: readonly number[] = [429, 500, 502, 503, 504]\n\n/**\n * Message fragments emitted by real upstreams during this class of outage.\n * Matched case-insensitively as a last resort, after code and status.\n */\nconst UPSTREAM_UNAVAILABLE_MESSAGES: readonly string[] = [\n 'bad gateway',\n 'service unavailable',\n 'inference temporarily unavailable',\n 'provider inference is unavailable',\n 'insufficient balance',\n 'usage limits',\n 'quota exceeded',\n 'rate limit',\n 'overloaded',\n 'temporarily unavailable',\n]\n\nfunction readString(source: Record<string, unknown>, key: string): string | undefined {\n const value = source[key]\n return typeof value === 'string' && value.trim().length > 0 ? value : undefined\n}\n\n/**\n * True when `signal` — a thrown error OR a resolved result payload — indicates\n * the model's upstream is unavailable and another model is worth trying.\n *\n * Checked in order of decreasing confidence: explicit code, HTTP status, then\n * message text. A resolved payload only counts as a failure when it carries an\n * explicit failure marker (`success: false`, or an `error`/`errorCode` field) —\n * a successful result is never misread as an outage.\n */\nexport function isUpstreamUnavailable(signal: unknown): boolean {\n if (signal === null || typeof signal !== 'object') return false\n const record = signal as Record<string, unknown>\n\n // A resolved payload that explicitly reports success is never an outage.\n if (record.success === true) return false\n\n const nested = record.error\n const nestedRecord = nested !== null && typeof nested === 'object' ? (nested as Record<string, unknown>) : undefined\n\n const code =\n readString(record, 'errorCode') ??\n readString(record, 'code') ??\n (nestedRecord ? (readString(nestedRecord, 'code') ?? readString(nestedRecord, 'type')) : undefined)\n if (code && UPSTREAM_UNAVAILABLE_CODES.includes(code)) return true\n\n for (const key of ['status', 'statusCode', 'httpStatus']) {\n const value = record[key]\n if (typeof value === 'number' && UPSTREAM_UNAVAILABLE_STATUSES.includes(value)) return true\n }\n\n const message =\n readString(record, 'message') ??\n readString(record, 'error') ??\n (nestedRecord ? readString(nestedRecord, 'message') : undefined)\n if (!message) return false\n const lowered = message.toLowerCase()\n return UPSTREAM_UNAVAILABLE_MESSAGES.some((fragment) => lowered.includes(fragment))\n}\n\n/** One model tried, and how it went. */\nexport interface ModelFailoverAttempt {\n model: string\n ok: boolean\n /** Why this model was abandoned. Absent when `ok`. */\n reason?: string\n}\n\n/** The outcome of a failover run: the value plus the full attempt trail. */\nexport interface ModelFailoverResult<T> {\n value: T\n /** The model that actually produced `value`. */\n model: string\n attempts: ModelFailoverAttempt[]\n /** True when the preferred (first) model did not serve the request. */\n usedFallback: boolean\n}\n\n/** Every model in the chain failed; carries the trail for logging. */\nexport class ModelFailoverExhaustedError extends Error {\n readonly attempts: ModelFailoverAttempt[]\n constructor(attempts: ModelFailoverAttempt[]) {\n const trail = attempts.map((a) => `${a.model}: ${a.reason ?? 'failed'}`).join(' | ')\n super(`All ${attempts.length} model(s) failed. ${trail}`)\n this.name = 'ModelFailoverExhaustedError'\n this.attempts = attempts\n }\n}\n\n/** Inputs to {@link runWithModelFailover}. */\nexport interface RunWithModelFailoverInput<T> {\n /** Preferred model first, then fallbacks in descending preference. */\n models: readonly string[]\n /** Executes one turn with the given model. */\n run: (model: string) => Promise<T>\n /**\n * Classifies a resolved result as an upstream outage. Defaults to\n * {@link isUpstreamUnavailable}, which understands the sandbox's\n * `{ success: false, errorCode }` payload.\n */\n isUnavailableResult?: (result: T) => boolean\n /** Classifies a thrown error. Defaults to {@link isUpstreamUnavailable}. */\n isUnavailableError?: (error: unknown) => boolean\n /** Observability hook fired each time a model is abandoned. */\n onFallback?: (attempt: ModelFailoverAttempt, nextModel: string) => void\n}\n\nfunction describe(signal: unknown): string {\n if (signal instanceof Error) return signal.message\n if (signal !== null && typeof signal === 'object') {\n const record = signal as Record<string, unknown>\n const message = readString(record, 'error') ?? readString(record, 'message') ?? readString(record, 'errorCode')\n if (message) return message\n }\n return String(signal)\n}\n\n/**\n * Run `run` against the first model in `models` that does not report an upstream\n * outage, falling through the chain in order.\n *\n * A non-outage failure (bad request, auth, content filter) is re-thrown\n * immediately rather than retried down the chain — those fail identically on\n * every model, so walking the chain would only multiply latency and spend.\n *\n * @throws ModelFailoverExhaustedError when every model reports an outage.\n */\nexport async function runWithModelFailover<T>(\n input: RunWithModelFailoverInput<T>,\n): Promise<ModelFailoverResult<T>> {\n const models = input.models.map((m) => m.trim()).filter((m) => m.length > 0)\n if (models.length === 0) throw new Error('runWithModelFailover requires at least one model')\n\n const isUnavailableResult = input.isUnavailableResult ?? ((r: T) => isUpstreamUnavailable(r))\n const isUnavailableError = input.isUnavailableError ?? isUpstreamUnavailable\n const attempts: ModelFailoverAttempt[] = []\n\n for (let index = 0; index < models.length; index += 1) {\n const model = models[index]!\n let result: T\n try {\n result = await input.run(model)\n } catch (error) {\n if (!isUnavailableError(error)) throw error\n const attempt: ModelFailoverAttempt = { model, ok: false, reason: describe(error) }\n attempts.push(attempt)\n const next = models[index + 1]\n if (next) input.onFallback?.(attempt, next)\n continue\n }\n\n if (isUnavailableResult(result)) {\n const attempt: ModelFailoverAttempt = { model, ok: false, reason: describe(result) }\n attempts.push(attempt)\n const next = models[index + 1]\n if (next) input.onFallback?.(attempt, next)\n continue\n }\n\n attempts.push({ model, ok: true })\n return { value: result, model, attempts, usedFallback: index > 0 }\n }\n\n throw new ModelFailoverExhaustedError(attempts)\n}\n\n/**\n * Build a failover chain: the preferred model first, then `fallbacks`, with\n * duplicates removed so a model is never retried twice in one turn.\n */\nexport function buildModelChain(preferred: string, fallbacks: readonly string[]): string[] {\n const chain: string[] = []\n for (const model of [preferred, ...fallbacks]) {\n const cleaned = typeof model === 'string' ? model.trim() : ''\n if (cleaned.length > 0 && !chain.includes(cleaned)) chain.push(cleaned)\n }\n return chain\n}\n"],"mappings":"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","names":[]}