@aid-framework/runtime 0.0.0 → 0.1.48

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 (188) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +183 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/agents/deep_agents.d.ts +78 -0
  5. package/dist/agents/deep_agents.d.ts.map +1 -0
  6. package/dist/agents/deep_agents.js +477 -0
  7. package/dist/agents/deep_agents.js.map +1 -0
  8. package/dist/agents/engine.d.ts +85 -0
  9. package/dist/agents/engine.d.ts.map +1 -0
  10. package/dist/agents/engine.js +129 -0
  11. package/dist/agents/engine.js.map +1 -0
  12. package/dist/agents/gateway_chat_model.d.ts +151 -0
  13. package/dist/agents/gateway_chat_model.d.ts.map +1 -0
  14. package/dist/agents/gateway_chat_model.js +350 -0
  15. package/dist/agents/gateway_chat_model.js.map +1 -0
  16. package/dist/agents/guardrail_middleware.d.ts +57 -0
  17. package/dist/agents/guardrail_middleware.d.ts.map +1 -0
  18. package/dist/agents/guardrail_middleware.js +85 -0
  19. package/dist/agents/guardrail_middleware.js.map +1 -0
  20. package/dist/agents/guardrails.d.ts +124 -0
  21. package/dist/agents/guardrails.d.ts.map +1 -0
  22. package/dist/agents/guardrails.js +336 -0
  23. package/dist/agents/guardrails.js.map +1 -0
  24. package/dist/agents/index.d.ts +15 -0
  25. package/dist/agents/index.d.ts.map +1 -0
  26. package/dist/agents/index.js +15 -0
  27. package/dist/agents/index.js.map +1 -0
  28. package/dist/agents/ir.d.ts +144 -0
  29. package/dist/agents/ir.d.ts.map +1 -0
  30. package/dist/agents/ir.js +513 -0
  31. package/dist/agents/ir.js.map +1 -0
  32. package/dist/agents/loop.d.ts +145 -0
  33. package/dist/agents/loop.d.ts.map +1 -0
  34. package/dist/agents/loop.js +1025 -0
  35. package/dist/agents/loop.js.map +1 -0
  36. package/dist/agents/memory.d.ts +115 -0
  37. package/dist/agents/memory.d.ts.map +1 -0
  38. package/dist/agents/memory.js +258 -0
  39. package/dist/agents/memory.js.map +1 -0
  40. package/dist/agents/state.d.ts +137 -0
  41. package/dist/agents/state.d.ts.map +1 -0
  42. package/dist/agents/state.js +255 -0
  43. package/dist/agents/state.js.map +1 -0
  44. package/dist/concurrency.d.ts +36 -0
  45. package/dist/concurrency.d.ts.map +1 -0
  46. package/dist/concurrency.js +86 -0
  47. package/dist/concurrency.js.map +1 -0
  48. package/dist/control.d.ts +116 -0
  49. package/dist/control.d.ts.map +1 -0
  50. package/dist/control.js +281 -0
  51. package/dist/control.js.map +1 -0
  52. package/dist/cost.d.ts +95 -0
  53. package/dist/cost.d.ts.map +1 -0
  54. package/dist/cost.js +161 -0
  55. package/dist/cost.js.map +1 -0
  56. package/dist/errors.d.ts +213 -0
  57. package/dist/errors.d.ts.map +1 -0
  58. package/dist/errors.js +316 -0
  59. package/dist/errors.js.map +1 -0
  60. package/dist/evals/dataset.d.ts +45 -0
  61. package/dist/evals/dataset.d.ts.map +1 -0
  62. package/dist/evals/dataset.js +147 -0
  63. package/dist/evals/dataset.js.map +1 -0
  64. package/dist/evals/gate.d.ts +236 -0
  65. package/dist/evals/gate.d.ts.map +1 -0
  66. package/dist/evals/gate.js +572 -0
  67. package/dist/evals/gate.js.map +1 -0
  68. package/dist/evals/metrics.d.ts +141 -0
  69. package/dist/evals/metrics.d.ts.map +1 -0
  70. package/dist/evals/metrics.js +364 -0
  71. package/dist/evals/metrics.js.map +1 -0
  72. package/dist/evals/predicate.d.ts +79 -0
  73. package/dist/evals/predicate.d.ts.map +1 -0
  74. package/dist/evals/predicate.js +178 -0
  75. package/dist/evals/predicate.js.map +1 -0
  76. package/dist/evals/runner.d.ts +94 -0
  77. package/dist/evals/runner.d.ts.map +1 -0
  78. package/dist/evals/runner.js +202 -0
  79. package/dist/evals/runner.js.map +1 -0
  80. package/dist/gateway.d.ts +118 -0
  81. package/dist/gateway.d.ts.map +1 -0
  82. package/dist/gateway.js +250 -0
  83. package/dist/gateway.js.map +1 -0
  84. package/dist/index.d.ts +83 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +59 -0
  87. package/dist/index.js.map +1 -0
  88. package/dist/internal/json.d.ts +44 -0
  89. package/dist/internal/json.d.ts.map +1 -0
  90. package/dist/internal/json.js +90 -0
  91. package/dist/internal/json.js.map +1 -0
  92. package/dist/internal/text.d.ts +17 -0
  93. package/dist/internal/text.d.ts.map +1 -0
  94. package/dist/internal/text.js +22 -0
  95. package/dist/internal/text.js.map +1 -0
  96. package/dist/pipeline.d.ts +80 -0
  97. package/dist/pipeline.d.ts.map +1 -0
  98. package/dist/pipeline.js +159 -0
  99. package/dist/pipeline.js.map +1 -0
  100. package/dist/policy.d.ts +52 -0
  101. package/dist/policy.d.ts.map +1 -0
  102. package/dist/policy.js +199 -0
  103. package/dist/policy.js.map +1 -0
  104. package/dist/prompts.d.ts +86 -0
  105. package/dist/prompts.d.ts.map +1 -0
  106. package/dist/prompts.js +337 -0
  107. package/dist/prompts.js.map +1 -0
  108. package/dist/providers/base.d.ts +19 -0
  109. package/dist/providers/base.d.ts.map +1 -0
  110. package/dist/providers/base.js +13 -0
  111. package/dist/providers/base.js.map +1 -0
  112. package/dist/providers/fake.d.ts +98 -0
  113. package/dist/providers/fake.d.ts.map +1 -0
  114. package/dist/providers/fake.js +181 -0
  115. package/dist/providers/fake.js.map +1 -0
  116. package/dist/providers/http.d.ts +62 -0
  117. package/dist/providers/http.d.ts.map +1 -0
  118. package/dist/providers/http.js +245 -0
  119. package/dist/providers/http.js.map +1 -0
  120. package/dist/retrieval/chunking.d.ts +88 -0
  121. package/dist/retrieval/chunking.d.ts.map +1 -0
  122. package/dist/retrieval/chunking.js +188 -0
  123. package/dist/retrieval/chunking.js.map +1 -0
  124. package/dist/retrieval/citations.d.ts +98 -0
  125. package/dist/retrieval/citations.d.ts.map +1 -0
  126. package/dist/retrieval/citations.js +134 -0
  127. package/dist/retrieval/citations.js.map +1 -0
  128. package/dist/retrieval/connectors.d.ts +74 -0
  129. package/dist/retrieval/connectors.d.ts.map +1 -0
  130. package/dist/retrieval/connectors.js +218 -0
  131. package/dist/retrieval/connectors.js.map +1 -0
  132. package/dist/retrieval/embedding.d.ts +116 -0
  133. package/dist/retrieval/embedding.d.ts.map +1 -0
  134. package/dist/retrieval/embedding.js +282 -0
  135. package/dist/retrieval/embedding.js.map +1 -0
  136. package/dist/retrieval/gate.d.ts +52 -0
  137. package/dist/retrieval/gate.d.ts.map +1 -0
  138. package/dist/retrieval/gate.js +91 -0
  139. package/dist/retrieval/gate.js.map +1 -0
  140. package/dist/retrieval/hybrid.d.ts +76 -0
  141. package/dist/retrieval/hybrid.d.ts.map +1 -0
  142. package/dist/retrieval/hybrid.js +150 -0
  143. package/dist/retrieval/hybrid.js.map +1 -0
  144. package/dist/retrieval/metrics.d.ts +126 -0
  145. package/dist/retrieval/metrics.d.ts.map +1 -0
  146. package/dist/retrieval/metrics.js +200 -0
  147. package/dist/retrieval/metrics.js.map +1 -0
  148. package/dist/retrieval/pgvector.d.ts +62 -0
  149. package/dist/retrieval/pgvector.d.ts.map +1 -0
  150. package/dist/retrieval/pgvector.js +262 -0
  151. package/dist/retrieval/pgvector.js.map +1 -0
  152. package/dist/retrieval/rerank.d.ts +61 -0
  153. package/dist/retrieval/rerank.d.ts.map +1 -0
  154. package/dist/retrieval/rerank.js +105 -0
  155. package/dist/retrieval/rerank.js.map +1 -0
  156. package/dist/retrieval/retriever.d.ts +125 -0
  157. package/dist/retrieval/retriever.d.ts.map +1 -0
  158. package/dist/retrieval/retriever.js +281 -0
  159. package/dist/retrieval/retriever.js.map +1 -0
  160. package/dist/retrieval/stores.d.ts +106 -0
  161. package/dist/retrieval/stores.d.ts.map +1 -0
  162. package/dist/retrieval/stores.js +142 -0
  163. package/dist/retrieval/stores.js.map +1 -0
  164. package/dist/retrieval/types.d.ts +314 -0
  165. package/dist/retrieval/types.d.ts.map +1 -0
  166. package/dist/retrieval/types.js +388 -0
  167. package/dist/retrieval/types.js.map +1 -0
  168. package/dist/steps.d.ts +156 -0
  169. package/dist/steps.d.ts.map +1 -0
  170. package/dist/steps.js +199 -0
  171. package/dist/steps.js.map +1 -0
  172. package/dist/structured.d.ts +91 -0
  173. package/dist/structured.d.ts.map +1 -0
  174. package/dist/structured.js +155 -0
  175. package/dist/structured.js.map +1 -0
  176. package/dist/tools.d.ts +100 -0
  177. package/dist/tools.d.ts.map +1 -0
  178. package/dist/tools.js +201 -0
  179. package/dist/tools.js.map +1 -0
  180. package/dist/tracing.d.ts +104 -0
  181. package/dist/tracing.d.ts.map +1 -0
  182. package/dist/tracing.js +155 -0
  183. package/dist/tracing.js.map +1 -0
  184. package/dist/types.d.ts +240 -0
  185. package/dist/types.d.ts.map +1 -0
  186. package/dist/types.js +357 -0
  187. package/dist/types.js.map +1 -0
  188. package/package.json +80 -14
@@ -0,0 +1,1025 @@
1
+ import { ConcurrencyLimiter } from '../concurrency.js';
2
+ import { Budget, LedgerEntry } from '../cost.js';
3
+ import { AgentBudgetDegradedError, AgentTransferCycleError, BudgetExceededError, CapabilityMismatchError, ConfigurationError, GuardrailBlockedError, PolicyEvaluationError, ToolPermissionError, } from '../errors.js';
4
+ import { ModelGateway } from '../gateway.js';
5
+ import { evaluatePolicy, Principal, policySource } from '../policy.js';
6
+ import { ToolExecutor } from '../tools.js';
7
+ import { CompletionRequest, Message, TokenUsage, ToolCall, ToolDefinition, } from '../types.js';
8
+ import { checkEngine, DEEPAGENTS } from './engine.js';
9
+ import { ACTION_BLOCK, ACTION_ESCALATE, ACTION_REDACT, GuardrailEngine, } from './guardrails.js';
10
+ import { fromIr, hasControlTools, stageGuardrails, } from './ir.js';
11
+ import { checkMemorySupported, InProcessMemoryStore, renderContext, ThreadMemory, } from './memory.js';
12
+ import { RunResult, } from './state.js';
13
+ /**
14
+ * The native agent engine: one loop, three ways to leave it.
15
+ *
16
+ * The loop is a model call, its tool calls, the observations, repeat -- until the model
17
+ * answers without a tool call, until `maxSteps` is spent, until a budget is crossed, or
18
+ * until a human is needed. It consumes a `RuntimeSpec` (the IR seam in `agents/ir`) and
19
+ * calls models through `ModelGateway`, so fallback, cost accounting, and tracing behave
20
+ * exactly as they do for a pipeline step.
21
+ *
22
+ * Two control edges, and the difference is not cosmetic:
23
+ *
24
+ * **transfer** -- IR `handoffs`. Control *moves*. The caller does not resume; the target
25
+ * owns the rest of the turn and its answer is the run's answer. The target inherits the
26
+ * conversation but not the caller's instructions: a system prompt belongs to an agent, not
27
+ * to a transcript. The runtime primitive is `transfer`, and it never returns to the caller.
28
+ *
29
+ * **delegate** -- IR `delegates` (Deep Agents `subagents`). Control is *lent*. The delegate
30
+ * gets an isolated context -- its own system prompt and the one message it was handed,
31
+ * nothing else -- and returns exactly one observation. The caller resumes, sees that
32
+ * observation as a tool result, and authors the final answer. The runtime primitive is
33
+ * `delegate`.
34
+ *
35
+ * So the observable difference is who produces the final answer, and whose transcript
36
+ * contains the other's messages: after a transfer the answer is the target's and the
37
+ * caller's steps are a prefix of the target's run; after a delegate the answer is the
38
+ * caller's and the delegate's transcript is a separate, closed conversation.
39
+ *
40
+ * Budgets are ratified decision D1: crossing one is a hard failure. An agent that declares
41
+ * `onBudgetExhausted: degrade` instead steps **one** rung down its declared alias ladder,
42
+ * **once**, and both the step record and the ledger show the cheaper alias. It is never a
43
+ * retry loop: a degraded frame that still does not fit fails.
44
+ *
45
+ * Span recording (`agent_span`, HITL and guardrail events) is not ported here: decision D-7
46
+ * defers agent-level tracing to the layer that also gives this runtime a tracer seam, and no
47
+ * conformance case observes a span. The observable behaviours the Python module records
48
+ * spans *for* -- status, steps, transfer target, degradation -- are all in `RunResult`.
49
+ */
50
+ /** Namespacing for the control tools. A model cannot transfer by emitting prose. */
51
+ export const TRANSFER_PREFIX = 'transfer_to_';
52
+ export const DELEGATE_PREFIX = 'delegate_to_';
53
+ /** The one argument a control tool takes: what the other agent needs to know. */
54
+ export const CONTROL_ARGUMENT = 'message';
55
+ const DEGRADE = 'degrade';
56
+ const THREAD = 'thread';
57
+ const GLOBAL = 'global';
58
+ const IN_PROCESS = 'in-process';
59
+ const RESTORED_ALIAS = 'restored';
60
+ const TOOL_CONFIRMATION = 'tool-confirmation';
61
+ const GUARDRAIL_CHANNEL = 'guardrail';
62
+ const GUARDRAIL_OUTPUT = 'output';
63
+ const GUARDRAIL_TOOL = 'tool-call';
64
+ /**
65
+ * Side effects that must be explicitly granted. `read` and `none` are not in here on
66
+ * purpose: permissioning exists to gate effects, and gating reads would make every hostile
67
+ * tool the interesting one.
68
+ */
69
+ const SIDE_EFFECTING = new Set(['write', 'destructive']);
70
+ const REJECTED = 'rejected by the human approver';
71
+ export function outcomeOf(init) {
72
+ return {
73
+ status: init.status,
74
+ agentId: init.agentId,
75
+ answer: init.answer ?? '',
76
+ steps: init.steps,
77
+ path: init.path,
78
+ checkpoint: init.checkpoint,
79
+ guardrails: init.guardrails,
80
+ };
81
+ }
82
+ /** `round(x, 6)`, so a sum of float costs does not differ from Python's by an epsilon. */
83
+ function round6(value) {
84
+ return Math.round(value * 1e6) / 1e6;
85
+ }
86
+ /** Runs the IR's agents on the native engine. */
87
+ export class AgentRuntime {
88
+ #spec;
89
+ #gateway;
90
+ #executor;
91
+ #stores;
92
+ #defaultStore;
93
+ #guardrails;
94
+ #toolsByName;
95
+ constructor(spec, gateway, options = {}) {
96
+ this.#spec = spec;
97
+ this.#gateway = gateway;
98
+ this.#executor = new ToolExecutor(options.tools ?? [], { limiter: options.limiter });
99
+ this.#stores =
100
+ options.stores instanceof Map
101
+ ? new Map(options.stores)
102
+ : new Map(Object.entries(options.stores ?? {}));
103
+ this.#defaultStore = options.memoryStore ?? new InProcessMemoryStore();
104
+ this.#guardrails = new GuardrailEngine(spec.guardrails);
105
+ const byName = new Map();
106
+ for (const definition of spec.tools.values())
107
+ byName.set(definition.name, definition);
108
+ this.#toolsByName = byName;
109
+ this.#checkDeclarations();
110
+ }
111
+ /**
112
+ * Build a runtime from an emitted IR document.
113
+ *
114
+ * The IR's `deployments[].concurrency` becomes one limiter shared by the gateway and the
115
+ * tool executor, so model calls, tool calls, and delegation all draw from the same budget
116
+ * rather than running two independent pools.
117
+ */
118
+ static fromIr(document, options) {
119
+ const spec = fromIr(document);
120
+ const limiter = spec.concurrency === undefined ? undefined : new ConcurrencyLimiter(spec.concurrency);
121
+ const gateway = new ModelGateway({
122
+ providers: options.providers,
123
+ aliases: Object.fromEntries(spec.models),
124
+ ...(options.ledger === undefined ? {} : { ledger: options.ledger }),
125
+ ...(options.budget === undefined ? {} : { budget: options.budget }),
126
+ ...(limiter === undefined ? {} : { limiter }),
127
+ });
128
+ return new AgentRuntime(spec, gateway, {
129
+ ...(options.tools === undefined ? {} : { tools: options.tools }),
130
+ ...(options.stores === undefined ? {} : { stores: options.stores }),
131
+ ...(options.memoryStore === undefined ? {} : { memoryStore: options.memoryStore }),
132
+ ...(limiter === undefined ? {} : { limiter }),
133
+ });
134
+ }
135
+ get spec() {
136
+ return this.#spec;
137
+ }
138
+ get gateway() {
139
+ return this.#gateway;
140
+ }
141
+ /** Run `agentId` to a stop condition on one user message. */
142
+ async run(agentId, text, options = {}) {
143
+ const agent = this.#spec.agent(agentId);
144
+ checkEngine(agent);
145
+ const key = options.memoryKey ?? agent.id;
146
+ const frame = await this.#open(agent, [
147
+ Message.system(this.#spec.systemPrompt(agent)),
148
+ ...(options.conversation ?? []),
149
+ Message.user(text),
150
+ ], { path: [agent.id], memoryKey: key });
151
+ const startCost = this.#gateway.ledger.costUsd;
152
+ const startTokens = this.#gateway.ledger.totalTokens;
153
+ const outcome = await this.#drive(frame, {
154
+ principal: options.principal ?? new Principal(),
155
+ memoryKey: key,
156
+ });
157
+ return this.#wrap(outcome, startCost, startTokens);
158
+ }
159
+ /** Continue a paused run from its checkpoint, with a human's answer. */
160
+ async resume(checkpoint, decision, options = {}) {
161
+ this.#restoreLedger(checkpoint);
162
+ const startCost = this.#gateway.ledger.costUsd;
163
+ const startTokens = this.#gateway.ledger.totalTokens;
164
+ const outcome = await this.#resumeFrom(checkpoint, decision, options.principal ?? new Principal());
165
+ return this.#wrap(outcome, startCost, startTokens);
166
+ }
167
+ async #resumeFrom(checkpoint, decision, principal) {
168
+ if (checkpoint.resumeKind === 'delegate') {
169
+ throw new ConfigurationError(`the checkpoint belongs to agent ${JSON.stringify(checkpoint.agentId)}, which was waiting ` +
170
+ 'inside a delegate; resume the checkpoint that carries the caller link');
171
+ }
172
+ const agent = this.#spec.agent(checkpoint.agentId);
173
+ checkEngine(agent);
174
+ const frame = await this.#frameFrom(checkpoint);
175
+ frame.resumedDecision = decision;
176
+ frame.resumedChannel = checkpoint.channel;
177
+ const escalation = checkpoint.escalation;
178
+ if (decision === 'approve') {
179
+ if (escalation !== undefined && escalation.stage === GUARDRAIL_OUTPUT) {
180
+ // The model already produced this answer and a person released it; re-generating
181
+ // would spend tokens to answer the same question and could produce a different
182
+ // answer than the one that was approved.
183
+ const answer = escalation.text;
184
+ frame.messages.push(Message.assistant(answer));
185
+ await this.#remember(agent, answer, checkpoint.memoryKey);
186
+ return this.#guardrailCompleted(frame, answer);
187
+ }
188
+ frame.messages.push(...(await this.#observe(agent, checkpoint.calls, principal)));
189
+ }
190
+ else if (escalation !== undefined) {
191
+ // A person declined to release what the guardrail flagged. That is a decision, not an
192
+ // error, and the run stops here with the guardrail's own message as the answer rather
193
+ // than inventing one.
194
+ return outcomeOf({
195
+ status: 'blocked',
196
+ agentId: agent.id,
197
+ answer: escalation.message,
198
+ steps: frame.steps,
199
+ path: frame.path,
200
+ guardrails: frame.guardrails,
201
+ });
202
+ }
203
+ else {
204
+ frame.messages.push(...checkpoint.calls.map((call) => Message.tool(call.id, REJECTED, { name: call.name })));
205
+ }
206
+ const outcome = await this.#drive(frame, { principal, memoryKey: checkpoint.memoryKey });
207
+ if (checkpoint.caller === undefined)
208
+ return outcome;
209
+ if (outcome.status === 'paused' && outcome.checkpoint !== undefined) {
210
+ // The delegate paused again; the caller is still the one waiting, and the caller link
211
+ // has to survive the second pause or the run cannot continue.
212
+ return { ...outcome, checkpoint: { ...outcome.checkpoint, caller: checkpoint.caller } };
213
+ }
214
+ return this.#resumeCaller(checkpoint.caller, outcome, principal);
215
+ }
216
+ async #resumeCaller(caller, inner, principal) {
217
+ const frame = await this.#frameFrom(caller);
218
+ const call = caller.calls[0];
219
+ if (call === undefined) {
220
+ throw new ConfigurationError('a caller checkpoint holds no pending call, so there is nothing to resume into');
221
+ }
222
+ frame.messages.push(Message.tool(call.id, inner.answer, { name: call.name }));
223
+ frame.steps.push(...inner.steps);
224
+ frame.guardrails.push(...inner.guardrails);
225
+ return this.#drive(frame, { principal, memoryKey: caller.memoryKey });
226
+ }
227
+ /** Complete a frame after an approved escalation released its answer. */
228
+ #guardrailCompleted(frame, answer) {
229
+ return outcomeOf({
230
+ status: 'completed',
231
+ agentId: frame.agent.id,
232
+ answer,
233
+ steps: frame.steps,
234
+ path: frame.path,
235
+ guardrails: frame.guardrails,
236
+ });
237
+ }
238
+ async #drive(frame, options) {
239
+ const budget = this.#budgetFor(frame);
240
+ return this.#runEngine(frame, {
241
+ ...(budget === undefined ? {} : { budget }),
242
+ principal: options.principal,
243
+ memoryKey: options.memoryKey,
244
+ });
245
+ }
246
+ /**
247
+ * Drive the frame on the engine its agent declared.
248
+ *
249
+ * The native loop is the default and the only engine loaded eagerly. `deepagents` is
250
+ * imported here, on demand: the harness is an optional peer dependency, so a static import
251
+ * would make this package unloadable for every application that never opts in. The caller
252
+ * has already run `checkEngine`, which is what turns an absent harness into
253
+ * `DeepAgentsUnavailableError` -- so this import is reached only when the packages are
254
+ * installed at the pinned releases.
255
+ */
256
+ async #runEngine(frame, options) {
257
+ if (frame.agent.engine === DEEPAGENTS) {
258
+ const { DeepAgentsEngine } = await import('./deep_agents.js');
259
+ return new DeepAgentsEngine(this.#deepAgentsHost()).drive(frame, options.budget, options.principal, options.memoryKey);
260
+ }
261
+ return this.#runLoop(frame, options);
262
+ }
263
+ /**
264
+ * The narrow surface the Deep Agents backend is allowed to reach.
265
+ *
266
+ * A closure built inside the class, because the loop's state lives behind ECMAScript
267
+ * `#private` fields that a sibling module could not read. Passing exactly these fifteen
268
+ * operations keeps the backend on the same accounting, authorization, guardrail and
269
+ * memory paths as the native loop instead of giving it a second implementation of each.
270
+ */
271
+ #deepAgentsHost() {
272
+ return {
273
+ gateway: this.#gateway,
274
+ agent: (agentId) => this.#spec.agent(agentId),
275
+ systemPrompt: (agent) => this.#spec.systemPrompt(agent),
276
+ fallbacksFor: (alias) => this.#spec.fallbacksFor(alias),
277
+ budgetFor: (frame) => this.#budgetFor(frame),
278
+ toolDefinitions: (agent) => this.#toolDefinitions(agent),
279
+ toolNameFor: (toolId, agent) => this.#requireTool(toolId, `agent ${JSON.stringify(agent.id)}`).name,
280
+ toolRequiresConfirmation: (name) => this.#toolsByName.get(name)?.requiresConfirmation === true,
281
+ needsApproval: (agent, calls, principal) => this.#needsApproval(agent, calls, principal),
282
+ authorize: (agent, call, principal) => this.#authorize(agent, call, principal),
283
+ observe: (agent, calls, principal) => this.#observe(agent, calls, principal),
284
+ checkpoint: (frame, calls, memoryKey) => this.#checkpoint(frame, calls, memoryKey),
285
+ guardrailInput: (frame, memoryKey) => this.#guardrailText('input', frame, memoryKey, true),
286
+ guardrailAnswer: (frame, text, memoryKey) => this.#guardrailAnswer(frame, text, memoryKey),
287
+ guardrailToolCalls: (frame, calls, memoryKey) => this.#guardrailToolCalls(frame, calls, memoryKey),
288
+ remember: (agent, answer, memoryKey) => this.#remember(agent, answer, memoryKey),
289
+ outcome: (status, frame, answer) => this.#outcome(status, frame, answer),
290
+ };
291
+ }
292
+ async #runLoop(frame, options) {
293
+ const agent = frame.agent;
294
+ const { budget, principal, memoryKey } = options;
295
+ for (;;) {
296
+ if (frame.step >= agent.maxSteps) {
297
+ return this.#outcome('max-steps', frame);
298
+ }
299
+ if (!frame.inputChecked) {
300
+ frame.inputChecked = true;
301
+ const paused = this.#guardrailText('input', frame, memoryKey, true);
302
+ if (paused !== undefined)
303
+ return paused;
304
+ }
305
+ const request = this.#request(frame);
306
+ const routed = this.#route(frame, request, budget);
307
+ const generation = await this.#generate(agent, routed.alias, request, budget);
308
+ frame.step += 1;
309
+ const record = {
310
+ index: frame.step,
311
+ agentId: agent.id,
312
+ kind: 'answer',
313
+ alias: generation.alias,
314
+ model: generation.response.model,
315
+ costUsd: generation.costUsd,
316
+ totalTokens: generation.usage.totalTokens,
317
+ toolNames: [],
318
+ target: undefined,
319
+ degradedFrom: routed.degradedFrom,
320
+ };
321
+ frame.steps.push(record);
322
+ const response = generation.response;
323
+ if (response.toolCalls.length === 0) {
324
+ const guarded = this.#guardrailAnswer(frame, response.text, memoryKey);
325
+ if (typeof guarded !== 'string')
326
+ return guarded;
327
+ frame.messages.push(Message.assistant(guarded));
328
+ await this.#remember(agent, guarded, memoryKey);
329
+ return this.#outcome('completed', frame, guarded);
330
+ }
331
+ const control = this.#controlCall(response.toolCalls);
332
+ if (control !== undefined) {
333
+ const moved = await this.#control(frame, record, control, response, {
334
+ ...(budget === undefined ? {} : { budget }),
335
+ principal,
336
+ memoryKey,
337
+ });
338
+ if (moved !== undefined)
339
+ return moved;
340
+ continue;
341
+ }
342
+ record.kind = 'tools';
343
+ record.toolNames = response.toolCalls.map((call) => call.name);
344
+ const guardResult = this.#guardrailToolCalls(frame, response.toolCalls, memoryKey);
345
+ if (guardResult.paused !== undefined)
346
+ return guardResult.paused;
347
+ frame.messages.push(Message.assistantToolCalls(response.text, guardResult.calls));
348
+ if (this.#needsApproval(agent, guardResult.calls, principal)) {
349
+ const checkpoint = this.#checkpoint(frame, guardResult.calls, memoryKey);
350
+ return outcomeOf({
351
+ status: 'paused',
352
+ agentId: agent.id,
353
+ steps: frame.steps,
354
+ path: frame.path,
355
+ checkpoint,
356
+ guardrails: frame.guardrails,
357
+ });
358
+ }
359
+ frame.messages.push(...(await this.#observe(agent, guardResult.calls, principal)));
360
+ }
361
+ }
362
+ /**
363
+ * Run a whole-transcript stage (`input`) and apply its action.
364
+ *
365
+ * `rewrite` is what `redact` does here: the last user message is replaced with the masked
366
+ * text *in place*, so the model is called on the redacted turn rather than on the original
367
+ * with a note attached.
368
+ */
369
+ #guardrailText(stage, frame, memoryKey, rewrite) {
370
+ const index = lastUserIndex(frame.messages);
371
+ const text = index === undefined ? '' : (frame.messages[index]?.content ?? '');
372
+ const verdict = this.#guardrails.checkInput(stageGuardrails(frame.agent, stage), text);
373
+ this.#recordGuardrails(frame, verdict);
374
+ const deciding = verdict.deciding;
375
+ if (verdict.action === ACTION_BLOCK && deciding !== undefined) {
376
+ throw new GuardrailBlockedError(deciding.guardrail, stage, deciding.message);
377
+ }
378
+ if (verdict.action === ACTION_ESCALATE) {
379
+ return this.#guardrailPause(frame, verdict, memoryKey, [], '');
380
+ }
381
+ if (rewrite && verdict.action === ACTION_REDACT && index !== undefined) {
382
+ frame.messages[index] = Message.user(verdict.text);
383
+ }
384
+ return undefined;
385
+ }
386
+ /**
387
+ * Run the `output` stage over an answer, returning the text to ship.
388
+ *
389
+ * An `Outcome` means the answer is held: an escalation paused the run and the checkpoint
390
+ * carries the text, so approving it releases this exact answer without paying for a second
391
+ * generation.
392
+ */
393
+ #guardrailAnswer(frame, text, memoryKey) {
394
+ const verdict = this.#guardrails.checkOutput(stageGuardrails(frame.agent, GUARDRAIL_OUTPUT), text);
395
+ this.#recordGuardrails(frame, verdict);
396
+ const deciding = verdict.deciding;
397
+ if (verdict.action === ACTION_BLOCK && deciding !== undefined) {
398
+ throw new GuardrailBlockedError(deciding.guardrail, GUARDRAIL_OUTPUT, deciding.message);
399
+ }
400
+ if (verdict.action === ACTION_ESCALATE) {
401
+ return this.#guardrailPause(frame, verdict, memoryKey, [], text);
402
+ }
403
+ return verdict.action === ACTION_REDACT ? verdict.text : text;
404
+ }
405
+ /**
406
+ * Run the `tool-call` stage over every call in one step.
407
+ *
408
+ * The guardrail scopes on tool *ids*, so each model name is resolved through the IR before
409
+ * the engine sees it. A name the IR does not declare keeps its own spelling as the scope
410
+ * key -- it will be refused later for a different reason, and mapping it to some other
411
+ * tool's id would let a guardrail scoped to one tool decide about another.
412
+ */
413
+ #guardrailToolCalls(frame, calls, memoryKey) {
414
+ const guarded = [];
415
+ let escalation;
416
+ for (const original of calls) {
417
+ let call = original;
418
+ const definition = this.#toolsByName.get(call.name);
419
+ const toolId = definition !== undefined ? definition.id : call.name;
420
+ const verdict = this.#guardrails.checkToolCall(stageGuardrails(frame.agent, GUARDRAIL_TOOL), toolId, call.args);
421
+ this.#recordGuardrails(frame, verdict);
422
+ const deciding = verdict.deciding;
423
+ if (verdict.action === ACTION_BLOCK && deciding !== undefined) {
424
+ throw new GuardrailBlockedError(deciding.guardrail, GUARDRAIL_TOOL, deciding.message);
425
+ }
426
+ if (verdict.action === ACTION_ESCALATE && escalation === undefined)
427
+ escalation = verdict;
428
+ if (verdict.action === ACTION_REDACT) {
429
+ call = new ToolCall({ id: call.id, name: call.name, arguments: { ...verdict.arguments } });
430
+ }
431
+ guarded.push(call);
432
+ }
433
+ if (escalation !== undefined) {
434
+ return {
435
+ calls: guarded,
436
+ paused: this.#guardrailPause(frame, escalation, memoryKey, guarded, ''),
437
+ };
438
+ }
439
+ return { calls: guarded, paused: undefined };
440
+ }
441
+ /** Record what fired. Every record reaches the result, not only the deciding one. */
442
+ #recordGuardrails(frame, verdict) {
443
+ frame.guardrails.push(...verdict.records);
444
+ }
445
+ /** Enter HITL for an `escalate`. A pause, never a failure. */
446
+ #guardrailPause(frame, verdict, memoryKey, calls, text) {
447
+ const deciding = verdict.deciding;
448
+ if (deciding === undefined) {
449
+ throw new ConfigurationError('a guardrail verdict with no deciding record cannot be escalated');
450
+ }
451
+ const approval = frame.agent.approval;
452
+ const escalation = {
453
+ guardrail: deciding.guardrail,
454
+ stage: verdict.stage,
455
+ message: deciding.message,
456
+ channel: approval === undefined ? GUARDRAIL_CHANNEL : approval.channel,
457
+ text,
458
+ };
459
+ const checkpoint = this.#checkpoint(frame, calls, memoryKey, escalation);
460
+ return outcomeOf({
461
+ status: 'paused',
462
+ agentId: frame.agent.id,
463
+ steps: frame.steps,
464
+ path: frame.path,
465
+ checkpoint,
466
+ guardrails: frame.guardrails,
467
+ });
468
+ }
469
+ /** Act on a control call. `undefined` means the caller keeps control. */
470
+ async #control(frame, record, call, response, options) {
471
+ const transferring = call.name.startsWith(TRANSFER_PREFIX);
472
+ const prefix = transferring ? TRANSFER_PREFIX : DELEGATE_PREFIX;
473
+ const targetId = call.name.slice(prefix.length);
474
+ const declared = transferring ? frame.agent.handoffs : frame.agent.delegates;
475
+ if (!declared.includes(targetId)) {
476
+ throw new ConfigurationError(`agent ${JSON.stringify(frame.agent.id)} called control tool ` +
477
+ `${JSON.stringify(call.name)}, but does not declare it; declared handoffs ` +
478
+ `${listRepr(frame.agent.handoffs)} and delegates ${listRepr(frame.agent.delegates)}`);
479
+ }
480
+ if (frame.path.includes(targetId)) {
481
+ throw new AgentTransferCycleError([...frame.path, targetId]);
482
+ }
483
+ const message = controlMessage(call, response);
484
+ if (transferring) {
485
+ record.kind = 'transfer';
486
+ record.target = targetId;
487
+ return this.#transfer(frame, targetId, message, options);
488
+ }
489
+ record.kind = 'delegate';
490
+ record.target = targetId;
491
+ return this.#delegate(frame, call, response, targetId, message, options);
492
+ }
493
+ /**
494
+ * Move control to another agent. The caller does not come back.
495
+ *
496
+ * The target inherits the conversation -- every message except the caller's system prompt,
497
+ * which is the caller's instructions and not the target's.
498
+ */
499
+ async #transfer(frame, targetId, message, options) {
500
+ const target = this.#spec.agent(targetId);
501
+ frame.messages.push(Message.assistant(message));
502
+ const history = frame.messages.filter((item) => item.role !== 'system');
503
+ const targetFrame = await this.#open(target, [Message.system(this.#spec.systemPrompt(target)), ...history], { path: [...frame.path, target.id], memoryKey: options.memoryKey });
504
+ const outcome = await this.#drive(targetFrame, options);
505
+ return {
506
+ ...outcome,
507
+ steps: [...frame.steps, ...outcome.steps],
508
+ guardrails: [...frame.guardrails, ...outcome.guardrails],
509
+ path: [...frame.path, target.id],
510
+ };
511
+ }
512
+ /**
513
+ * Lend control to another agent, then continue as the caller.
514
+ *
515
+ * The delegate's context is isolated: its own system prompt and the single message it was
516
+ * handed. Nothing else of the caller's transcript is visible, which is what makes its
517
+ * result one observation rather than a continuation.
518
+ */
519
+ async #delegate(frame, call, response, targetId, message, options) {
520
+ const target = this.#spec.agent(targetId);
521
+ frame.messages.push(Message.assistantToolCalls(response.text, response.toolCalls));
522
+ const delegateFrame = await this.#open(target, [Message.system(this.#spec.systemPrompt(target)), Message.user(message)], { path: [...frame.path, target.id], memoryKey: options.memoryKey });
523
+ const inner = await this.#drive(delegateFrame, options);
524
+ if (inner.status === 'paused') {
525
+ const caller = {
526
+ ...this.#checkpoint(frame, [call], options.memoryKey),
527
+ resumeKind: 'delegate',
528
+ channel: '',
529
+ };
530
+ return {
531
+ ...inner,
532
+ steps: [...frame.steps, ...inner.steps],
533
+ guardrails: [...frame.guardrails, ...inner.guardrails],
534
+ checkpoint: inner.checkpoint === undefined ? undefined : { ...inner.checkpoint, caller },
535
+ };
536
+ }
537
+ if (inner.status !== 'completed') {
538
+ // The delegate ran out of steps or budget, so it has no result to hand back; the
539
+ // caller cannot borrow an answer that was never produced.
540
+ return {
541
+ ...inner,
542
+ steps: [...frame.steps, ...inner.steps],
543
+ guardrails: [...frame.guardrails, ...inner.guardrails],
544
+ };
545
+ }
546
+ frame.steps.push(...inner.steps);
547
+ frame.guardrails.push(...inner.guardrails);
548
+ frame.messages.push(Message.tool(call.id, inner.answer, { name: call.name }));
549
+ return this.#runLoop(frame, options);
550
+ }
551
+ /**
552
+ * Turn calls into observations, in the order the model asked for them.
553
+ *
554
+ * A name this agent cannot call comes back as a tool error instead of raising: the model
555
+ * composed the call, so the model is the party that can correct it, exactly as for bad
556
+ * arguments. A missing *registration* is a different failure and is refused when the
557
+ * runtime is built, where the application can fix it.
558
+ *
559
+ * Permission is the third case and it does raise, before any handler runs. The grant is the
560
+ * agent's declared tool list, and a side-effecting tool additionally needs an `auth` policy
561
+ * that holds for this principal. A model cannot repair either: the spec is what is wrong.
562
+ */
563
+ async #observe(agent, calls, principal) {
564
+ const registered = new Set(this.#executor.names());
565
+ const known = calls.filter((call) => registered.has(call.name));
566
+ for (const call of known)
567
+ this.#authorize(agent, call, principal);
568
+ const results = await this.#executor.executeAll(known);
569
+ const executed = new Map(results.map((result) => [result.callId, result]));
570
+ const observations = [];
571
+ for (const call of calls) {
572
+ const result = executed.get(call.id);
573
+ if (result === undefined) {
574
+ observations.push(Message.tool(call.id, `error: this agent has no tool named ${JSON.stringify(call.name)}`, {
575
+ name: call.name,
576
+ }));
577
+ continue;
578
+ }
579
+ observations.push(Message.tool(result.callId, result.content, { name: result.name }));
580
+ }
581
+ return observations;
582
+ }
583
+ /**
584
+ * Refuse a call this agent may not make, before the handler body runs.
585
+ *
586
+ * Deny by default, in three steps:
587
+ *
588
+ * 1. the agent must declare the tool -- the IR's `tools` is the grant, and an undeclared
589
+ * tool is refused even when the runtime has it registered;
590
+ * 2. a `write`/`destructive` tool must declare an `auth` policy, because a side effect with
591
+ * no authorization is an accident waiting to be called;
592
+ * 3. that policy is evaluated here, against *this* principal, not at build time -- `auth`
593
+ * is a question about the caller, and the caller is only known now.
594
+ *
595
+ * An undecidable policy is a denial. `PolicyEvaluationError` means the evaluator could not
596
+ * answer, and "could not answer" must never render as "allowed".
597
+ */
598
+ #authorize(agent, call, principal) {
599
+ const granted = new Set(agent.tools.map((toolId) => this.#requireTool(toolId, `agent ${JSON.stringify(agent.id)}`).name));
600
+ if (!granted.has(call.name)) {
601
+ throw new ToolPermissionError(agent.id, call.name, 'the agent does not declare it; the tool list is the grant, and there is no ' +
602
+ 'implicit wildcard');
603
+ }
604
+ const definition = this.#toolsByName.get(call.name);
605
+ if (definition === undefined) {
606
+ throw new ToolPermissionError(agent.id, call.name, 'it is registered on this runtime but not declared in the IR, so its side effects ' +
607
+ 'and auth policy are unknown');
608
+ }
609
+ if (definition.auth !== undefined) {
610
+ let allowed;
611
+ try {
612
+ allowed = evaluatePolicy(definition.auth, principal);
613
+ }
614
+ catch (error) {
615
+ if (!(error instanceof PolicyEvaluationError))
616
+ throw error;
617
+ throw new ToolPermissionError(agent.id, call.name, `its auth policy ${policySource(definition.auth)} could not be decided (${error.message})`);
618
+ }
619
+ if (!allowed) {
620
+ throw new ToolPermissionError(agent.id, call.name, `its auth policy ${policySource(definition.auth)} denies this principal`);
621
+ }
622
+ return;
623
+ }
624
+ if (SIDE_EFFECTING.has(definition.sideEffects)) {
625
+ throw new ToolPermissionError(agent.id, call.name, `it has sideEffects ${JSON.stringify(definition.sideEffects)} and declares no auth ` +
626
+ 'policy; a side effect with no authorization is denied by default');
627
+ }
628
+ }
629
+ #request(frame) {
630
+ let messages = [...frame.messages];
631
+ if (frame.memory.thread !== undefined)
632
+ messages = frame.memory.thread.trim(messages);
633
+ if (frame.memory.context.length > 0) {
634
+ const head = messages[0];
635
+ const rest = messages.slice(1);
636
+ if (head !== undefined)
637
+ messages = [head, Message.system(frame.memory.context), ...rest];
638
+ }
639
+ return new CompletionRequest({ messages, tools: this.#toolDefinitions(frame.agent) });
640
+ }
641
+ #toolDefinitions(agent) {
642
+ const definitions = agent.tools.map((toolId) => this.#executor.definition(this.#requireTool(toolId, `agent ${JSON.stringify(agent.id)}`).name));
643
+ for (const target of agent.handoffs)
644
+ definitions.push(controlDefinition(TRANSFER_PREFIX, target));
645
+ for (const target of agent.delegates)
646
+ definitions.push(controlDefinition(DELEGATE_PREFIX, target));
647
+ return definitions;
648
+ }
649
+ /**
650
+ * Call the model, refusing a silent downgrade to one that cannot tool-call.
651
+ *
652
+ * The pinned model must advertise `tool-calling` when the agent has tools or control edges.
653
+ * A gateway fallback is chosen for a provider failure without re-negotiating capabilities,
654
+ * so the answer's alias is checked too: an agent that cannot call a tool would otherwise
655
+ * answer from memory and look fine.
656
+ *
657
+ * The agent's ceiling goes to the gateway as the call's budget, so a breach is the
658
+ * gateway's own `BudgetExceededError` -- hard failure by default, with no second accounting
659
+ * path in the loop.
660
+ */
661
+ async #generate(agent, alias, request, budget) {
662
+ const required = agent.tools.length > 0 || hasControlTools(agent) ? ['tool-calling'] : [];
663
+ if (required.length > 0)
664
+ this.#gateway.require(alias, required);
665
+ const generation = await this.#gateway.generate(alias, request, {
666
+ fallbacks: this.#spec.fallbacksFor(alias),
667
+ ...(budget === undefined ? {} : { budget }),
668
+ });
669
+ if (required.length > 0) {
670
+ const resolved = this.#gateway.resolve(generation.alias).model;
671
+ if (!required.every((capability) => resolved.capabilities.has(capability))) {
672
+ throw new CapabilityMismatchError(generation.alias, required);
673
+ }
674
+ }
675
+ return generation;
676
+ }
677
+ /** Pick the alias for this call, stepping down the ladder at most once. */
678
+ #route(frame, request, budget) {
679
+ const agent = frame.agent;
680
+ if (agent.onBudgetExhausted !== DEGRADE || frame.degraded) {
681
+ return { alias: frame.alias, degradedFrom: undefined };
682
+ }
683
+ // Degrade steps down a *price* ladder, so it needs a dollar ceiling to step down from. A
684
+ // token-only ceiling cannot be made affordable by switching alias, and the gate refuses
685
+ // degrade without maxCostBudget, so there is nothing to route here. Returning keeps that
686
+ // hard failure with the ledger.
687
+ if (budget === undefined || budget.maxUsd === undefined) {
688
+ return { alias: frame.alias, degradedFrom: undefined };
689
+ }
690
+ if (this.#gateway.fits(frame.alias, request, budget)) {
691
+ return { alias: frame.alias, degradedFrom: undefined };
692
+ }
693
+ const cheaper = this.#spec.cheaperFallback(frame.alias);
694
+ if (cheaper === undefined) {
695
+ throw new AgentBudgetDegradedError(`agent ${JSON.stringify(agent.id)} declares onBudgetExhausted: degrade, but alias ` +
696
+ `${JSON.stringify(frame.alias)} declares no cheaper fallback; ` +
697
+ 'ir/agents-budget-degrade rejects this at build time, so this IR did not come ' +
698
+ 'through the gate');
699
+ }
700
+ if (!this.#gateway.fits(cheaper, request, budget)) {
701
+ // One rung was available and is still too dear: the budget is exhausted.
702
+ throw new BudgetExceededError(budget.scope, budget.maxUsd, this.#gateway.ledger.costUsd);
703
+ }
704
+ const previous = frame.alias;
705
+ frame.alias = cheaper;
706
+ frame.degraded = true;
707
+ return { alias: cheaper, degradedFrom: previous };
708
+ }
709
+ /**
710
+ * The agent's ceiling, expressed as absolute spend for one ledger.
711
+ *
712
+ * The ledger is shared by the whole run, so a per-agent ceiling is anchored at the spend
713
+ * the agent started from. That keeps a single accounting path instead of a second ledger
714
+ * per agent, and it is what lets a cheaper alias make the *same* ceiling affordable again.
715
+ */
716
+ #budgetFor(frame) {
717
+ const agent = frame.agent;
718
+ if (agent.maxCostBudget === undefined && agent.maxTokensBudget === undefined)
719
+ return undefined;
720
+ return new Budget({
721
+ ...(agent.maxCostBudget === undefined
722
+ ? {}
723
+ : { maxUsd: round6(frame.costAnchor + agent.maxCostBudget) }),
724
+ ...(agent.maxTokensBudget === undefined
725
+ ? {}
726
+ : { maxTotalTokens: frame.tokenAnchor + agent.maxTokensBudget }),
727
+ scope: `agent:${agent.id}`,
728
+ });
729
+ }
730
+ #controlCall(calls) {
731
+ const controls = calls.filter((call) => call.name.startsWith(TRANSFER_PREFIX) || call.name.startsWith(DELEGATE_PREFIX));
732
+ if (controls.length === 0)
733
+ return undefined;
734
+ if (calls.length > 1) {
735
+ throw new ConfigurationError('a control call must be the only call in its step, because it changes who owns the ' +
736
+ `rest of the turn; got ${listRepr(calls.map((call) => call.name))}`);
737
+ }
738
+ return controls[0];
739
+ }
740
+ /**
741
+ * Whether a person must answer before these calls run.
742
+ *
743
+ * Two triggers, both from the IR: the tool declares `requiresConfirmation`, or the agent's
744
+ * `humanInTheLoop.when` policy holds for this principal. The policy is evaluated against
745
+ * the caller, never against a step counter, which is why a pause cannot fire on "step 3"
746
+ * unless the IR's policy says so.
747
+ */
748
+ #needsApproval(agent, calls, principal) {
749
+ for (const call of calls) {
750
+ if (this.#toolsByName.get(call.name)?.requiresConfirmation === true)
751
+ return true;
752
+ }
753
+ return agent.approval !== undefined && evaluatePolicy(agent.approval.when, principal);
754
+ }
755
+ #checkpoint(frame, calls, memoryKey, escalation) {
756
+ const approval = frame.agent.approval;
757
+ let channel;
758
+ if (escalation !== undefined)
759
+ channel = escalation.channel;
760
+ else if (approval !== undefined)
761
+ channel = approval.channel;
762
+ else
763
+ channel = TOOL_CONFIRMATION;
764
+ return {
765
+ agentId: frame.agent.id,
766
+ step: frame.step,
767
+ messages: [...frame.messages],
768
+ steps: [...frame.steps],
769
+ calls: [...calls],
770
+ channel,
771
+ when: approval === undefined ? {} : { ...approval.when },
772
+ resumeKind: 'approve',
773
+ costAnchorUsd: frame.costAnchor,
774
+ tokenAnchor: frame.tokenAnchor,
775
+ spentUsd: this.#gateway.ledger.costUsd,
776
+ spentTokens: this.#gateway.ledger.totalTokens,
777
+ degraded: frame.degraded,
778
+ alias: frame.alias,
779
+ path: frame.path,
780
+ memoryKey,
781
+ caller: undefined,
782
+ escalation,
783
+ guardrails: [...frame.guardrails],
784
+ };
785
+ }
786
+ /**
787
+ * Carry a pause's spend into this process's ledger before resuming.
788
+ *
789
+ * A resume in a fresh process starts with an empty ledger, which would hand the agent a
790
+ * ceiling it has already partly spent. The restored amount is a real ledger entry under a
791
+ * distinct alias, so the spend stays auditable instead of becoming a number the loop
792
+ * quietly remembers.
793
+ */
794
+ #restoreLedger(checkpoint) {
795
+ const ledger = this.#gateway.ledger;
796
+ const missingUsd = round6(checkpoint.spentUsd - ledger.costUsd);
797
+ const missingTokens = checkpoint.spentTokens - ledger.totalTokens;
798
+ if (missingUsd <= 0 && missingTokens <= 0)
799
+ return;
800
+ ledger.charge(undefined, new LedgerEntry({
801
+ alias: RESTORED_ALIAS,
802
+ model: RESTORED_ALIAS,
803
+ provider: RESTORED_ALIAS,
804
+ usage: new TokenUsage({ inputTokens: Math.max(missingTokens, 0), outputTokens: 0 }),
805
+ costUsd: Math.max(missingUsd, 0),
806
+ }));
807
+ }
808
+ async #open(agent, messages, options) {
809
+ checkEngine(agent);
810
+ const memory = await this.#memoryState(agent, options.memoryKey);
811
+ const ledger = this.#gateway.ledger;
812
+ return {
813
+ agent,
814
+ messages: [...messages],
815
+ alias: agent.model,
816
+ path: options.path,
817
+ step: 0,
818
+ degraded: false,
819
+ memory,
820
+ memoryKey: options.memoryKey,
821
+ costAnchor: ledger.costUsd,
822
+ tokenAnchor: ledger.totalTokens,
823
+ steps: [],
824
+ resumedDecision: undefined,
825
+ resumedChannel: '',
826
+ inputChecked: false,
827
+ guardrails: [],
828
+ };
829
+ }
830
+ async #frameFrom(checkpoint) {
831
+ const agent = this.#spec.agent(checkpoint.agentId);
832
+ const memory = await this.#memoryState(agent, checkpoint.memoryKey);
833
+ return {
834
+ agent,
835
+ messages: [...checkpoint.messages],
836
+ alias: checkpoint.alias.length > 0 ? checkpoint.alias : agent.model,
837
+ path: checkpoint.path,
838
+ step: checkpoint.step,
839
+ degraded: checkpoint.degraded,
840
+ memory,
841
+ memoryKey: checkpoint.memoryKey,
842
+ costAnchor: checkpoint.costAnchorUsd,
843
+ tokenAnchor: checkpoint.tokenAnchor,
844
+ steps: [...checkpoint.steps],
845
+ resumedDecision: undefined,
846
+ resumedChannel: '',
847
+ // A checkpoint exists only after the input stage ran, so a resumed frame must not
848
+ // re-check the message it already passed -- and must not re-apply a redaction to text
849
+ // that is already masked.
850
+ inputChecked: true,
851
+ guardrails: [...checkpoint.guardrails],
852
+ };
853
+ }
854
+ async #memoryState(agent, memoryKey) {
855
+ let thread;
856
+ const blocks = [];
857
+ const scopes = [];
858
+ let entries = 0;
859
+ let tokens = 0;
860
+ for (const definition of agent.memory) {
861
+ checkMemorySupported({
862
+ memoryId: definition.id,
863
+ kind: definition.kind,
864
+ summarizeModel: definition.summarizeModel,
865
+ ttl: definition.ttl,
866
+ redact: definition.redact,
867
+ });
868
+ scopes.push(definition.scope);
869
+ if (definition.scope === THREAD) {
870
+ if (thread === undefined || definition.tokenBudget < budgetOf(thread)) {
871
+ thread = new ThreadMemory(definition.tokenBudget);
872
+ }
873
+ continue;
874
+ }
875
+ const values = await this.#storeFor(definition).read({
876
+ scope: definition.scope,
877
+ key: storageKey(definition, memoryKey),
878
+ });
879
+ const rendered = renderContext(values, definition.tokenBudget);
880
+ if (rendered.text.length > 0)
881
+ blocks.push(`[${definition.id}]\n${rendered.text}`);
882
+ entries += values.length;
883
+ tokens += rendered.tokens;
884
+ }
885
+ return {
886
+ thread,
887
+ context: blocks.join('\n\n'),
888
+ scopes,
889
+ entries,
890
+ tokens,
891
+ };
892
+ }
893
+ /** Record the answer in every memory that outlives the run. */
894
+ async #remember(agent, answer, memoryKey) {
895
+ if (answer.length === 0)
896
+ return;
897
+ for (const definition of agent.memory) {
898
+ if (definition.scope === THREAD)
899
+ continue;
900
+ await this.#storeFor(definition).append({
901
+ scope: definition.scope,
902
+ key: storageKey(definition, memoryKey),
903
+ value: answer,
904
+ });
905
+ }
906
+ }
907
+ #storeFor(definition) {
908
+ const backend = definition.backend;
909
+ if (backend === undefined || backend === IN_PROCESS)
910
+ return this.#defaultStore;
911
+ const store = this.#stores.get(backend);
912
+ if (store === undefined) {
913
+ const registered = [...this.#stores.keys()].sort();
914
+ throw new ConfigurationError(`memory ${JSON.stringify(definition.id)} names backend ` +
915
+ `${JSON.stringify(backend)}, which is not registered here; registered stores: ` +
916
+ `${registered.length === 0 ? '<none>' : listRepr(registered)}`);
917
+ }
918
+ return store;
919
+ }
920
+ #checkDeclarations() {
921
+ const registered = new Set(this.#executor.names());
922
+ for (const agent of this.#spec.agents.values()) {
923
+ const declared = new Set(agent.tools.map((toolId) => this.#requireTool(toolId, `agent ${JSON.stringify(agent.id)}`).name));
924
+ const missing = [...declared].filter((name) => !registered.has(name)).sort();
925
+ if (missing.length > 0) {
926
+ throw new ConfigurationError(`agent ${JSON.stringify(agent.id)} declares tools that are not registered on this ` +
927
+ `runtime: ${listRepr(missing)}; registered tools: ${listRepr([...registered].sort())}`);
928
+ }
929
+ const control = new Set([
930
+ ...agent.handoffs.map((target) => TRANSFER_PREFIX + target),
931
+ ...agent.delegates.map((target) => DELEGATE_PREFIX + target),
932
+ ]);
933
+ const shadowed = [...control].filter((name) => declared.has(name)).sort();
934
+ if (shadowed.length > 0) {
935
+ throw new ConfigurationError(`agent ${JSON.stringify(agent.id)} declares a tool named like a control tool ` +
936
+ `(${listRepr(shadowed)}); transfer and delegation would be silently shadowed`);
937
+ }
938
+ }
939
+ }
940
+ #outcome(status, frame, answer = '') {
941
+ return outcomeOf({
942
+ status,
943
+ agentId: frame.agent.id,
944
+ answer,
945
+ steps: frame.steps,
946
+ path: frame.path,
947
+ guardrails: frame.guardrails,
948
+ });
949
+ }
950
+ #wrap(outcome, startCost, startTokens) {
951
+ const ledger = this.#gateway.ledger;
952
+ return new RunResult({
953
+ status: outcome.status,
954
+ agentId: outcome.agentId,
955
+ answer: outcome.answer,
956
+ steps: outcome.steps,
957
+ path: outcome.path,
958
+ costUsd: round6(Math.max(ledger.costUsd - startCost, 0)),
959
+ totalTokens: Math.max(ledger.totalTokens - startTokens, 0),
960
+ ...(outcome.checkpoint === undefined ? {} : { checkpoint: outcome.checkpoint }),
961
+ guardrails: outcome.guardrails,
962
+ });
963
+ }
964
+ /** One IR tool id, resolved or refused by name. */
965
+ #requireTool(toolId, where) {
966
+ const definition = this.#spec.tools.get(toolId);
967
+ if (definition === undefined) {
968
+ throw new ConfigurationError(`${where} names tool ${JSON.stringify(toolId)}, which is not in the IR`);
969
+ }
970
+ return definition;
971
+ }
972
+ }
973
+ function budgetOf(thread) {
974
+ return thread.tokenBudget ?? 0;
975
+ }
976
+ /** The turn the input stage guards: the most recent user message. */
977
+ function lastUserIndex(messages) {
978
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
979
+ if (messages[index]?.role === 'user')
980
+ return index;
981
+ }
982
+ return undefined;
983
+ }
984
+ /** Global scope is shared by every caller; user scope is per key. */
985
+ function storageKey(definition, memoryKey) {
986
+ if (definition.scope === GLOBAL)
987
+ return definition.id;
988
+ return `${memoryKey}:${definition.id}`;
989
+ }
990
+ function controlMessage(call, response) {
991
+ const raw = call.args[CONTROL_ARGUMENT];
992
+ if (typeof raw === 'string' && raw.length > 0)
993
+ return raw;
994
+ return response.text.length > 0 ? response.text : `control passed to ${call.name}`;
995
+ }
996
+ function controlDefinition(prefix, target) {
997
+ const transferring = prefix === TRANSFER_PREFIX;
998
+ const description = transferring
999
+ ? `Transfer the conversation to agent ${JSON.stringify(target)}. Control moves: you will ` +
1000
+ 'not answer again, and the other agent produces the final answer.'
1001
+ : `Delegate a self-contained task to agent ${JSON.stringify(target)}. Control returns to ` +
1002
+ 'you with its result as an observation; you still produce the final answer.';
1003
+ return new ToolDefinition({
1004
+ name: `${prefix}${target}`,
1005
+ description,
1006
+ parameters: {
1007
+ type: 'object',
1008
+ properties: {
1009
+ [CONTROL_ARGUMENT]: {
1010
+ type: 'string',
1011
+ description: transferring
1012
+ ? 'Where to take the conversation from here.'
1013
+ : 'The whole task; the other agent cannot see this conversation.',
1014
+ },
1015
+ },
1016
+ required: [CONTROL_ARGUMENT],
1017
+ additionalProperties: false,
1018
+ },
1019
+ });
1020
+ }
1021
+ /** `['a', 'b']`, the shape Python's `{list(...)}` puts in a diagnostic. */
1022
+ function listRepr(values) {
1023
+ return `[${values.map((value) => JSON.stringify(value)).join(', ')}]`;
1024
+ }
1025
+ //# sourceMappingURL=loop.js.map