@retinue/agentkit 0.1.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 (246) hide show
  1. package/README.md +59 -277
  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/embeddings/openai.d.ts +45 -0
  6. package/dist/adapters/embeddings/openai.js +109 -0
  7. package/dist/adapters/memory/connections.d.ts +10 -0
  8. package/dist/adapters/memory/connections.js +87 -0
  9. package/dist/adapters/memory/graph.d.ts +17 -0
  10. package/dist/adapters/memory/graph.js +267 -0
  11. package/dist/adapters/memory/index.d.ts +3 -0
  12. package/dist/adapters/memory/index.js +3 -0
  13. package/dist/adapters/memory/rate-limit.d.ts +14 -0
  14. package/dist/adapters/memory/rate-limit.js +28 -0
  15. package/dist/adapters/memory/runtime.js +0 -0
  16. package/dist/adapters/postgres/connections.d.ts +15 -0
  17. package/dist/adapters/postgres/connections.js +134 -0
  18. package/dist/adapters/postgres/graph.d.ts +24 -0
  19. package/dist/adapters/postgres/graph.js +325 -0
  20. package/dist/adapters/postgres/index.d.ts +2 -0
  21. package/dist/adapters/postgres/index.js +2 -0
  22. package/dist/adapters/postgres/migrations.js +198 -0
  23. package/dist/adapters/postgres/run-store.js +26 -2
  24. package/dist/adapters/postgres/schema.d.ts +61 -2
  25. package/dist/adapters/postgres/schema.js +95 -10
  26. package/dist/adapters/redis/index.d.ts +1 -0
  27. package/dist/adapters/redis/index.js +1 -0
  28. package/dist/adapters/redis/rate-limit.d.ts +46 -0
  29. package/dist/adapters/redis/rate-limit.js +55 -0
  30. package/dist/adapters/supabase/index.d.ts +15 -0
  31. package/dist/adapters/supabase/index.js +15 -0
  32. package/dist/adapters/supabase/rls.js +33 -0
  33. package/dist/agents/agent.d.ts +99 -1
  34. package/dist/agents/agent.js +215 -16
  35. package/dist/agents/define.js +24 -4
  36. package/dist/agents/engine.d.ts +48 -0
  37. package/dist/agents/engine.js +370 -13
  38. package/dist/audio/index.d.ts +151 -0
  39. package/dist/audio/index.js +183 -0
  40. package/dist/capabilities/index.d.ts +5 -1
  41. package/dist/capabilities/index.js +23 -0
  42. package/dist/capabilities/runtime.d.ts +8 -0
  43. package/dist/connections/cipher.d.ts +103 -0
  44. package/dist/connections/cipher.js +141 -0
  45. package/dist/connections/index.d.ts +156 -0
  46. package/dist/connections/index.js +72 -0
  47. package/dist/connections/oauth/client.d.ts +84 -0
  48. package/dist/connections/oauth/client.js +141 -0
  49. package/dist/connections/oauth/index.d.ts +166 -0
  50. package/dist/connections/oauth/index.js +226 -0
  51. package/dist/connections/oauth/service.d.ts +90 -0
  52. package/dist/connections/oauth/service.js +132 -0
  53. package/dist/connections/pause.d.ts +68 -0
  54. package/dist/connections/pause.js +97 -0
  55. package/dist/connections/resolver.d.ts +59 -0
  56. package/dist/connections/resolver.js +138 -0
  57. package/dist/connections/resume.d.ts +64 -0
  58. package/dist/connections/resume.js +99 -0
  59. package/dist/connections/vault-cipher.d.ts +95 -0
  60. package/dist/connections/vault-cipher.js +206 -0
  61. package/dist/core/budget.d.ts +55 -0
  62. package/dist/core/budget.js +56 -0
  63. package/dist/core/content-parts.d.ts +26 -2
  64. package/dist/core/content-parts.js +1 -0
  65. package/dist/core/context.d.ts +25 -0
  66. package/dist/core/errors.d.ts +1 -1
  67. package/dist/core/errors.js +15 -0
  68. package/dist/core/events.d.ts +110 -2
  69. package/dist/core/events.js +33 -2
  70. package/dist/core/index.d.ts +1 -0
  71. package/dist/core/index.js +1 -0
  72. package/dist/core/validation.js +19 -0
  73. package/dist/documents/index.d.ts +14 -0
  74. package/dist/documents/parsers/text.d.ts +16 -0
  75. package/dist/documents/parsers/text.js +54 -2
  76. package/dist/entries/connections.d.ts +10 -0
  77. package/dist/entries/connections.js +10 -0
  78. package/dist/entries/guardrails.d.ts +14 -0
  79. package/dist/entries/guardrails.js +14 -0
  80. package/dist/entries/knowledge.d.ts +17 -0
  81. package/dist/entries/knowledge.js +16 -0
  82. package/dist/entries/mcp-server.d.ts +9 -0
  83. package/dist/entries/mcp-server.js +9 -0
  84. package/dist/entries/runtime.d.ts +10 -0
  85. package/dist/entries/runtime.js +8 -0
  86. package/dist/entries/testing.d.ts +28 -0
  87. package/dist/entries/testing.js +28 -0
  88. package/dist/graphql/resolvers.d.ts +7 -1
  89. package/dist/graphql/resolvers.js +20 -0
  90. package/dist/graphql/schema.d.ts +1 -1
  91. package/dist/graphql/schema.js +44 -0
  92. package/dist/guardrails/index.d.ts +115 -0
  93. package/dist/guardrails/index.js +108 -0
  94. package/dist/guardrails/moderation.d.ts +53 -0
  95. package/dist/guardrails/moderation.js +75 -0
  96. package/dist/guardrails/pii.d.ts +75 -0
  97. package/dist/guardrails/pii.js +193 -0
  98. package/dist/knowledge/communities.d.ts +166 -0
  99. package/dist/knowledge/communities.js +377 -0
  100. package/dist/knowledge/graph-global.d.ts +153 -0
  101. package/dist/knowledge/graph-global.js +246 -0
  102. package/dist/knowledge/graph-retrieval.d.ts +150 -0
  103. package/dist/knowledge/graph-retrieval.js +303 -0
  104. package/dist/knowledge/graph.d.ts +223 -0
  105. package/dist/knowledge/graph.js +369 -0
  106. package/dist/knowledge/index.d.ts +25 -0
  107. package/dist/knowledge/index.js +37 -1
  108. package/dist/knowledge/navigate.d.ts +89 -0
  109. package/dist/knowledge/navigate.js +107 -0
  110. package/dist/knowledge/retrieval.d.ts +98 -5
  111. package/dist/knowledge/retrieval.js +158 -28
  112. package/dist/mcp-server/index.d.ts +168 -0
  113. package/dist/mcp-server/index.js +175 -0
  114. package/dist/models/extraction.d.ts +73 -0
  115. package/dist/models/extraction.js +118 -0
  116. package/dist/models/index.d.ts +40 -1
  117. package/dist/models/index.js +22 -1
  118. package/dist/models/pricing.d.ts +2 -0
  119. package/dist/models/pricing.js +17 -1
  120. package/dist/models/provider-factory.js +9 -7
  121. package/dist/models/streaming.d.ts +111 -1
  122. package/dist/models/streaming.js +184 -3
  123. package/dist/persistence/index.d.ts +230 -0
  124. package/dist/persistence/index.js +2 -0
  125. package/dist/runtime/index.d.ts +1 -1
  126. package/dist/runtime/index.js +12 -0
  127. package/dist/runtime/worker.d.ts +36 -1
  128. package/dist/runtime/worker.js +37 -4
  129. package/dist/security/checklist.js +55 -0
  130. package/dist/security/findings.js +18 -9
  131. package/dist/server/bin.d.ts +19 -0
  132. package/dist/server/bin.js +226 -0
  133. package/dist/server/boot.d.ts +20 -2
  134. package/dist/server/boot.js +29 -3
  135. package/dist/server/cli.d.ts +13 -0
  136. package/dist/server/cli.js +7 -4
  137. package/dist/server/doctor.d.ts +74 -0
  138. package/dist/server/doctor.js +280 -0
  139. package/dist/skills/catalogue.d.ts +49 -0
  140. package/dist/skills/catalogue.js +61 -0
  141. package/dist/skills/context.d.ts +61 -0
  142. package/dist/skills/context.js +107 -0
  143. package/dist/skills/index.d.ts +2 -0
  144. package/dist/skills/index.js +2 -0
  145. package/dist/telemetry/spans.js +17 -0
  146. package/dist/testing/conformance/artifact-exports.d.ts +20 -0
  147. package/dist/testing/conformance/artifact-exports.js +172 -0
  148. package/dist/testing/conformance/artifacts.d.ts +11 -0
  149. package/dist/testing/conformance/artifacts.js +316 -0
  150. package/dist/testing/conformance/audio.d.ts +38 -0
  151. package/dist/testing/conformance/audio.js +145 -0
  152. package/dist/testing/conformance/capability.d.ts +23 -0
  153. package/dist/testing/conformance/capability.js +27 -0
  154. package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
  155. package/dist/testing/conformance/checkpoint-store.js +72 -0
  156. package/dist/testing/conformance/connections.d.ts +14 -0
  157. package/dist/testing/conformance/connections.js +171 -0
  158. package/dist/testing/conformance/conversation-store.d.ts +10 -0
  159. package/dist/testing/conformance/conversation-store.js +60 -0
  160. package/dist/testing/conformance/evaluation.d.ts +10 -0
  161. package/dist/testing/conformance/evaluation.js +295 -0
  162. package/dist/testing/conformance/files.d.ts +19 -0
  163. package/dist/testing/conformance/files.js +454 -0
  164. package/dist/testing/conformance/flows.d.ts +16 -0
  165. package/dist/testing/conformance/flows.js +193 -0
  166. package/dist/testing/conformance/graph.d.ts +22 -0
  167. package/dist/testing/conformance/graph.js +500 -0
  168. package/dist/testing/conformance/hitl.d.ts +25 -0
  169. package/dist/testing/conformance/hitl.js +523 -0
  170. package/dist/testing/conformance/index.d.ts +163 -0
  171. package/dist/testing/conformance/index.js +321 -0
  172. package/dist/testing/conformance/invariants.d.ts +23 -0
  173. package/dist/testing/conformance/invariants.js +80 -0
  174. package/dist/testing/conformance/knowledge.d.ts +41 -0
  175. package/dist/testing/conformance/knowledge.js +592 -0
  176. package/dist/testing/conformance/parents.d.ts +50 -0
  177. package/dist/testing/conformance/parents.js +39 -0
  178. package/dist/testing/conformance/rate-limit.d.ts +25 -0
  179. package/dist/testing/conformance/rate-limit.js +71 -0
  180. package/dist/testing/conformance/records.d.ts +36 -0
  181. package/dist/testing/conformance/records.js +400 -0
  182. package/dist/testing/conformance/rollups.d.ts +30 -0
  183. package/dist/testing/conformance/rollups.js +425 -0
  184. package/dist/testing/conformance/run-coordinator.d.ts +27 -0
  185. package/dist/testing/conformance/run-coordinator.js +150 -0
  186. package/dist/testing/conformance/run-event-log.d.ts +9 -0
  187. package/dist/testing/conformance/run-event-log.js +138 -0
  188. package/dist/testing/conformance/run-store.d.ts +12 -0
  189. package/dist/testing/conformance/run-store.js +294 -0
  190. package/dist/testing/conformance/session-state.d.ts +38 -0
  191. package/dist/testing/conformance/session-state.js +198 -0
  192. package/dist/testing/conformance/usage-limits.d.ts +10 -0
  193. package/dist/testing/conformance/usage-limits.js +309 -0
  194. package/dist/testing/conformance.d.ts +8 -0
  195. package/dist/testing/conformance.js +8 -0
  196. package/dist/testing/memory-backend.d.ts +39 -0
  197. package/dist/testing/memory-backend.js +41 -0
  198. package/dist/testing/pglite.d.ts +55 -0
  199. package/dist/testing/pglite.js +78 -0
  200. package/dist/testing/stub-model.d.ts +58 -0
  201. package/dist/testing/stub-model.js +71 -0
  202. package/dist/testing/supabase-storage-double.d.ts +35 -0
  203. package/dist/testing/supabase-storage-double.js +101 -0
  204. package/dist/toolkit/files.d.ts +125 -0
  205. package/dist/toolkit/files.js +320 -0
  206. package/dist/toolkit/http.d.ts +27 -0
  207. package/dist/toolkit/http.js +48 -0
  208. package/dist/toolkit/index.d.ts +31 -1
  209. package/dist/toolkit/index.js +26 -1
  210. package/dist/toolkit/mime.d.ts +97 -0
  211. package/dist/toolkit/mime.js +233 -0
  212. package/dist/toolkit/sandbox.d.ts +119 -0
  213. package/dist/toolkit/sandbox.js +239 -0
  214. package/dist/toolkit/ssrf.d.ts +139 -0
  215. package/dist/toolkit/ssrf.js +349 -0
  216. package/dist/toolkit/vendor.d.ts +91 -0
  217. package/dist/toolkit/vendor.js +153 -0
  218. package/dist/toolkit/web.d.ts +13 -0
  219. package/dist/toolkit/web.js +7 -1
  220. package/dist/tools/budget.d.ts +28 -0
  221. package/dist/tools/budget.js +35 -0
  222. package/dist/tools/credentials.d.ts +316 -0
  223. package/dist/tools/credentials.js +369 -0
  224. package/dist/tools/define.d.ts +45 -0
  225. package/dist/tools/define.js +26 -0
  226. package/dist/tools/find.d.ts +109 -0
  227. package/dist/tools/find.js +210 -0
  228. package/dist/tools/index.d.ts +35 -2
  229. package/dist/tools/index.js +11 -0
  230. package/dist/tools/library/audio.d.ts +63 -0
  231. package/dist/tools/library/audio.js +148 -0
  232. package/dist/tools/library/fs.d.ts +24 -0
  233. package/dist/tools/library/fs.js +102 -0
  234. package/dist/tools/library/index.d.ts +37 -2
  235. package/dist/tools/library/index.js +47 -0
  236. package/dist/tools/library/shell.d.ts +45 -0
  237. package/dist/tools/library/shell.js +70 -0
  238. package/dist/tools/meta-tools.js +8 -0
  239. package/dist/tools/registry.d.ts +146 -0
  240. package/dist/tools/registry.js +250 -4
  241. package/dist/usage/index.d.ts +21 -0
  242. package/dist/usage/index.js +1 -0
  243. package/dist/usage/rate-limit.d.ts +122 -0
  244. package/dist/usage/rate-limit.js +131 -0
  245. package/dist/usage/recorder.js +3 -0
  246. package/package.json +33 -2
@@ -18,10 +18,25 @@
18
18
  * on a refusal the model cannot resolve.
19
19
  */
20
20
  import { AgentPlatformError, isAgentPlatformError } from "../core/errors.js";
21
+ import { connectionNeedOf } from "../connections/pause.js";
21
22
  import { asId } from "../core/ids.js";
22
- import { streamModelTurn } from "../models/index.js";
23
+ import { applyInputGuardrails, applyOutputGuardrails } from "../guardrails/index.js";
24
+ import { streamModelTurn, turnText } from "../models/index.js";
23
25
  import { decideRetry, deriveRunMessageId, toPlatformError, DEFAULT_RETRY_POLICY, } from "../runtime/index.js";
26
+ import { META_TOOLS } from "../tools/index.js";
27
+ // The specific modules rather than `core/index.js`: the barrel pulls in `zod` through `core/validation.ts`,
28
+ // and a subpath's dependency graph is a guarantee this package tests for.
29
+ import { applyTokenBudget } from "../core/budget.js";
30
+ import { estimateTokens } from "../core/tokens.js";
24
31
  import { isQuestionPending } from "../hitl/service.js";
32
+ /**
33
+ * What one tool costs the model's context.
34
+ *
35
+ * A `ModelTurnTool` is not a catalogue entry — it carries the full input schema, because that is what a provider
36
+ * puts in the request — so this deliberately does *not* reuse `entryTokens`. Using the compact estimate here
37
+ * would understate a schema-heavy tool by an order of magnitude and produce a budget that never binds.
38
+ */
39
+ export const turnToolTokens = (tool) => estimateTokens(`${tool.name} ${tool.description ?? ""}`) + estimateTokens(JSON.stringify(tool.inputSchema ?? {}));
25
40
  const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
26
41
  /**
27
42
  * Take citation candidates off a tool's result — #165.
@@ -83,18 +98,119 @@ const questionMarker = (thrown, toolName) => {
83
98
  },
84
99
  };
85
100
  };
101
+ /**
102
+ * A record becomes an event, with no value ever attached.
103
+ *
104
+ * One place, so a future field on `GuardrailRecord` cannot reach the event log by being spread in somewhere: the
105
+ * mapping is explicit, field by field, and adding one here is a decision rather than a consequence.
106
+ */
107
+ const verdictEvent = (record) => ({
108
+ type: "guardrail.verdict",
109
+ guardrail: record.guardrail,
110
+ subject: record.subject,
111
+ outcome: record.outcome,
112
+ ...(record.what === undefined ? {} : { what: record.what }),
113
+ ...(record.code === undefined ? {} : { code: record.code }),
114
+ ...(record.threw === undefined ? {} : { threw: record.threw }),
115
+ });
86
116
  export const createDefaultEngine = (deps) => {
87
117
  const policy = deps.retry ?? DEFAULT_RETRY_POLICY;
88
118
  const sleep = deps.sleep ?? defaultSleep;
89
119
  const now = deps.now ?? Date.now;
90
120
  const streamTurn = deps.streamTurn ?? streamModelTurn;
91
121
  return {
92
- async *run({ run, context, signal }) {
93
- const manifest = await deps.loadManifest({ agentId: run.agentId, version: run.agentVersion, context });
122
+ async *run({ run, context: hostContext, signal }) {
123
+ const manifest = await deps.loadManifest({
124
+ agentId: run.agentId,
125
+ version: run.agentVersion,
126
+ context: hostContext,
127
+ });
128
+ /**
129
+ * The agent's tool policy, put on the context for everything downstream — task #244.
130
+ *
131
+ * The engine is the only layer that holds both halves: the manifest (per agent) and the registry (per
132
+ * deployment). Rather than threading the policy through `buildTools`, tool execution, `execute_tool` and
133
+ * every delegating tool, it travels on the context — which a model cannot write to, so `excluded` cannot
134
+ * be widened from inside a turn. `ExecutionContext.shadow` is the precedent and the same argument.
135
+ *
136
+ * Scoped **once, here**, and used everywhere below. A host-supplied context that already carried a policy
137
+ * is overridden by the manifest's: the manifest is what the run's `agentVersion` pins, so a stored
138
+ * definition — not the caller — decides what this agent may reach.
139
+ */
140
+ const context = { ...hostContext, agentToolPolicy: manifest.toolPolicy };
94
141
  const resolved = deps.resolveModel(manifest, context);
142
+ /**
143
+ * A structured agent needs a model that can do it — task #243 AC-3.
144
+ *
145
+ * Checked at resolution, before a token is spent, and here rather than only in the host's `resolveModel`
146
+ * because that callback is the host's: a host that has not been updated would resolve a text-only model
147
+ * and the agent would silently get prose, which is the defect being fixed rather than a new one.
148
+ *
149
+ * Skipped when the host returned no `definition`, following the same rule `modelModalities` already uses
150
+ * (#185): a caller that did not say what the model can do has not said it cannot do this, and refusing
151
+ * every structured agent from every host that has not been updated would be an outage dressed as a check.
152
+ */
153
+ if (manifest.responseFormat?.kind === "structured" && resolved.definition !== undefined) {
154
+ if (resolved.definition.capabilities?.structuredOutput !== true)
155
+ throw new AgentPlatformError({
156
+ code: "capability_unavailable",
157
+ message: `agent "${manifest.id}" asks for a structured response format and the resolved model ` +
158
+ `${resolved.modelId} does not declare the \`structuredOutput\` capability. Add ` +
159
+ "`requiredCapabilities: { structuredOutput: true }` to the agent's model policy so resolution " +
160
+ "picks a model that can, rather than discovering it mid-turn.",
161
+ retryable: false,
162
+ });
163
+ }
95
164
  const system = (await (deps.systemPrompt?.(manifest, context) ?? manifest.instructions)) || undefined;
96
165
  const history = await deps.loadHistory(context, run);
97
- const declared = deps.buildTools ? await deps.buildTools(context, manifest) : [];
166
+ const built = deps.buildTools ? await deps.buildTools(context, manifest) : [];
167
+ /**
168
+ * The budget, applied to the list the model will actually see — AC-3.
169
+ *
170
+ * Meta-tools are protected: dropping `find_tools` to save its own ~35 tokens would leave the model with a
171
+ * shortened list and no way to discover that it was shortened, which is the failure this whole mechanism
172
+ * exists to prevent.
173
+ *
174
+ * `findable` is *derived* rather than configured. Whether truncation is a deferral or an amputation
175
+ * depends on one fact — is `find_tools` in the model's hands this turn — and asking the host to declare
176
+ * that separately would let the declaration be wrong.
177
+ */
178
+ const budgetOutcome = deps.catalogBudget === undefined
179
+ ? undefined
180
+ : applyTokenBudget({
181
+ items: built,
182
+ budget: deps.catalogBudget,
183
+ tokensOf: turnToolTokens,
184
+ nameOf: (tool) => tool.name,
185
+ /**
186
+ * Protected: meta-tools, and whatever the agent declared preloaded — task #244.
187
+ *
188
+ * `toolPolicy.preloaded` and `toolPolicy.categories` say "these are loaded up front; everything
189
+ * else is discovered lazily". In this architecture *resident* is what "loaded up front" means, so
190
+ * the honest interpretation of both fields is that a budget may not drop them. With no budget
191
+ * configured every tool is resident anyway and the fields are a no-op — correct, and the reason
192
+ * they cannot be enforced anywhere else.
193
+ *
194
+ * Note this cannot make a tool appear: an excluded tool never reaches `built` (the registry
195
+ * removed it), and naming an excluded tool as preloaded does not resurrect it. Exclusion is a
196
+ * permission and residency is a budget; the permission wins.
197
+ */
198
+ protect: (tool) => META_TOOLS.includes(tool.name) ||
199
+ (manifest.toolPolicy?.preloaded ?? []).includes(tool.name) ||
200
+ (tool.category !== undefined && (manifest.toolPolicy?.categories ?? []).includes(tool.category)),
201
+ });
202
+ const declared = budgetOutcome?.resident ?? built;
203
+ if (budgetOutcome !== undefined && (budgetOutcome.dropped.length > 0 || budgetOutcome.overBudget)) {
204
+ yield {
205
+ type: "catalog.truncated",
206
+ catalog: "tools",
207
+ budgetTokens: budgetOutcome.budgetTokens,
208
+ residentTokens: budgetOutcome.residentTokens,
209
+ dropped: budgetOutcome.dropped,
210
+ findable: declared.some((tool) => tool.name === "find_tools"),
211
+ ...(budgetOutcome.overBudget ? { overBudget: true } : {}),
212
+ };
213
+ }
98
214
  const maxSteps = manifest.limits?.maxSteps ?? 8;
99
215
  const messageId = deriveRunMessageId(run.id);
100
216
  // A decision taken while the run was parked. Executed before the model gets another turn, so the
@@ -113,6 +229,46 @@ export const createDefaultEngine = (deps) => {
113
229
  for (const event of questionEvents(answered, messages))
114
230
  yield event;
115
231
  }
232
+ /**
233
+ * Inspection, before the model sees anything — REQ-046 (#205), AC-1.
234
+ *
235
+ * Placed after resumption so an approved side effect that already happened is not re-inspected, and
236
+ * *before* the tools are built so a refusal costs nothing: no provider call, no tool discovery, no spend.
237
+ *
238
+ * The subject is the newest user turn rather than the whole history. Re-inspecting history every turn
239
+ * would re-refuse a conversation over something already allowed, and a guardrail that changes its mind
240
+ * about the past makes a conversation impossible to continue.
241
+ */
242
+ const guardrails = deps.guardrails ?? [];
243
+ if (guardrails.length > 0) {
244
+ const latest = [...messages].reverse().find((m) => m.role === "user");
245
+ const decision = await applyInputGuardrails(guardrails, { text: latest ? turnText(latest) : "" }, context);
246
+ for (const record of decision.records)
247
+ yield verdictEvent(record);
248
+ if (decision.outcome === "refused") {
249
+ /**
250
+ * The turn ends here, and it ends *visibly*.
251
+ *
252
+ * A text part rather than a thrown error: a refusal is a policy outcome, not a crash, and a run that
253
+ * failed with a stack trace tells the person nothing and the operator the wrong thing. The model is
254
+ * never called, which is what AC-3 asks for — "the turn does not proceed".
255
+ */
256
+ const partId = `${messageId}:guardrail:refused`;
257
+ yield {
258
+ type: "part.added",
259
+ messageId,
260
+ part: {
261
+ id: partId,
262
+ type: "text",
263
+ schemaVersion: 1,
264
+ createdAt: new Date(0).toISOString(),
265
+ text: decision.message,
266
+ },
267
+ };
268
+ yield { type: "run.completed" };
269
+ return;
270
+ }
271
+ }
116
272
  /**
117
273
  * The tools the model may call, with execution routed through the approval loop.
118
274
  *
@@ -131,6 +287,58 @@ export const createDefaultEngine = (deps) => {
131
287
  * carried on and completed. The person's answer arrived for a run that was already over.
132
288
  */
133
289
  let pendingQuestion = null;
290
+ /**
291
+ * A connection a tool needed and the tenant does not have — task #264.
292
+ *
293
+ * Tracked exactly like `pendingApproval` and `pendingQuestion`, and for the same reason: the run has to
294
+ * stop on an *event* the worker understands rather than on an error the model would try to work around.
295
+ */
296
+ /**
297
+ * Turns a marked connection failure into a pause, or leaves it to fail — task #264.
298
+ *
299
+ * Three ways to end up failing rather than pausing, and each is deliberate:
300
+ *
301
+ * - the failure is not a connection gap at all
302
+ * - no `connectionConsent` is wired, so this deployment has no flow to send anybody to
303
+ * - the callback returns `null`, meaning *this provider* is token-only and there is no login URL
304
+ *
305
+ * All three fail, because the alternative is a run parked for ever on a consent screen nobody can reach.
306
+ */
307
+ const consentMarker = async (thrown, toolName) => {
308
+ const need = connectionNeedOf(thrown);
309
+ if (need === null || deps.connectionConsent === undefined)
310
+ return null;
311
+ const offer = await deps.connectionConsent({ context, need: { ...need, toolName } });
312
+ if (offer === null)
313
+ return null;
314
+ return {
315
+ event: {
316
+ type: "connection.requested",
317
+ provider: need.provider,
318
+ loginUrl: offer.loginUrl,
319
+ scopes: offer.scopes,
320
+ toolName,
321
+ expiresAt: offer.expiresAt,
322
+ },
323
+ // Returned to the model, not thrown — the same choice the approval path makes. The tool call stays a
324
+ // real part of the record with a real result, and the run pauses on the event rather than on an error
325
+ // the model would try to work around.
326
+ marker: {
327
+ status: "connection_required",
328
+ provider: need.provider,
329
+ message: `${toolName} needs a ${need.provider} connection. The run is paused; do not retry.`,
330
+ },
331
+ };
332
+ };
333
+ /**
334
+ * The formed event, not the parts.
335
+ *
336
+ * Storing the pieces and building the event at the check site fought control-flow analysis: the
337
+ * assignment happens inside a tool's `execute` closure, so the outer `let` narrows to `never` at the
338
+ * check and every property access errors. Holding the event itself needs no property access there, and
339
+ * reads more like `pendingApproval` — a value the emit site simply yields.
340
+ */
341
+ let pendingConnection = null;
134
342
  /**
135
343
  * Citation candidates a tool handed back this turn, waiting for the claims they ground — #165.
136
344
  *
@@ -139,6 +347,15 @@ export const createDefaultEngine = (deps) => {
139
347
  * the passage supports. Emitting at the tool call would produce citations supporting nothing.
140
348
  */
141
349
  const pendingCitations = [];
350
+ /**
351
+ * Guardrail verdicts on tool arguments, buffered — REQ-046 (#205), AC-2 and AC-4.
352
+ *
353
+ * Buffered for the same reason as the citations above: the inspection happens inside a tool's `execute`,
354
+ * which is a callback the model's stream invokes and not a generator, so it cannot yield. Dropping the
355
+ * records instead would satisfy the enforcement half of AC-2 and quietly fail AC-4 — the check would work
356
+ * and leave no trace, which is the combination that makes an incident unreconstructable.
357
+ */
358
+ const pendingVerdicts = [];
142
359
  const approvals = deps.approvals;
143
360
  /**
144
361
  * Every tool is wrapped, whether or not an approval gate is configured.
@@ -147,19 +364,86 @@ export const createDefaultEngine = (deps) => {
147
364
  * interception point at all — and a question raised by one of its tools could not be noticed. The gate
148
365
  * decides *approvals*; parking a run on a question is not its business.
149
366
  */
367
+ /**
368
+ * What each call actually ran, keyed by the provider's call id — task #210.
369
+ *
370
+ * `execute_tool` names its target, so the tool the model called and the action performed are two different
371
+ * things. The wrapper is the only place that knows both, and the events are emitted somewhere else, so the
372
+ * fact has to be carried across. Without it a `destructive` tool invoked through `execute_tool` appears in
373
+ * the audit trail as "execute_tool", which is not an answer to the question the trail exists to answer.
374
+ */
375
+ const ranByCall = new Map();
150
376
  const tools = declared.map((t) => ({
151
377
  ...t,
152
- execute: async (input) => {
378
+ execute: async (input, options) => {
379
+ /**
380
+ * What this call is allowed to tell us — task #210.
381
+ *
382
+ * The host's closure is what reaches the registry, so it is the only thing that can know a call
383
+ * resolved to a different tool. `report` is how it says so, and the map is read where the events are
384
+ * emitted.
385
+ */
386
+ const report = (fact) => {
387
+ if (options?.toolCallId !== undefined)
388
+ ranByCall.set(options.toolCallId, fact.ranToolName);
389
+ };
390
+ /**
391
+ * A tool call is an output — AC-2.
392
+ *
393
+ * Before either branch below, so it applies whether or not an approval gate is configured, and
394
+ * *before* the gate so a refused call never becomes an approval request: asking a person to approve
395
+ * something that will not happen is how approving comes to feel meaningless.
396
+ *
397
+ * The refusal is returned as a tool *result* rather than thrown. The model then sees why its call did
398
+ * not happen and can say so, which is the difference between a run that explains itself and one that
399
+ * dies with a stack trace the person cannot act on.
400
+ */
401
+ if (guardrails.length > 0) {
402
+ const decision = await applyOutputGuardrails(guardrails, { kind: "tool-call", toolName: t.name, input }, context);
403
+ pendingVerdicts.push(...decision.records);
404
+ if (decision.outcome === "refused") {
405
+ return { refused: true, guardrail: decision.by, code: decision.code, message: decision.message };
406
+ }
407
+ // A redacted call runs with the redaction, not with what the model typed.
408
+ if (decision.value.kind === "tool-call")
409
+ input = decision.value.input;
410
+ }
411
+ /**
412
+ * What a tool hands back is inspected too — AC-3.
413
+ *
414
+ * A tool result is content entering the model's context from outside the tenant, and it is the
415
+ * likeliest source of personal data in a run: a document read by a tool contains whatever the document
416
+ * contains. Checking arguments and not results would guard the direction data leaves and ignore the
417
+ * direction it arrives.
418
+ *
419
+ * A refusal replaces the result rather than throwing, for the same reason as above: the model is told
420
+ * why and can say so, instead of the run dying where the person cannot see the cause.
421
+ */
422
+ const inspectResult = async (output) => {
423
+ if (guardrails.length === 0)
424
+ return output;
425
+ const decision = await applyOutputGuardrails(guardrails, { kind: "tool-result", toolName: t.name, output }, context);
426
+ pendingVerdicts.push(...decision.records);
427
+ if (decision.outcome === "refused") {
428
+ return { refused: true, guardrail: decision.by, code: decision.code, message: decision.message };
429
+ }
430
+ return decision.value.kind === "tool-result" ? decision.value.output : output;
431
+ };
153
432
  if (approvals === undefined) {
154
433
  try {
155
- return collectCitations(await t.execute(input), pendingCitations);
434
+ return collectCitations(await inspectResult(await t.execute(input, { ...(options ?? {}), report })), pendingCitations);
156
435
  }
157
436
  catch (thrown) {
158
437
  const parked = questionMarker(thrown, t.name);
159
- if (parked === null)
438
+ if (parked !== null) {
439
+ pendingQuestion = parked.interactionId;
440
+ return parked.marker;
441
+ }
442
+ const consent = await consentMarker(thrown, t.name);
443
+ if (consent === null)
160
444
  throw thrown;
161
- pendingQuestion = parked.interactionId;
162
- return parked.marker;
445
+ pendingConnection = consent.event;
446
+ return consent.marker;
163
447
  }
164
448
  }
165
449
  const outcome = await approvals.runTool(context, run.id, { name: t.name, input });
@@ -175,6 +459,9 @@ export const createDefaultEngine = (deps) => {
175
459
  message: `${t.name} needs human approval before it can run. The run is paused; do not retry.`,
176
460
  };
177
461
  }
462
+ // The approval path reaches the registry itself, so the fact needs no host cooperation here.
463
+ if (outcome.result.ranToolName !== undefined)
464
+ report({ ranToolName: outcome.result.ranToolName });
178
465
  if (!outcome.result.ok) {
179
466
  // The registry flattens a delegate's throw into a result, so the question arrives here as a code.
180
467
  const parked = questionMarker(outcome.result.error, t.name);
@@ -184,12 +471,14 @@ export const createDefaultEngine = (deps) => {
184
471
  }
185
472
  throw new AgentPlatformError(outcome.result.error);
186
473
  }
187
- return collectCitations(outcome.result.data, pendingCitations);
474
+ return collectCitations(await inspectResult(outcome.result.data), pendingCitations);
188
475
  },
189
476
  }));
190
477
  let attempt = 1;
191
478
  for (;;) {
192
479
  let emitted = 0;
480
+ // Whether this turn produced the structured answer a structured agent promises — #243.
481
+ let sawStructured = false;
193
482
  const textParts = new Map();
194
483
  const controller = new AbortController();
195
484
  try {
@@ -227,6 +516,15 @@ export const createDefaultEngine = (deps) => {
227
516
  ...(resolved.definition === undefined
228
517
  ? {}
229
518
  : { modelModalities: resolved.definition.inputModalities }),
519
+ // Mapped from the manifest here, so the model layer stays free of any dependency on `agents/`.
520
+ ...(manifest.responseFormat?.kind === "structured"
521
+ ? { structuredOutput: { schema: manifest.responseFormat.schema } }
522
+ : {}),
523
+ // From the resolved model's own declaration — #247. Absent definition means send nothing, the same
524
+ // rule `modelModalities` follows: a host that has not said what its model does has not said it caches.
525
+ ...(resolved.definition?.capabilities?.promptCaching === undefined
526
+ ? {}
527
+ : { promptCaching: resolved.definition.capabilities.promptCaching }),
230
528
  tools,
231
529
  maxSteps,
232
530
  abortSignal: controller.signal,
@@ -238,8 +536,10 @@ export const createDefaultEngine = (deps) => {
238
536
  controller.abort();
239
537
  return;
240
538
  }
241
- for (const event of mapChunk(chunk, messageId, resolved, textParts)) {
539
+ for (const event of mapChunk(chunk, messageId, resolved, textParts, ranByCall)) {
242
540
  emitted += 1;
541
+ if (event.type === "part.added" && event.part.type === "structured")
542
+ sawStructured = true;
243
543
  yield event;
244
544
  }
245
545
  // Raised by a tool call this turn. Stop here rather than letting the model keep going: the
@@ -257,6 +557,13 @@ export const createDefaultEngine = (deps) => {
257
557
  yield { type: "question.requested", interactionId: asId(pendingQuestion) };
258
558
  return;
259
559
  }
560
+ // The third stop — #264. The event carries the provider, the scopes and the URL, and **no secret**:
561
+ // it is rendered in a UI and clicked by a person, so it goes wherever a screenshot goes.
562
+ if (pendingConnection !== null) {
563
+ controller.abort();
564
+ yield pendingConnection;
565
+ return;
566
+ }
260
567
  }
261
568
  /**
262
569
  * Citations last, grounding the claims that were actually written — #165.
@@ -270,6 +577,25 @@ export const createDefaultEngine = (deps) => {
270
577
  * After the stream, so a citation cannot appear above text the reader is already looking at — the
271
578
  * append-only property `citationViewModel` depends on.
272
579
  */
580
+ /**
581
+ * A structured agent must have produced a structured answer — task #243 AC-2.
582
+ *
583
+ * `streamModelTurn` already validates and fails, so in the normal path this never fires. It fires for
584
+ * a host that supplied its own `streamTurn`, and that is the case worth guarding: the guarantee a
585
+ * consumer bought is "structured or an error", and if it depended solely on the shipped model layer
586
+ * then any host replacing that layer would silently get prose again — the original defect, reachable
587
+ * through a documented extension point.
588
+ */
589
+ if (manifest.responseFormat?.kind === "structured" && !sawStructured)
590
+ throw new AgentPlatformError({
591
+ code: "provider_error",
592
+ message: `agent "${manifest.id}" asks for a structured response format and the turn produced none. ` +
593
+ "The run fails rather than returning the turn's text, which would be prose presented as a " +
594
+ "validated object.",
595
+ retryable: true,
596
+ });
597
+ for (const record of pendingVerdicts.splice(0))
598
+ yield verdictEvent(record);
273
599
  if (deps.citations !== undefined && pendingCitations.length > 0) {
274
600
  const claims = [...textParts.values()].map((t) => t.partId);
275
601
  if (claims.length > 0) {
@@ -392,7 +718,9 @@ function* approvalEvents(resumed, messageId, messages) {
392
718
  });
393
719
  }
394
720
  /** Map one neutral chunk to zero or more engine events. Mutates `textParts` to accumulate deltas. */
395
- function* mapChunk(chunk, messageId, resolved, textParts) {
721
+ function* mapChunk(chunk, messageId, resolved, textParts,
722
+ /** What each call resolved to, when it was not what the model named — task #210. */
723
+ ranByCall = new Map()) {
396
724
  switch (chunk.type) {
397
725
  case "text-delta": {
398
726
  const existing = textParts.get(chunk.id);
@@ -425,6 +753,7 @@ function* mapChunk(chunk, messageId, resolved, textParts) {
425
753
  return;
426
754
  }
427
755
  case "tool-result": {
756
+ const ran = ranByCall.get(chunk.toolCallId);
428
757
  const part = {
429
758
  id: `${chunk.toolCallId}:result`,
430
759
  type: "tool-result",
@@ -432,10 +761,16 @@ function* mapChunk(chunk, messageId, resolved, textParts) {
432
761
  createdAt: new Date(0).toISOString(),
433
762
  toolCallId: asId(chunk.toolCallId),
434
763
  toolName: chunk.toolName,
764
+ ...(ran === undefined ? {} : { ranToolName: ran }),
435
765
  output: chunk.output,
436
766
  truncated: false,
437
767
  };
438
- yield { type: "tool.completed", toolCallId: asId(chunk.toolCallId), toolName: chunk.toolName };
768
+ yield {
769
+ type: "tool.completed",
770
+ toolCallId: asId(chunk.toolCallId),
771
+ toolName: chunk.toolName,
772
+ ...(ran === undefined ? {} : { ranToolName: ran }),
773
+ };
439
774
  yield { type: "part.added", messageId, part };
440
775
  return;
441
776
  }
@@ -445,6 +780,9 @@ function* mapChunk(chunk, messageId, resolved, textParts) {
445
780
  inputTokens: chunk.usage.inputTokens,
446
781
  outputTokens: chunk.usage.outputTokens,
447
782
  cachedInputTokens: chunk.usage.cachedInputTokens,
783
+ // Carried only when the provider reported it, so a turn with no breakdown is distinguishable from a
784
+ // turn that wrote nothing — the same rule `imageCount` follows two lines down.
785
+ ...(chunk.usage.cacheWriteTokens === undefined ? {} : { cacheWriteTokens: chunk.usage.cacheWriteTokens }),
448
786
  ...(chunk.usage.reasoningTokens !== undefined ? { reasoningTokens: chunk.usage.reasoningTokens } : {}),
449
787
  // Counted at the send site (`nonTextCounts`), carried through so the ledger records it (#185).
450
788
  ...(chunk.usage.imageCount !== undefined ? { imageCount: chunk.usage.imageCount } : {}),
@@ -455,6 +793,25 @@ function* mapChunk(chunk, messageId, resolved, textParts) {
455
793
  };
456
794
  return;
457
795
  }
796
+ case "structured-output": {
797
+ /**
798
+ * The validated answer of a structured agent — task #243.
799
+ *
800
+ * Emitted once, complete. `streamModelTurn` has already validated it against the schema and fails the turn
801
+ * if it does not conform, so reaching here means the value satisfies what the caller asked for. Nothing
802
+ * partial is ever emitted: a half-built object does not satisfy a schema, so streaming one would publish
803
+ * values that violate the contract.
804
+ */
805
+ const part = {
806
+ id: `${messageId}:structured`,
807
+ type: "structured",
808
+ schemaVersion: 1,
809
+ createdAt: new Date(0).toISOString(),
810
+ value: chunk.value,
811
+ };
812
+ yield { type: "part.added", messageId, part };
813
+ return;
814
+ }
458
815
  case "error":
459
816
  throw chunk.error instanceof Error ? chunk.error : new Error(String(chunk.error));
460
817
  }