@retinue/agentkit 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/audio/openai.d.ts +36 -0
  3. package/dist/adapters/audio/openai.js +136 -0
  4. package/dist/adapters/bullmq/lock.d.ts +26 -2
  5. package/dist/adapters/memory/connections.d.ts +10 -0
  6. package/dist/adapters/memory/connections.js +87 -0
  7. package/dist/adapters/memory/graph.d.ts +17 -0
  8. package/dist/adapters/memory/graph.js +267 -0
  9. package/dist/adapters/memory/index.d.ts +3 -0
  10. package/dist/adapters/memory/index.js +3 -0
  11. package/dist/adapters/memory/rate-limit.d.ts +14 -0
  12. package/dist/adapters/memory/rate-limit.js +28 -0
  13. package/dist/adapters/memory/runtime.js +0 -0
  14. package/dist/adapters/postgres/connections.d.ts +15 -0
  15. package/dist/adapters/postgres/connections.js +134 -0
  16. package/dist/adapters/postgres/graph.d.ts +24 -0
  17. package/dist/adapters/postgres/graph.js +325 -0
  18. package/dist/adapters/postgres/index.d.ts +2 -0
  19. package/dist/adapters/postgres/index.js +2 -0
  20. package/dist/adapters/postgres/migrations.js +198 -0
  21. package/dist/adapters/postgres/run-store.js +26 -2
  22. package/dist/adapters/postgres/schema.d.ts +61 -2
  23. package/dist/adapters/postgres/schema.js +95 -10
  24. package/dist/adapters/redis/index.d.ts +1 -0
  25. package/dist/adapters/redis/index.js +1 -0
  26. package/dist/adapters/redis/rate-limit.d.ts +46 -0
  27. package/dist/adapters/redis/rate-limit.js +55 -0
  28. package/dist/adapters/supabase/index.d.ts +15 -0
  29. package/dist/adapters/supabase/index.js +15 -0
  30. package/dist/adapters/supabase/rls.js +33 -0
  31. package/dist/agents/agent.d.ts +78 -1
  32. package/dist/agents/agent.js +118 -5
  33. package/dist/agents/define.js +24 -4
  34. package/dist/agents/engine.d.ts +20 -0
  35. package/dist/agents/engine.js +177 -6
  36. package/dist/audio/index.d.ts +151 -0
  37. package/dist/audio/index.js +183 -0
  38. package/dist/connections/cipher.d.ts +103 -0
  39. package/dist/connections/cipher.js +141 -0
  40. package/dist/connections/index.d.ts +156 -0
  41. package/dist/connections/index.js +72 -0
  42. package/dist/connections/oauth/client.d.ts +84 -0
  43. package/dist/connections/oauth/client.js +141 -0
  44. package/dist/connections/oauth/index.d.ts +166 -0
  45. package/dist/connections/oauth/index.js +226 -0
  46. package/dist/connections/oauth/service.d.ts +90 -0
  47. package/dist/connections/oauth/service.js +132 -0
  48. package/dist/connections/pause.d.ts +68 -0
  49. package/dist/connections/pause.js +97 -0
  50. package/dist/connections/resolver.d.ts +59 -0
  51. package/dist/connections/resolver.js +138 -0
  52. package/dist/connections/resume.d.ts +64 -0
  53. package/dist/connections/resume.js +99 -0
  54. package/dist/connections/vault-cipher.d.ts +95 -0
  55. package/dist/connections/vault-cipher.js +206 -0
  56. package/dist/core/content-parts.d.ts +18 -2
  57. package/dist/core/content-parts.js +1 -0
  58. package/dist/core/context.d.ts +25 -0
  59. package/dist/core/errors.d.ts +1 -1
  60. package/dist/core/errors.js +15 -0
  61. package/dist/core/events.d.ts +44 -2
  62. package/dist/core/events.js +31 -2
  63. package/dist/core/validation.js +19 -0
  64. package/dist/entries/connections.d.ts +10 -0
  65. package/dist/entries/connections.js +10 -0
  66. package/dist/entries/knowledge.d.ts +8 -0
  67. package/dist/entries/knowledge.js +8 -0
  68. package/dist/entries/mcp-server.d.ts +9 -0
  69. package/dist/entries/mcp-server.js +9 -0
  70. package/dist/entries/runtime.d.ts +10 -0
  71. package/dist/entries/runtime.js +8 -0
  72. package/dist/entries/testing.d.ts +28 -0
  73. package/dist/entries/testing.js +28 -0
  74. package/dist/graphql/resolvers.d.ts +3 -1
  75. package/dist/graphql/resolvers.js +14 -0
  76. package/dist/knowledge/communities.d.ts +166 -0
  77. package/dist/knowledge/communities.js +377 -0
  78. package/dist/knowledge/graph-global.d.ts +153 -0
  79. package/dist/knowledge/graph-global.js +246 -0
  80. package/dist/knowledge/graph-retrieval.d.ts +150 -0
  81. package/dist/knowledge/graph-retrieval.js +303 -0
  82. package/dist/knowledge/graph.d.ts +223 -0
  83. package/dist/knowledge/graph.js +369 -0
  84. package/dist/knowledge/index.d.ts +24 -0
  85. package/dist/knowledge/index.js +36 -1
  86. package/dist/knowledge/retrieval.d.ts +26 -1
  87. package/dist/knowledge/retrieval.js +77 -1
  88. package/dist/mcp-server/index.d.ts +168 -0
  89. package/dist/mcp-server/index.js +175 -0
  90. package/dist/models/extraction.d.ts +73 -0
  91. package/dist/models/extraction.js +118 -0
  92. package/dist/models/index.d.ts +40 -1
  93. package/dist/models/index.js +22 -1
  94. package/dist/models/pricing.d.ts +2 -0
  95. package/dist/models/pricing.js +17 -1
  96. package/dist/models/provider-factory.js +9 -7
  97. package/dist/models/streaming.d.ts +89 -0
  98. package/dist/models/streaming.js +179 -2
  99. package/dist/persistence/index.d.ts +230 -0
  100. package/dist/persistence/index.js +2 -0
  101. package/dist/runtime/index.d.ts +1 -1
  102. package/dist/runtime/index.js +12 -0
  103. package/dist/runtime/worker.d.ts +36 -1
  104. package/dist/runtime/worker.js +37 -4
  105. package/dist/security/checklist.js +46 -0
  106. package/dist/server/bin.d.ts +19 -0
  107. package/dist/server/bin.js +226 -0
  108. package/dist/server/boot.d.ts +20 -2
  109. package/dist/server/boot.js +29 -3
  110. package/dist/server/cli.d.ts +13 -0
  111. package/dist/server/cli.js +7 -4
  112. package/dist/server/doctor.d.ts +74 -0
  113. package/dist/server/doctor.js +280 -0
  114. package/dist/skills/context.d.ts +61 -0
  115. package/dist/skills/context.js +107 -0
  116. package/dist/skills/index.d.ts +1 -0
  117. package/dist/skills/index.js +1 -0
  118. package/dist/telemetry/spans.js +5 -0
  119. package/dist/testing/conformance/artifact-exports.d.ts +20 -0
  120. package/dist/testing/conformance/artifact-exports.js +172 -0
  121. package/dist/testing/conformance/artifacts.d.ts +11 -0
  122. package/dist/testing/conformance/artifacts.js +316 -0
  123. package/dist/testing/conformance/audio.d.ts +38 -0
  124. package/dist/testing/conformance/audio.js +145 -0
  125. package/dist/testing/conformance/capability.d.ts +23 -0
  126. package/dist/testing/conformance/capability.js +27 -0
  127. package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
  128. package/dist/testing/conformance/checkpoint-store.js +72 -0
  129. package/dist/testing/conformance/connections.d.ts +14 -0
  130. package/dist/testing/conformance/connections.js +171 -0
  131. package/dist/testing/conformance/conversation-store.d.ts +10 -0
  132. package/dist/testing/conformance/conversation-store.js +60 -0
  133. package/dist/testing/conformance/evaluation.d.ts +10 -0
  134. package/dist/testing/conformance/evaluation.js +295 -0
  135. package/dist/testing/conformance/files.d.ts +19 -0
  136. package/dist/testing/conformance/files.js +454 -0
  137. package/dist/testing/conformance/flows.d.ts +16 -0
  138. package/dist/testing/conformance/flows.js +193 -0
  139. package/dist/testing/conformance/graph.d.ts +22 -0
  140. package/dist/testing/conformance/graph.js +500 -0
  141. package/dist/testing/conformance/hitl.d.ts +25 -0
  142. package/dist/testing/conformance/hitl.js +523 -0
  143. package/dist/testing/conformance/index.d.ts +163 -0
  144. package/dist/testing/conformance/index.js +321 -0
  145. package/dist/testing/conformance/invariants.d.ts +23 -0
  146. package/dist/testing/conformance/invariants.js +80 -0
  147. package/dist/testing/conformance/knowledge.d.ts +41 -0
  148. package/dist/testing/conformance/knowledge.js +592 -0
  149. package/dist/testing/conformance/parents.d.ts +50 -0
  150. package/dist/testing/conformance/parents.js +39 -0
  151. package/dist/testing/conformance/rate-limit.d.ts +25 -0
  152. package/dist/testing/conformance/rate-limit.js +71 -0
  153. package/dist/testing/conformance/records.d.ts +36 -0
  154. package/dist/testing/conformance/records.js +400 -0
  155. package/dist/testing/conformance/rollups.d.ts +30 -0
  156. package/dist/testing/conformance/rollups.js +425 -0
  157. package/dist/testing/conformance/run-coordinator.d.ts +27 -0
  158. package/dist/testing/conformance/run-coordinator.js +150 -0
  159. package/dist/testing/conformance/run-event-log.d.ts +9 -0
  160. package/dist/testing/conformance/run-event-log.js +138 -0
  161. package/dist/testing/conformance/run-store.d.ts +12 -0
  162. package/dist/testing/conformance/run-store.js +294 -0
  163. package/dist/testing/conformance/session-state.d.ts +38 -0
  164. package/dist/testing/conformance/session-state.js +198 -0
  165. package/dist/testing/conformance/usage-limits.d.ts +10 -0
  166. package/dist/testing/conformance/usage-limits.js +309 -0
  167. package/dist/testing/conformance.d.ts +8 -0
  168. package/dist/testing/conformance.js +8 -0
  169. package/dist/testing/memory-backend.d.ts +39 -0
  170. package/dist/testing/memory-backend.js +41 -0
  171. package/dist/testing/pglite.d.ts +55 -0
  172. package/dist/testing/pglite.js +78 -0
  173. package/dist/testing/stub-model.d.ts +58 -0
  174. package/dist/testing/stub-model.js +71 -0
  175. package/dist/testing/supabase-storage-double.d.ts +35 -0
  176. package/dist/testing/supabase-storage-double.js +101 -0
  177. package/dist/toolkit/http.d.ts +27 -0
  178. package/dist/toolkit/http.js +48 -0
  179. package/dist/toolkit/index.d.ts +27 -1
  180. package/dist/toolkit/index.js +24 -1
  181. package/dist/toolkit/mime.d.ts +97 -0
  182. package/dist/toolkit/mime.js +233 -0
  183. package/dist/toolkit/ssrf.d.ts +139 -0
  184. package/dist/toolkit/ssrf.js +349 -0
  185. package/dist/toolkit/vendor.d.ts +91 -0
  186. package/dist/toolkit/vendor.js +153 -0
  187. package/dist/tools/credentials.d.ts +265 -6
  188. package/dist/tools/credentials.js +318 -3
  189. package/dist/tools/define.d.ts +14 -0
  190. package/dist/tools/define.js +3 -0
  191. package/dist/tools/index.d.ts +21 -0
  192. package/dist/tools/index.js +7 -0
  193. package/dist/tools/library/audio.d.ts +63 -0
  194. package/dist/tools/library/audio.js +148 -0
  195. package/dist/tools/library/index.d.ts +8 -0
  196. package/dist/tools/library/index.js +7 -0
  197. package/dist/tools/registry.d.ts +33 -0
  198. package/dist/tools/registry.js +72 -2
  199. package/dist/usage/index.d.ts +21 -0
  200. package/dist/usage/index.js +1 -0
  201. package/dist/usage/rate-limit.d.ts +122 -0
  202. package/dist/usage/rate-limit.js +131 -0
  203. package/dist/usage/recorder.js +3 -0
  204. package/package.json +29 -2
@@ -13,11 +13,27 @@
13
13
  * Cached input is billed at the cache-read rate when present. This is what feeds usage accounting.
14
14
  */
15
15
  export const computeModelCostMinorUnits = (pricing, usage) => {
16
+ /**
17
+ * Three kinds of input token, and all three are inside `inputTokens` — task #247.
18
+ *
19
+ * Measured against a live provider: `noCacheTokens + cacheReadTokens + cacheWriteTokens === inputTokens`. So
20
+ * fresh input is the remainder after both cache quantities, and adding them on top would double-bill — the
21
+ * same trap `nonTextInput` exists for.
22
+ *
23
+ * A cache **write** is not a discount. Anthropic charges 1.25× a fresh input token to write an entry, so
24
+ * folding writes into fresh input *under-bills* the first turn of every conversation — the direction that
25
+ * looks like a saving and is not. `cacheWritePerMillion` existed in `ModelPricing` and was read by nothing
26
+ * until now.
27
+ */
16
28
  const cachedIn = usage.cachedInputTokens ?? 0;
17
- const freshIn = Math.max(0, usage.inputTokens - cachedIn);
29
+ const cacheWrite = usage.cacheWriteTokens ?? 0;
30
+ const freshIn = Math.max(0, usage.inputTokens - cachedIn - cacheWrite);
18
31
  const perMillion = (tokens, price) => (tokens * price) / 1_000_000;
19
32
  const tokenCost = perMillion(freshIn, pricing.inputPerMillion) +
20
33
  perMillion(cachedIn, pricing.cacheReadPerMillion ?? pricing.inputPerMillion) +
34
+ // Defaulting a write to the plain input rate is the conservative choice: it neither invents a premium a
35
+ // provider does not charge nor silently discounts one it does.
36
+ perMillion(cacheWrite, pricing.cacheWritePerMillion ?? pricing.inputPerMillion) +
21
37
  perMillion(usage.outputTokens, pricing.outputPerMillion);
22
38
  /**
23
39
  * Non-text input, charged **only** where the provider prices it separately — #185 AC-4.
@@ -45,14 +45,16 @@ export const createProviderFactory = (config = {}) => {
45
45
  });
46
46
  }
47
47
  return createOpenAICompatible({ name: c.name ?? "openai-compatible", baseURL: c.baseURL, apiKey: c.apiKey })(model.modelId);
48
- case "bedrock":
49
- // Bedrock wiring (@ai-sdk/amazon-bedrock + AWS credentials) is a follow-up.
50
- throw new AgentPlatformError({
51
- code: "capability_unavailable",
52
- message: "Amazon Bedrock provider is not wired yet",
53
- retryable: false,
54
- });
55
48
  default: {
49
+ /**
50
+ * Unreachable while `MODEL_PROVIDERS` and this switch agree — which is the point, and is now the
51
+ * *only* thing standing between a declared provider and a runtime failure.
52
+ *
53
+ * `"bedrock"` used to have a case here that threw. It was removed from the union in #256 rather than
54
+ * wired, because a declared provider that throws is worse than an absent one: it typechecks,
55
+ * satisfies this `never` assertion, and fails for whoever selects it first. `provider-coverage.test.ts`
56
+ * is what turns "mentioned in a switch" into "actually constructs".
57
+ */
56
58
  const exhaustive = model.provider;
57
59
  throw new AgentPlatformError({
58
60
  code: "capability_unavailable",
@@ -89,6 +89,14 @@ export type ModelTurnTool = {
89
89
  /** Zod schema or JSON-schema object; a permissive object schema is used when absent. */
90
90
  readonly inputSchema?: unknown;
91
91
  execute(input: unknown, options?: ModelToolCallOptions): Promise<unknown>;
92
+ /**
93
+ * The tool's category, when the caller knows it — task #244.
94
+ *
95
+ * Optional and never sent to the provider. It exists so a caller can express a policy *about* categories —
96
+ * `AgentManifest.toolPolicy.categories` names the ones that must stay resident when the catalogue is bounded —
97
+ * without the engine having to reach back into the registry for a descriptor it was already handed.
98
+ */
99
+ readonly category?: string;
92
100
  };
93
101
  export type ModelTurnRequest = {
94
102
  readonly model: ResolvedModel;
@@ -116,6 +124,29 @@ export type ModelTurnRequest = {
116
124
  * `temperature` matters for a second reason: the evaluation harness (#141) rests its reproducibility argument
117
125
  * partly on temperature zero, and there was no way to ask for it on a real run.
118
126
  */
127
+ /**
128
+ * Ask the model for a value conforming to a schema, rather than prose — task #243.
129
+ *
130
+ * Neutral on purpose: this layer sits below `agents/`, so it takes a schema rather than an
131
+ * `AgentManifest["responseFormat"]`. The engine does the mapping.
132
+ *
133
+ * The schema must be validatable by this process — see `structuredValidator`. A bare JSON schema is refused,
134
+ * because the AI SDK's `jsonSchema()` wrapper leaves `validate` undefined: it constrains the provider request
135
+ * and checks nothing on the way back, which would make "structured" a request rather than a guarantee. That
136
+ * distinction is the entire point of the task.
137
+ */
138
+ readonly structuredOutput?: {
139
+ readonly schema: unknown;
140
+ };
141
+ /**
142
+ * Where this turn's prompt prefix may be cached, and how — task #247.
143
+ *
144
+ * `"explicit"` makes this layer emit the provider's cache directive; `"automatic"` and `"none"` emit nothing,
145
+ * for opposite reasons — one needs no help and the other would reject the field. Mapped by the engine from
146
+ * `ModelDefinition.capabilities.promptCaching`, so a caller that supplies no definition sends nothing, which
147
+ * is the behaviour every existing host already has.
148
+ */
149
+ readonly promptCaching?: "automatic" | "explicit" | "none";
119
150
  readonly maxOutputTokens?: number;
120
151
  readonly temperature?: number;
121
152
  readonly topP?: number;
@@ -124,7 +155,26 @@ export type ModelTurnRequest = {
124
155
  export type NeutralUsage = {
125
156
  readonly inputTokens: number;
126
157
  readonly outputTokens: number;
158
+ /**
159
+ * Input tokens served from a prompt cache — a **subset** of `inputTokens`, not an addition to it.
160
+ *
161
+ * Read from the provider's `inputTokenDetails.cacheReadTokens` (task #247). It used to be read from
162
+ * `totalUsage.cachedInputTokens`, **a field the AI SDK does not send** — so this was zero on every turn, and
163
+ * `computeModelCostMinorUnits` billed cached tokens at the full input rate. Measured against a live model, a
164
+ * turn reusing a 9,700-token prefix reported 9,472 cache-read tokens and this platform recorded none of them.
165
+ */
127
166
  readonly cachedInputTokens: number;
167
+ /**
168
+ * Input tokens written *into* a prompt cache — also a subset of `inputTokens`.
169
+ *
170
+ * Its own quantity because it is priced differently and, on some providers, priced **higher** than a fresh
171
+ * input token: Anthropic charges 1.25× to write a cache entry. Folding it into fresh input under-bills a
172
+ * cache write and over-credits the first turn of every conversation, which is the direction that looks like a
173
+ * saving and is not.
174
+ *
175
+ * Absent means "not reported", not "none" — the rule `imageCount` already follows.
176
+ */
177
+ readonly cacheWriteTokens?: number;
128
178
  readonly reasoningTokens?: number;
129
179
  /**
130
180
  * Non-text input, counted from what **we sent** rather than from what the provider reported — #185 AC-4.
@@ -158,9 +208,48 @@ export type NeutralStreamChunk = {
158
208
  } | {
159
209
  readonly type: "finish";
160
210
  readonly usage: NeutralUsage;
211
+ }
212
+ /** A validated structured answer — emitted once, at the end, only when `structuredOutput` was asked for. */
213
+ | {
214
+ readonly type: "structured-output";
215
+ readonly value: unknown;
161
216
  } | {
162
217
  readonly type: "error";
163
218
  readonly error: unknown;
164
219
  };
220
+ /**
221
+ * A schema this process can actually check, or a refusal — task #243.
222
+ *
223
+ * The rule: **only a schema with a validator is accepted for structured output.** Zod (a direct dependency) and
224
+ * anything else implementing Standard Schema qualify. A bare JSON-schema object does not, and refusing it is a
225
+ * deliberate choice rather than an omission:
226
+ *
227
+ * - The AI SDK's `jsonSchema()` wrapper returns `{ _type, jsonSchema, validate }` with **`validate` undefined**.
228
+ * It constrains the provider's generation and validates nothing coming back. Accepting one would mean the
229
+ * platform says "structured", the provider mostly complies, and nobody checks — which is a softer version of
230
+ * the bug being fixed, not a fix.
231
+ * - Validating JSON schema properly needs `ajv`, and that is a new runtime dependency for every consumer of a
232
+ * package whose entire dependency list is `ai` and `zod`. Not worth it when `z.object({...})` is one line and
233
+ * already validates.
234
+ *
235
+ * So this fails closed, at wiring time, with a message naming the fix. Tools keep taking JSON schema — a tool's
236
+ * arguments are validated by the provider and a bad call is a tool error the model can see and retry, which is
237
+ * a different situation from a guarantee made to a caller about a return value.
238
+ */
239
+ export declare const structuredValidator: (schema: unknown) => ((value: unknown) => {
240
+ readonly ok: true;
241
+ } | {
242
+ readonly ok: false;
243
+ readonly detail: string;
244
+ });
245
+ /**
246
+ * The cache-read count, from wherever the provider put it — task #247.
247
+ *
248
+ * Exported so the arithmetic is testable without a provider: the whole defect this fixes was a field name that
249
+ * did not exist, which no amount of testing *through* a fake could reveal.
250
+ */
251
+ export declare const cacheRead: (usage: Record<string, unknown>) => number;
252
+ /** The cache-write count, or `undefined` when the provider did not report one. */
253
+ export declare const cacheWrite: (usage: Record<string, unknown>) => number | undefined;
165
254
  export declare function streamModelTurn(req: ModelTurnRequest): AsyncIterable<NeutralStreamChunk>;
166
255
  //# sourceMappingURL=streaming.d.ts.map
@@ -6,7 +6,7 @@
6
6
  * continues — and yields provider-neutral chunks. The agent engine consumes these chunks without
7
7
  * ever importing the SDK, so switching providers changes nothing above this layer.
8
8
  */
9
- import { jsonSchema, stepCountIs, streamText, tool as aiTool } from "ai";
9
+ import { Output, jsonSchema, stepCountIs, streamText, tool as aiTool } from "ai";
10
10
  import { AgentPlatformError } from "../core/errors.js";
11
11
  /** The text of a turn, for callers that count tokens or log. Non-text parts contribute nothing. */
12
12
  export const turnText = (message) => typeof message.content === "string"
@@ -58,6 +58,62 @@ export const modalitiesOf = (messages) => {
58
58
  }
59
59
  return [...found];
60
60
  };
61
+ /**
62
+ * A schema this process can actually check, or a refusal — task #243.
63
+ *
64
+ * The rule: **only a schema with a validator is accepted for structured output.** Zod (a direct dependency) and
65
+ * anything else implementing Standard Schema qualify. A bare JSON-schema object does not, and refusing it is a
66
+ * deliberate choice rather than an omission:
67
+ *
68
+ * - The AI SDK's `jsonSchema()` wrapper returns `{ _type, jsonSchema, validate }` with **`validate` undefined**.
69
+ * It constrains the provider's generation and validates nothing coming back. Accepting one would mean the
70
+ * platform says "structured", the provider mostly complies, and nobody checks — which is a softer version of
71
+ * the bug being fixed, not a fix.
72
+ * - Validating JSON schema properly needs `ajv`, and that is a new runtime dependency for every consumer of a
73
+ * package whose entire dependency list is `ai` and `zod`. Not worth it when `z.object({...})` is one line and
74
+ * already validates.
75
+ *
76
+ * So this fails closed, at wiring time, with a message naming the fix. Tools keep taking JSON schema — a tool's
77
+ * arguments are validated by the provider and a bad call is a tool error the model can see and retry, which is
78
+ * a different situation from a guarantee made to a caller about a return value.
79
+ */
80
+ export const structuredValidator = (schema) => {
81
+ const standard = schema?.["~standard"];
82
+ if (schema !== null && typeof schema === "object" && typeof standard?.validate === "function") {
83
+ return (value) => {
84
+ const result = standard.validate(value);
85
+ // Standard Schema is allowed to return a promise; a validator that cannot answer synchronously is not
86
+ // usable here, and silently treating a pending promise as success is how this would pass having checked
87
+ // nothing. Refuse instead.
88
+ if (result instanceof Promise)
89
+ return { ok: false, detail: "the schema validates asynchronously, which this path cannot await" };
90
+ if (result.issues === undefined || result.issues.length === 0)
91
+ return { ok: true };
92
+ return {
93
+ ok: false,
94
+ detail: result.issues
95
+ .map((i) => `${(i.path ?? []).join(".") || "(root)"}: ${i.message ?? "invalid"}`)
96
+ .join("; "),
97
+ };
98
+ };
99
+ }
100
+ if (isZodSchema(schema)) {
101
+ const parse = schema
102
+ .safeParse;
103
+ return (value) => {
104
+ const result = parse.call(schema, value);
105
+ return result.success ? { ok: true } : { ok: false, detail: result.error?.message ?? "invalid" };
106
+ };
107
+ }
108
+ throw new AgentPlatformError({
109
+ code: "capability_unavailable",
110
+ message: "a structured response format needs a schema this process can validate — a Zod schema, or anything " +
111
+ "implementing Standard Schema. A plain JSON-schema object is refused: the AI SDK sends it to the " +
112
+ "provider but validates nothing on the way back, so the platform would be promising a shape it never " +
113
+ "checks. Use `z.object({ … })`.",
114
+ retryable: false,
115
+ });
116
+ };
61
117
  const isZodSchema = (s) => typeof s === "object" && s !== null && typeof s.safeParse === "function";
62
118
  /** A JSON-schema-shaped object: `{ type: "object", … }`. Enough to tell one from a Zod schema or from absence. */
63
119
  const isJsonSchema = (s) => typeof s === "object" && s !== null && typeof s.type === "string";
@@ -148,6 +204,25 @@ const toModelContent = (content) => {
148
204
  ...(part.filename === undefined ? {} : { filename: part.filename }),
149
205
  });
150
206
  };
207
+ /**
208
+ * The cache-read count, from wherever the provider put it — task #247.
209
+ *
210
+ * Exported so the arithmetic is testable without a provider: the whole defect this fixes was a field name that
211
+ * did not exist, which no amount of testing *through* a fake could reveal.
212
+ */
213
+ export const cacheRead = (usage) => {
214
+ const details = usage.inputTokenDetails;
215
+ if (details?.cacheReadTokens !== undefined)
216
+ return num(details.cacheReadTokens);
217
+ return num(usage.cachedInputTokens);
218
+ };
219
+ /** The cache-write count, or `undefined` when the provider did not report one. */
220
+ export const cacheWrite = (usage) => {
221
+ const details = usage.inputTokenDetails;
222
+ if (details?.cacheWriteTokens !== undefined)
223
+ return num(details.cacheWriteTokens);
224
+ return usage.cacheWriteTokens === undefined ? undefined : num(usage.cacheWriteTokens);
225
+ };
151
226
  export async function* streamModelTurn(req) {
152
227
  /**
153
228
  * Refuse a modality the model cannot take — #185.
@@ -173,11 +248,52 @@ export async function* streamModelTurn(req) {
173
248
  });
174
249
  }
175
250
  const messages = req.messages.map((m) => ({ role: m.role, content: toModelContent(m.content) }));
251
+ /**
252
+ * Validated before the call, not after — task #243 AC-2/AC-3.
253
+ *
254
+ * A schema this process cannot check makes "structured" a request rather than a guarantee, and finding that
255
+ * out after a paid generation is finding it out in the worst place. `structuredValidator` throws here.
256
+ */
257
+ const validate = req.structuredOutput === undefined ? undefined : structuredValidator(req.structuredOutput.schema);
176
258
  const result = streamText({
177
259
  model: req.model,
178
260
  ...(req.system ? { system: req.system } : {}),
179
261
  messages,
180
262
  ...(req.tools && req.tools.length > 0 ? { tools: toToolSet(req.tools) } : {}),
263
+ /**
264
+ * `output` rather than `streamObject`, because tools must keep working — AC-4.
265
+ *
266
+ * `streamObject` has no tool loop at all, so a structured agent would silently lose every tool. This keeps
267
+ * `streamText`'s model↔tool loop and constrains only the final answer.
268
+ *
269
+ * The option is `output`, **not** `experimental_output`. It was named the latter in `ai@4` and the name was
270
+ * dropped in `ai@7`; passing the old one is not an error, it is *ignored* — `streamText` accepts the unknown
271
+ * key, the model is never constrained, and `result.output` comes back as ordinary prose. Written down
272
+ * because that is the whole defect class this task exists to close, met again inside the fix: the first
273
+ * version of this code passed `experimental_output`, typechecked, and did nothing. Only the live check
274
+ * against a real model found it.
275
+ */
276
+ ...(req.structuredOutput === undefined
277
+ ? {}
278
+ : { output: Output.object({ schema: req.structuredOutput.schema }) }),
279
+ /**
280
+ * The cache breakpoint, for providers that need one told — task #247.
281
+ *
282
+ * Only `"explicit"`. Anthropic caches nothing unless a block carries `cache_control`, so a platform that
283
+ * emitted nothing got no caching there at all — which is what this did. OpenAI needs no directive and would
284
+ * treat one as an unknown field, so `"automatic"` deliberately sends nothing.
285
+ *
286
+ * The breakpoint goes on the **system** block, which is where the stable prefix is: the system prompt and
287
+ * the tool catalogue are byte-identical across every turn of a conversation, and the history after them is
288
+ * not. Anthropic caches everything *up to* a breakpoint, so marking the system block caches the prompt and
289
+ * the tool definitions together.
290
+ *
291
+ * `providerOptions` rather than a top-level field, because this is provider-specific by construction and the
292
+ * AI SDK's neutral surface has no cache concept. A provider that ignores the namespace is unaffected.
293
+ */
294
+ ...(req.promptCaching === "explicit"
295
+ ? { providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } } }
296
+ : {}),
181
297
  stopWhen: stepCountIs(req.maxSteps ?? 8),
182
298
  ...(req.abortSignal ? { abortSignal: req.abortSignal } : {}),
183
299
  // Spread conditionally so an unset parameter leaves the provider's own default alone, rather than pinning it
@@ -190,6 +306,15 @@ export async function* streamModelTurn(req) {
190
306
  for await (const chunk of result.fullStream) {
191
307
  switch (chunk.type) {
192
308
  case "text-delta":
309
+ /**
310
+ * Swallowed on a structured turn — AC-5.
311
+ *
312
+ * With `experimental_output` the model's text *is* the JSON, arriving a fragment at a time. Forwarding it
313
+ * would put half-built JSON in the transcript as prose and leave a reader watching `{"na` appear. The
314
+ * decision is one complete part at the end instead; tool calls still stream, so the turn is not silent.
315
+ */
316
+ if (req.structuredOutput !== undefined)
317
+ break;
193
318
  yield { type: "text-delta", id: chunk.id ?? "text", text: chunk.text ?? "" };
194
319
  break;
195
320
  case "tool-call":
@@ -249,7 +374,20 @@ export async function* streamModelTurn(req) {
249
374
  usage: {
250
375
  inputTokens: num(usage.inputTokens),
251
376
  outputTokens: num(usage.outputTokens),
252
- cachedInputTokens: num(usage.cachedInputTokens),
377
+ /**
378
+ * From `inputTokenDetails`, with the old field as a fallback — task #247.
379
+ *
380
+ * The AI SDK reports the breakdown as
381
+ * `inputTokenDetails: { noCacheTokens, cacheReadTokens, cacheWriteTokens }`, and **not** as
382
+ * `cachedInputTokens`. Reading the latter — which is what this did — yielded `undefined` on every
383
+ * provider, so `num()` made it zero and every cached token was billed at the full input rate.
384
+ *
385
+ * The fallback is kept because the field is what a *host-supplied* `streamTurn` would most naturally
386
+ * set, and because a future SDK may reinstate it. Order matters: the detailed breakdown wins, since
387
+ * it is the one a real provider fills in.
388
+ */
389
+ cachedInputTokens: cacheRead(usage),
390
+ ...(cacheWrite(usage) === undefined ? {} : { cacheWriteTokens: cacheWrite(usage) }),
253
391
  ...(usage.reasoningTokens !== undefined ? { reasoningTokens: num(usage.reasoningTokens) } : {}),
254
392
  /**
255
393
  * Non-text input, counted from the request rather than read from the response — #185 AC-4.
@@ -272,5 +410,44 @@ export async function* streamModelTurn(req) {
272
410
  break;
273
411
  }
274
412
  }
413
+ if (validate === undefined)
414
+ return;
415
+ /**
416
+ * The structured answer, after the loop and after validation — AC-2.
417
+ *
418
+ * Read from the SDK's resolved output rather than reassembled from the text deltas that were swallowed above:
419
+ * the SDK has already parsed the JSON, and re-parsing a string this layer discarded would be two chances to
420
+ * get it wrong.
421
+ *
422
+ * Every failure here is a **run failure**, deliberately. The alternative is emitting the text as an ordinary
423
+ * answer, which is precisely the defect this task fixes: an agent that asked for a schema, got prose, and had
424
+ * no way to tell. A caller who wanted best-effort prose did not set a structured response format.
425
+ */
426
+ let value;
427
+ try {
428
+ value = await result.output;
429
+ }
430
+ catch (thrown) {
431
+ throw new AgentPlatformError({
432
+ code: "provider_error",
433
+ message: "the model produced no value conforming to the structured response format: " +
434
+ errorMessageOf(thrown) +
435
+ ". The turn is failed rather than returning the raw text, which would be prose presented as a " +
436
+ "validated object.",
437
+ retryable: true,
438
+ });
439
+ }
440
+ // Belt and braces over the SDK's own parse. `Output.object` validates a Standard Schema, but this layer is
441
+ // where the guarantee is made, and a guarantee that depends on a dependency's internals is a guarantee that
442
+ // changes when the dependency does.
443
+ const verdict = validate(value);
444
+ if (!verdict.ok)
445
+ throw new AgentPlatformError({
446
+ code: "provider_error",
447
+ message: `the model's answer does not satisfy the structured response format — ${verdict.detail}. ` +
448
+ "The turn is failed rather than returning it unchecked.",
449
+ retryable: true,
450
+ });
451
+ yield { type: "structured-output", value };
275
452
  }
276
453
  //# sourceMappingURL=streaming.js.map
@@ -237,7 +237,36 @@ export interface RunStore {
237
237
  workerId: string;
238
238
  leaseMs: number;
239
239
  now: string;
240
+ /**
241
+ * Refuse the claim when the tenant already holds this many **live leases** — REQ-058 (#246), task #265.
242
+ *
243
+ * Enforced *here*, inside the claim, and that placement is the whole design. Counting live leases and
244
+ * then claiming in a second statement is check-then-act: two workers in two processes both read
245
+ * "3 of 4 used" and both claim, and the tenant runs at 5. An adapter must make this one atomic
246
+ * operation — one SQL statement, or one turn of the event loop for the reference adapter.
247
+ *
248
+ * **Live leases, not a counter.** A counter incremented at admission and decremented at completion leaks
249
+ * a unit every time a worker dies mid-run, and a leaked unit is invisible until the tenant's effective
250
+ * concurrency has silently reached zero. A lease expires on its own, so a crashed worker's slot returns
251
+ * without anybody noticing it was gone.
252
+ *
253
+ * The run being claimed is never counted against itself, so recovering an expired lease is not blocked
254
+ * by the limit it is trying to satisfy.
255
+ *
256
+ * Absent or non-positive means unlimited.
257
+ */
258
+ maxConcurrent?: number;
240
259
  }): Promise<Run | null>;
260
+ /**
261
+ * How many of a tenant's runs hold a live lease right now.
262
+ *
263
+ * **Not the enforcement path** — `claim` is, because only `claim` can be atomic with the decision. This
264
+ * exists for two honest uses: telling an operator why a tenant's work is waiting, and letting the worker
265
+ * label a refused claim. A slightly stale answer costs a wrong log line, never a wrong admission.
266
+ */
267
+ countLive(input: TenantScope & {
268
+ now: string;
269
+ }): Promise<number>;
241
270
  /** Extend the lease. Returns false when the claim was lost (reaped/stolen) so the worker aborts. */
242
271
  keepalive(input: TenantScope & {
243
272
  id: RunId;
@@ -1524,6 +1553,207 @@ export interface KeywordIndex {
1524
1553
  sourceTypes?: readonly KnowledgeSourceType[];
1525
1554
  }): Promise<readonly KeywordSearchHit[]>;
1526
1555
  }
1556
+ /**
1557
+ * Entities and relationships extracted from chunks, so retrieval can answer what no chunk says.
1558
+ *
1559
+ * A fourth port beside `KnowledgeStore`, `VectorIndex` and `KeywordIndex`, for the same reason those are three
1560
+ * rather than one: it answers a different question and can be backed by a different system. A deployment on
1561
+ * Postgres satisfies all four with one database; one that later wants a graph database swaps this alone.
1562
+ *
1563
+ * **Provenance is structural, not decorative.** Every entity and every edge records the chunk ids it was
1564
+ * extracted from, and a row without provenance cannot be written. The retriever surfaces graph-derived material
1565
+ * as *citable* hits, so a claim with no traceable chunk is one the model would present as though a document
1566
+ * said it. `SEMANTIC_RELEVANCE_FLOOR` exists so retrieval can say "nothing"; this exists so the graph cannot say
1567
+ * "something" without saying where from.
1568
+ *
1569
+ * **A source's contribution is what gets replaced, not the entity.** An entity like "retry budget" is mentioned
1570
+ * by many sources, so re-indexing one document must not delete it — it must withdraw *that document's* claims
1571
+ * and leave the rest. Hence the split: contributions are per source and per chunk, and an entity exists exactly
1572
+ * as long as some contribution still names it. Deleting a source therefore prunes the entities only that source
1573
+ * knew about, automatically, with no reference counting for anybody to get wrong.
1574
+ */
1575
+ /** An entity's type as the extractor named it — free text, lowercased. Not a union: a corpus decides its own. */
1576
+ export type KnowledgeEntity = {
1577
+ /** Deterministic and readable: `type:normalised-name`. See `entityId` in `knowledge/graph.ts`. */
1578
+ readonly id: string;
1579
+ /** The canonical surface form — the one chosen deterministically from everything merged into it. */
1580
+ readonly name: string;
1581
+ readonly type: string;
1582
+ readonly description?: string;
1583
+ /**
1584
+ * Every distinct spelling merged into this entity, sorted — AC-6.
1585
+ *
1586
+ * Resolution merges things that are not the same, and the mistake is invisible unless the merges are
1587
+ * recorded. Storing the surface forms turns "why is this entity called that" from an archaeology problem
1588
+ * into a lookup.
1589
+ */
1590
+ readonly surfaceForms: readonly string[];
1591
+ /** Chunk ids this entity was extracted from, sorted. Never empty. */
1592
+ readonly provenance: readonly string[];
1593
+ };
1594
+ export type KnowledgeRelationship = {
1595
+ /** Deterministic: `fromId|type|toId`. */
1596
+ readonly id: string;
1597
+ readonly fromId: string;
1598
+ readonly toId: string;
1599
+ readonly type: string;
1600
+ readonly description?: string;
1601
+ /** How many chunks asserted this edge. The obvious traversal ordering, and it needs no calibration. */
1602
+ readonly weight: number;
1603
+ /** Chunk ids this edge was extracted from, sorted. Never empty. */
1604
+ readonly provenance: readonly string[];
1605
+ };
1606
+ /** What one source contributed, as written. The store merges these into entities and edges. */
1607
+ /**
1608
+ * A community as stored: the cluster, and whatever summary has been written for it.
1609
+ *
1610
+ * `summaryFingerprint` is the mechanism behind visible staleness. A summary records the membership it was
1611
+ * written against; when the graph changes and the community's `fingerprint` moves on, the two differ and the
1612
+ * summary is knowably out of date — without a timestamp anybody has to interpret, and without re-reading the
1613
+ * summary to guess.
1614
+ */
1615
+ export type StoredCommunity = {
1616
+ readonly id: string;
1617
+ readonly level: number;
1618
+ readonly entityIds: readonly string[];
1619
+ readonly relationshipIds: readonly string[];
1620
+ readonly chunkIds: readonly string[];
1621
+ readonly fingerprint: string;
1622
+ readonly summary?: string;
1623
+ /** The `fingerprint` the summary was written against. Differs from `fingerprint` exactly when stale. */
1624
+ readonly summaryFingerprint?: string;
1625
+ readonly summarisedAt?: string;
1626
+ };
1627
+ /** True when a community has a summary that no longer describes its membership. */
1628
+ export declare const isCommunityStale: (community: StoredCommunity) => boolean;
1629
+ export type GraphContribution = {
1630
+ readonly entities: readonly KnowledgeEntity[];
1631
+ readonly relationships: readonly KnowledgeRelationship[];
1632
+ };
1633
+ /**
1634
+ * Whether this tenant uses the graph at all, and which of its sources do.
1635
+ *
1636
+ * Two levels, because the cost is per chunk and paid at index time. A tenant switch alone would mean enabling
1637
+ * the feature silently multiplies every tenant's indexing bill — including for the ad-hoc PDF somebody attached
1638
+ * to one conversation. A per-source flag alone would leave no single place to say "this deployment does not use
1639
+ * GraphRAG", which is the guarantee AC-1 rests on.
1640
+ *
1641
+ * A source flag is stored and honoured **independently** of the tenant switch, so a deployment can mark its
1642
+ * handbook today and enable the tenant next week without re-marking anything.
1643
+ */
1644
+ export type GraphSettings = {
1645
+ readonly enabled: boolean;
1646
+ /** When it was last changed, for an operator wondering why a bill moved. */
1647
+ readonly updatedAt: string;
1648
+ };
1649
+ export interface GraphStore {
1650
+ getSettings(input: TenantScope): Promise<GraphSettings>;
1651
+ setEnabled(input: TenantScope & {
1652
+ enabled: boolean;
1653
+ at: string;
1654
+ }): Promise<GraphSettings>;
1655
+ /** Marks one source. Independent of the tenant switch — see `GraphSettings`. */
1656
+ setSourceEnabled(input: TenantScope & {
1657
+ sourceType: KnowledgeSourceType;
1658
+ sourceId: string;
1659
+ enabled: boolean;
1660
+ }): Promise<void>;
1661
+ isSourceEnabled(input: TenantScope & {
1662
+ sourceType: KnowledgeSourceType;
1663
+ sourceId: string;
1664
+ }): Promise<boolean>;
1665
+ listEnabledSources(input: TenantScope & PageRequest): Promise<Page<{
1666
+ readonly sourceType: KnowledgeSourceType;
1667
+ readonly sourceId: string;
1668
+ }>>;
1669
+ /**
1670
+ * Replaces one source's contribution to the graph.
1671
+ *
1672
+ * Replace rather than append, for the reason `replaceSource` is: re-indexing a changed document must not
1673
+ * leave its old claims in the graph. Entities and edges no source names any more are pruned by this call.
1674
+ *
1675
+ * Refuses a row with empty provenance rather than storing an untraceable claim.
1676
+ */
1677
+ replaceSourceGraph(input: TenantScope & {
1678
+ sourceType: KnowledgeSourceType;
1679
+ sourceId: string;
1680
+ contribution: GraphContribution;
1681
+ }): Promise<{
1682
+ readonly entities: number;
1683
+ readonly relationships: number;
1684
+ readonly pruned: number;
1685
+ }>;
1686
+ deleteSourceGraph(input: TenantScope & {
1687
+ sourceType: KnowledgeSourceType;
1688
+ sourceId: string;
1689
+ }): Promise<{
1690
+ readonly pruned: number;
1691
+ }>;
1692
+ getEntity(input: TenantScope & {
1693
+ id: string;
1694
+ }): Promise<KnowledgeEntity | null>;
1695
+ /** Entities by exact id, for a query-side resolver that has already normalised. */
1696
+ getEntities(input: TenantScope & {
1697
+ ids: readonly string[];
1698
+ }): Promise<readonly KnowledgeEntity[]>;
1699
+ /**
1700
+ * Entities whose **normalised name** matches, across every type — REQ-064 (#270), task #273.
1701
+ *
1702
+ * A query says "the retry budget" and the graph holds `concept:retry budget`; the type is not in the
1703
+ * question and cannot be. So resolution is by name, and the caller supplies names already normalised by
1704
+ * `normaliseName`.
1705
+ *
1706
+ * That the id *is* `type:normalisedName` is what makes this a suffix match rather than a second stored
1707
+ * column, and it is why query-side and index-side resolution agree by construction: the id was built by the
1708
+ * same function the caller just called. A separate `normalised_name` column would be a second copy to drift.
1709
+ */
1710
+ resolveEntities(input: TenantScope & {
1711
+ normalisedNames: readonly string[];
1712
+ }): Promise<readonly KnowledgeEntity[]>;
1713
+ listEntities(input: TenantScope & PageRequest & {
1714
+ type?: string;
1715
+ }): Promise<Page<KnowledgeEntity>>;
1716
+ /** Every edge touching any of `entityIds`, in either direction. The traversal primitive. */
1717
+ neighbours(input: TenantScope & {
1718
+ entityIds: readonly string[];
1719
+ limit: number;
1720
+ }): Promise<readonly KnowledgeRelationship[]>;
1721
+ /**
1722
+ * Replaces the whole community hierarchy — REQ-064 (#270), task #272.
1723
+ *
1724
+ * Whole rather than per level, because clustering is global: an entity moving between communities can change
1725
+ * every level above it, and a partial write would leave a hierarchy whose levels disagree about who is in
1726
+ * what. Summaries already written are carried over for communities whose fingerprint is unchanged, which is
1727
+ * what makes re-summarisation incremental — the expensive half — while clustering stays global and cheap.
1728
+ */
1729
+ replaceCommunities(input: TenantScope & {
1730
+ communities: readonly StoredCommunity[];
1731
+ }): Promise<{
1732
+ readonly written: number;
1733
+ readonly summariesKept: number;
1734
+ }>;
1735
+ listCommunities(input: TenantScope & PageRequest & {
1736
+ level?: number;
1737
+ }): Promise<Page<StoredCommunity>>;
1738
+ getCommunity(input: TenantScope & {
1739
+ id: string;
1740
+ }): Promise<StoredCommunity | null>;
1741
+ /** Records a generated summary against the fingerprint it was written for. */
1742
+ setCommunitySummary(input: TenantScope & {
1743
+ id: string;
1744
+ summary: string;
1745
+ fingerprint: string;
1746
+ at: string;
1747
+ }): Promise<void>;
1748
+ /**
1749
+ * A stable serialisation of the whole graph, for the determinism assertion — AC-6.
1750
+ *
1751
+ * On the port rather than built by the test, so *every* adapter is held to it. A test that serialised the
1752
+ * graph itself would prove the reference adapter deterministic and say nothing about Postgres, where row
1753
+ * order is the thing most likely to differ.
1754
+ */
1755
+ fingerprint(input: TenantScope): Promise<string>;
1756
+ }
1527
1757
  /**
1528
1758
  * Content-addressable blob storage for spilled tool output (`docs/03` → Tool results). A large
1529
1759
  * result is offloaded here and referenced by an authorized `BlobRef`, read back via
@@ -214,6 +214,8 @@ export const KEYWORD_STOPWORDS = new Set([
214
214
  export const stripStopwords = (query) => (query.toLowerCase().match(/[a-z0-9]+(?:-[a-z0-9]+)*/g) ?? [])
215
215
  .filter((term) => !KEYWORD_STOPWORDS.has(term))
216
216
  .join(" ");
217
+ /** True when a community has a summary that no longer describes its membership. */
218
+ export const isCommunityStale = (community) => community.summary !== undefined && community.summaryFingerprint !== community.fingerprint;
217
219
  export const ADAPTER_CAPABILITIES = [
218
220
  "transactions",
219
221
  "row-level-security",