@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.
- package/README.md +59 -277
- package/dist/adapters/audio/openai.d.ts +36 -0
- package/dist/adapters/audio/openai.js +136 -0
- package/dist/adapters/bullmq/lock.d.ts +26 -2
- package/dist/adapters/embeddings/openai.d.ts +45 -0
- package/dist/adapters/embeddings/openai.js +109 -0
- package/dist/adapters/memory/connections.d.ts +10 -0
- package/dist/adapters/memory/connections.js +87 -0
- package/dist/adapters/memory/graph.d.ts +17 -0
- package/dist/adapters/memory/graph.js +267 -0
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.js +3 -0
- package/dist/adapters/memory/rate-limit.d.ts +14 -0
- package/dist/adapters/memory/rate-limit.js +28 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/postgres/connections.d.ts +15 -0
- package/dist/adapters/postgres/connections.js +134 -0
- package/dist/adapters/postgres/graph.d.ts +24 -0
- package/dist/adapters/postgres/graph.js +325 -0
- package/dist/adapters/postgres/index.d.ts +2 -0
- package/dist/adapters/postgres/index.js +2 -0
- package/dist/adapters/postgres/migrations.js +198 -0
- package/dist/adapters/postgres/run-store.js +26 -2
- package/dist/adapters/postgres/schema.d.ts +61 -2
- package/dist/adapters/postgres/schema.js +95 -10
- package/dist/adapters/redis/index.d.ts +1 -0
- package/dist/adapters/redis/index.js +1 -0
- package/dist/adapters/redis/rate-limit.d.ts +46 -0
- package/dist/adapters/redis/rate-limit.js +55 -0
- package/dist/adapters/supabase/index.d.ts +15 -0
- package/dist/adapters/supabase/index.js +15 -0
- package/dist/adapters/supabase/rls.js +33 -0
- package/dist/agents/agent.d.ts +99 -1
- package/dist/agents/agent.js +215 -16
- package/dist/agents/define.js +24 -4
- package/dist/agents/engine.d.ts +48 -0
- package/dist/agents/engine.js +370 -13
- package/dist/audio/index.d.ts +151 -0
- package/dist/audio/index.js +183 -0
- package/dist/capabilities/index.d.ts +5 -1
- package/dist/capabilities/index.js +23 -0
- package/dist/capabilities/runtime.d.ts +8 -0
- package/dist/connections/cipher.d.ts +103 -0
- package/dist/connections/cipher.js +141 -0
- package/dist/connections/index.d.ts +156 -0
- package/dist/connections/index.js +72 -0
- package/dist/connections/oauth/client.d.ts +84 -0
- package/dist/connections/oauth/client.js +141 -0
- package/dist/connections/oauth/index.d.ts +166 -0
- package/dist/connections/oauth/index.js +226 -0
- package/dist/connections/oauth/service.d.ts +90 -0
- package/dist/connections/oauth/service.js +132 -0
- package/dist/connections/pause.d.ts +68 -0
- package/dist/connections/pause.js +97 -0
- package/dist/connections/resolver.d.ts +59 -0
- package/dist/connections/resolver.js +138 -0
- package/dist/connections/resume.d.ts +64 -0
- package/dist/connections/resume.js +99 -0
- package/dist/connections/vault-cipher.d.ts +95 -0
- package/dist/connections/vault-cipher.js +206 -0
- package/dist/core/budget.d.ts +55 -0
- package/dist/core/budget.js +56 -0
- package/dist/core/content-parts.d.ts +26 -2
- package/dist/core/content-parts.js +1 -0
- package/dist/core/context.d.ts +25 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.js +15 -0
- package/dist/core/events.d.ts +110 -2
- package/dist/core/events.js +33 -2
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/validation.js +19 -0
- package/dist/documents/index.d.ts +14 -0
- package/dist/documents/parsers/text.d.ts +16 -0
- package/dist/documents/parsers/text.js +54 -2
- package/dist/entries/connections.d.ts +10 -0
- package/dist/entries/connections.js +10 -0
- package/dist/entries/guardrails.d.ts +14 -0
- package/dist/entries/guardrails.js +14 -0
- package/dist/entries/knowledge.d.ts +17 -0
- package/dist/entries/knowledge.js +16 -0
- package/dist/entries/mcp-server.d.ts +9 -0
- package/dist/entries/mcp-server.js +9 -0
- package/dist/entries/runtime.d.ts +10 -0
- package/dist/entries/runtime.js +8 -0
- package/dist/entries/testing.d.ts +28 -0
- package/dist/entries/testing.js +28 -0
- package/dist/graphql/resolvers.d.ts +7 -1
- package/dist/graphql/resolvers.js +20 -0
- package/dist/graphql/schema.d.ts +1 -1
- package/dist/graphql/schema.js +44 -0
- package/dist/guardrails/index.d.ts +115 -0
- package/dist/guardrails/index.js +108 -0
- package/dist/guardrails/moderation.d.ts +53 -0
- package/dist/guardrails/moderation.js +75 -0
- package/dist/guardrails/pii.d.ts +75 -0
- package/dist/guardrails/pii.js +193 -0
- package/dist/knowledge/communities.d.ts +166 -0
- package/dist/knowledge/communities.js +377 -0
- package/dist/knowledge/graph-global.d.ts +153 -0
- package/dist/knowledge/graph-global.js +246 -0
- package/dist/knowledge/graph-retrieval.d.ts +150 -0
- package/dist/knowledge/graph-retrieval.js +303 -0
- package/dist/knowledge/graph.d.ts +223 -0
- package/dist/knowledge/graph.js +369 -0
- package/dist/knowledge/index.d.ts +25 -0
- package/dist/knowledge/index.js +37 -1
- package/dist/knowledge/navigate.d.ts +89 -0
- package/dist/knowledge/navigate.js +107 -0
- package/dist/knowledge/retrieval.d.ts +98 -5
- package/dist/knowledge/retrieval.js +158 -28
- package/dist/mcp-server/index.d.ts +168 -0
- package/dist/mcp-server/index.js +175 -0
- package/dist/models/extraction.d.ts +73 -0
- package/dist/models/extraction.js +118 -0
- package/dist/models/index.d.ts +40 -1
- package/dist/models/index.js +22 -1
- package/dist/models/pricing.d.ts +2 -0
- package/dist/models/pricing.js +17 -1
- package/dist/models/provider-factory.js +9 -7
- package/dist/models/streaming.d.ts +111 -1
- package/dist/models/streaming.js +184 -3
- package/dist/persistence/index.d.ts +230 -0
- package/dist/persistence/index.js +2 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +12 -0
- package/dist/runtime/worker.d.ts +36 -1
- package/dist/runtime/worker.js +37 -4
- package/dist/security/checklist.js +55 -0
- package/dist/security/findings.js +18 -9
- package/dist/server/bin.d.ts +19 -0
- package/dist/server/bin.js +226 -0
- package/dist/server/boot.d.ts +20 -2
- package/dist/server/boot.js +29 -3
- package/dist/server/cli.d.ts +13 -0
- package/dist/server/cli.js +7 -4
- package/dist/server/doctor.d.ts +74 -0
- package/dist/server/doctor.js +280 -0
- package/dist/skills/catalogue.d.ts +49 -0
- package/dist/skills/catalogue.js +61 -0
- package/dist/skills/context.d.ts +61 -0
- package/dist/skills/context.js +107 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/telemetry/spans.js +17 -0
- package/dist/testing/conformance/artifact-exports.d.ts +20 -0
- package/dist/testing/conformance/artifact-exports.js +172 -0
- package/dist/testing/conformance/artifacts.d.ts +11 -0
- package/dist/testing/conformance/artifacts.js +316 -0
- package/dist/testing/conformance/audio.d.ts +38 -0
- package/dist/testing/conformance/audio.js +145 -0
- package/dist/testing/conformance/capability.d.ts +23 -0
- package/dist/testing/conformance/capability.js +27 -0
- package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
- package/dist/testing/conformance/checkpoint-store.js +72 -0
- package/dist/testing/conformance/connections.d.ts +14 -0
- package/dist/testing/conformance/connections.js +171 -0
- package/dist/testing/conformance/conversation-store.d.ts +10 -0
- package/dist/testing/conformance/conversation-store.js +60 -0
- package/dist/testing/conformance/evaluation.d.ts +10 -0
- package/dist/testing/conformance/evaluation.js +295 -0
- package/dist/testing/conformance/files.d.ts +19 -0
- package/dist/testing/conformance/files.js +454 -0
- package/dist/testing/conformance/flows.d.ts +16 -0
- package/dist/testing/conformance/flows.js +193 -0
- package/dist/testing/conformance/graph.d.ts +22 -0
- package/dist/testing/conformance/graph.js +500 -0
- package/dist/testing/conformance/hitl.d.ts +25 -0
- package/dist/testing/conformance/hitl.js +523 -0
- package/dist/testing/conformance/index.d.ts +163 -0
- package/dist/testing/conformance/index.js +321 -0
- package/dist/testing/conformance/invariants.d.ts +23 -0
- package/dist/testing/conformance/invariants.js +80 -0
- package/dist/testing/conformance/knowledge.d.ts +41 -0
- package/dist/testing/conformance/knowledge.js +592 -0
- package/dist/testing/conformance/parents.d.ts +50 -0
- package/dist/testing/conformance/parents.js +39 -0
- package/dist/testing/conformance/rate-limit.d.ts +25 -0
- package/dist/testing/conformance/rate-limit.js +71 -0
- package/dist/testing/conformance/records.d.ts +36 -0
- package/dist/testing/conformance/records.js +400 -0
- package/dist/testing/conformance/rollups.d.ts +30 -0
- package/dist/testing/conformance/rollups.js +425 -0
- package/dist/testing/conformance/run-coordinator.d.ts +27 -0
- package/dist/testing/conformance/run-coordinator.js +150 -0
- package/dist/testing/conformance/run-event-log.d.ts +9 -0
- package/dist/testing/conformance/run-event-log.js +138 -0
- package/dist/testing/conformance/run-store.d.ts +12 -0
- package/dist/testing/conformance/run-store.js +294 -0
- package/dist/testing/conformance/session-state.d.ts +38 -0
- package/dist/testing/conformance/session-state.js +198 -0
- package/dist/testing/conformance/usage-limits.d.ts +10 -0
- package/dist/testing/conformance/usage-limits.js +309 -0
- package/dist/testing/conformance.d.ts +8 -0
- package/dist/testing/conformance.js +8 -0
- package/dist/testing/memory-backend.d.ts +39 -0
- package/dist/testing/memory-backend.js +41 -0
- package/dist/testing/pglite.d.ts +55 -0
- package/dist/testing/pglite.js +78 -0
- package/dist/testing/stub-model.d.ts +58 -0
- package/dist/testing/stub-model.js +71 -0
- package/dist/testing/supabase-storage-double.d.ts +35 -0
- package/dist/testing/supabase-storage-double.js +101 -0
- package/dist/toolkit/files.d.ts +125 -0
- package/dist/toolkit/files.js +320 -0
- package/dist/toolkit/http.d.ts +27 -0
- package/dist/toolkit/http.js +48 -0
- package/dist/toolkit/index.d.ts +31 -1
- package/dist/toolkit/index.js +26 -1
- package/dist/toolkit/mime.d.ts +97 -0
- package/dist/toolkit/mime.js +233 -0
- package/dist/toolkit/sandbox.d.ts +119 -0
- package/dist/toolkit/sandbox.js +239 -0
- package/dist/toolkit/ssrf.d.ts +139 -0
- package/dist/toolkit/ssrf.js +349 -0
- package/dist/toolkit/vendor.d.ts +91 -0
- package/dist/toolkit/vendor.js +153 -0
- package/dist/toolkit/web.d.ts +13 -0
- package/dist/toolkit/web.js +7 -1
- package/dist/tools/budget.d.ts +28 -0
- package/dist/tools/budget.js +35 -0
- package/dist/tools/credentials.d.ts +316 -0
- package/dist/tools/credentials.js +369 -0
- package/dist/tools/define.d.ts +45 -0
- package/dist/tools/define.js +26 -0
- package/dist/tools/find.d.ts +109 -0
- package/dist/tools/find.js +210 -0
- package/dist/tools/index.d.ts +35 -2
- package/dist/tools/index.js +11 -0
- package/dist/tools/library/audio.d.ts +63 -0
- package/dist/tools/library/audio.js +148 -0
- package/dist/tools/library/fs.d.ts +24 -0
- package/dist/tools/library/fs.js +102 -0
- package/dist/tools/library/index.d.ts +37 -2
- package/dist/tools/library/index.js +47 -0
- package/dist/tools/library/shell.d.ts +45 -0
- package/dist/tools/library/shell.js +70 -0
- package/dist/tools/meta-tools.js +8 -0
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +250 -4
- package/dist/usage/index.d.ts +21 -0
- package/dist/usage/index.js +1 -0
- package/dist/usage/rate-limit.d.ts +122 -0
- package/dist/usage/rate-limit.js +131 -0
- package/dist/usage/recorder.js +3 -0
- package/package.json +33 -2
package/dist/agents/engine.js
CHANGED
|
@@ -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 {
|
|
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({
|
|
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
|
|
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
|
|
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
|
-
|
|
162
|
-
return
|
|
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 {
|
|
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
|
}
|