@arnilo/prism 0.1.3 → 0.1.5

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 (43) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/agent-approval.d.ts +49 -0
  3. package/dist/agent-approval.js +178 -0
  4. package/dist/agent-run-lifecycle.d.ts +5 -1
  5. package/dist/agent-run-lifecycle.js +211 -3
  6. package/dist/agent-session.d.ts +98 -0
  7. package/dist/agent-session.js +1811 -0
  8. package/dist/agent-tool-dispatch.d.ts +13 -0
  9. package/dist/agent-tool-dispatch.js +92 -0
  10. package/dist/agents.d.ts +16 -9
  11. package/dist/agents.js +14 -2267
  12. package/dist/cli-init.d.ts +0 -2
  13. package/dist/cli-init.js +0 -2
  14. package/dist/cli-runner.js +1 -1
  15. package/dist/contracts-core.d.ts +1396 -0
  16. package/dist/contracts-core.js +119 -0
  17. package/dist/contracts-protocol.d.ts +594 -0
  18. package/dist/contracts-protocol.js +2 -0
  19. package/dist/contracts-run-state.d.ts +285 -0
  20. package/dist/contracts-run-state.js +77 -0
  21. package/dist/contracts.d.ts +7 -2269
  22. package/dist/contracts.js +7 -192
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/rpc.js +2 -1
  26. package/docs/agent-loops.md +1 -1
  27. package/docs/agent-session-runtime.md +4 -2
  28. package/docs/browser-automation.md +13 -9
  29. package/docs/coding-agent-tools.md +1 -2
  30. package/docs/compaction-observational-memory.md +5 -6
  31. package/docs/index.md +4 -4
  32. package/docs/migration.md +114 -1
  33. package/docs/performance.md +10 -0
  34. package/docs/provider-conformance.md +2 -2
  35. package/docs/provider-layer.md +1 -1
  36. package/docs/provider-packages.md +1 -1
  37. package/docs/provider-primitives.md +1 -1
  38. package/docs/public-contracts.md +4 -2
  39. package/docs/release-and-install.md +64 -0
  40. package/docs/tool-execution-primitives.md +3 -3
  41. package/docs/tools.md +2 -2
  42. package/docs/use-case-model-selection.md +12 -11
  43. package/package.json +2 -2
package/dist/agents.js CHANGED
@@ -1,2270 +1,17 @@
1
- import { createHash } from "node:crypto";
2
- import { resolveLoop, resolveToolConcurrency, singleShotLoop } from "./agent-loops.js";
3
- import { agentFingerprint, boundedLoopSnapshot, initialAgentRunState, loadAgentRunState, publicState, saveAgentRunState, validateRunStateOptions, } from "./agent-run-state.js";
4
- import { createDefaultCompactionStrategy, isCompactionEntryData } from "./compaction.js";
5
- import { AgentDecisionError, AgentDelegationSuspendedError, AgentLoopStateError, AgentRunError, AgentRunStateError, DEFAULT_MAX_PENDING_DECISIONS, HARD_MAX_ELICITATION_BYTES, HARD_MAX_PENDING_DECISIONS, MAX_ATTRIBUTION_DEPTH, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_MAX_STICKY_DECISIONS, MAX_DECISION_REASON_BYTES, MAX_ELICITATION_BYTES, } from "./contracts.js";
6
- import { assertGuardrailsAllowed, GuardrailError, runGuardrails } from "./guardrails.js";
7
- import { identityTelemetryAttributes, ownershipFromIdentity, resolveRunIdentity } from "./identity.js";
8
- import { createId } from "./ids.js";
9
- import { assembleProviderInput } from "./input.js";
10
- import { createProviderTurnMetadata, readProviderHttpStatus } from "./observability.js";
11
- import { providerToolCallDeltaContent, reconstructToolCallDeltas } from "./provider-events.js";
12
- import { createProviderRequestPolicyChain, normalizeProviderRequestPolicyResult } from "./provider-request-policy.js";
13
- import { errorToErrorInfo, redactAgentEvent, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry, } from "./redaction.js";
14
- import { createDefaultRetryPolicy, waitForRetry } from "./retry.js";
15
- import { isFlushableRunLedger } from "./run-ledger.js";
16
- import { RunLimitError, RunLimitTracker, resolveRunLimits } from "./run-limits.js";
17
- import { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, rebuildSessionContext, } from "./session-stores.js";
18
- import { resolveActiveSkills } from "./skills.js";
19
- import { createLoadedSkillSet, resolveSkillsDisclosure } from "./skill-disclosure.js";
20
- import { resolveToolResultFold } from "./tool-result-fold.js";
21
- import { assertStructuredOutputRequestSupported, resolveRunProviderOptions } from "./structured-output.js";
22
- import { composeSystemPrompt, mergeSystemPromptConfig } from "./system-prompts.js";
23
- import { createToolRegistry, dispatchToolCall, resolveToolEffectDeclaration } from "./tools.js";
24
- import { canonicalToolEffectJson, toolEffectArgumentsHash } from "./tool-effects.js";
25
- export function createAgent(config) {
26
- return {
27
- config,
28
- createSession(sessionConfig = {}) {
29
- return createAgentSession({ ...sessionConfig, agent: this });
30
- },
31
- };
32
- }
33
- export function createAgentSession(config) {
34
- return new RuntimeAgentSession(config);
35
- }
36
- /** Resume a persisted built-in run. A claimed/dispatched tool is never replayed automatically. */
37
- export async function resumeAgentRun(agent, ref, resume, options) {
38
- return executePreparedAgentRunResume(await prepareAgentRunResume(agent, ref, resume, options));
39
- }
40
- /** Subscribe before resuming one durable run. Early consumer return aborts that resumed execution. */
41
- export async function* resumeAgentRunStream(agent, ref, resume, options) {
42
- throwIfAbortedSignal(options.signal);
43
- const prepared = await prepareAgentRunResume(agent, ref, resume, options, options.signal);
44
- const subscription = prepared.session.subscribe(options);
45
- let settled = false;
46
- const runPromise = executePreparedAgentRunResume(prepared, options.signal).finally(() => {
47
- settled = true;
48
- });
49
- try {
50
- for await (const event of subscription) {
51
- if ("runId" in event && event.runId !== ref.runId)
52
- continue;
53
- yield event;
54
- }
55
- await runPromise;
56
- }
57
- finally {
58
- if (!settled) {
59
- prepared.session.abort(new Error("resume stream consumer closed"));
60
- await runPromise.catch(() => undefined);
61
- }
62
- }
63
- }
64
- async function prepareAgentRunResume(agent, ref, resume, options, signal) {
65
- throwIfAbortedSignal(signal);
66
- const { record, state } = await loadAgentRunState(options.checkpoints, ref, options.ownership);
67
- if (state.definitionRevision !== options.definitionRevision ||
68
- state.agentId !== (agent.config.id ?? agent.config.name) ||
69
- state.fingerprint !== agentFingerprint(agent, options.definitionRevision)) {
70
- throw new AgentRunStateError("Agent definition revision or fingerprint mismatch on resume");
71
- }
72
- if (record.version !== resume.expectedVersion || state.status !== "suspended") {
73
- throw new AgentRunStateError("Stale or non-suspended agent run resume");
74
- }
75
- const session = new RuntimeAgentSession({ agent, id: state.sessionId, leafId: state.leafId });
76
- // Opt-in session-state restore (plan 015 Task 4): names only; bodies re-resolve from
77
- // the live registry the next time the model (re)loads them via load_skill.
78
- if (options.persistSessionState && state.sessionState?.loadedSkillNames) {
79
- session.restoreLoadedSkills(state.sessionState.loadedSkillNames);
80
- }
81
- if (resume.decision !== undefined && resume.decisions !== undefined) {
82
- throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Resume accepts exactly one of decision or decisions");
83
- }
84
- const pendingDecisions = pendingDecisionsOf(state);
85
- // Legacy approve maps to allow-once on every pending decision; legacy deny keeps its
86
- // terminal-denied behavior. Batch decisions are validated and applied atomically below.
87
- const resolved = resume.decisions !== undefined
88
- ? await resolveRunDecisions({ agent, state, decisions: resume.decisions, signal })
89
- : resume.decision === "approve" && pendingDecisions
90
- ? await resolveRunDecisions({
91
- agent,
92
- state,
93
- decisions: pendingDecisions.map((pending) => ({ approvalId: pending.approvalId, outcome: "allow_once" })),
94
- signal,
95
- })
96
- : undefined;
97
- if (resume.decision === undefined && resume.decisions === undefined) {
98
- throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Resume requires a decision or decisions");
99
- }
100
- if (resolved && resolved.remaining.length > 0) {
101
- throwIfAbortedSignal(signal);
102
- const single = resolved.remaining.length === 1 ? resolved.remaining[0] : undefined;
103
- const interruption = {
104
- kind: state.interruption?.kind ?? "tool_approval",
105
- reason: `${resolved.remaining.length} approval request(s) remain`,
106
- ...(single?.toolCallId ? { toolCallId: single.toolCallId } : {}),
107
- ...(single?.scope.toolName ? { toolName: single.scope.toolName } : {}),
108
- pendingDecisions: resolved.remaining,
109
- };
110
- const resuspended = await saveAgentRunState({
111
- checkpoints: options.checkpoints,
112
- state: {
113
- ...state,
114
- status: "suspended",
115
- interruption,
116
- pending: undefined,
117
- // Decided approvals persist on their entries so a partial batch never loses them;
118
- // they dispatch (or synthesize their result) when the run finally resumes.
119
- pendingCalls: state.pendingCalls?.map((entry) => {
120
- const decision = resolved.decisionsById.get(entry.approvalId);
121
- return decision ? { ...entry, decision } : entry;
122
- }),
123
- // Decided nested approvals persist on their nested-run entries, keyed by
124
- // root-visible approval id, so a partial batch never loses them either.
125
- nestedRuns: state.nestedRuns?.map((entry) => {
126
- const decided = entry.approvals.filter((approval) => resolved.decisionsById.has(approval.id));
127
- if (decided.length === 0)
128
- return entry;
129
- return {
130
- ...entry,
131
- decisions: {
132
- ...entry.decisions,
133
- ...Object.fromEntries(decided.map((approval) => [approval.id, resolved.decisionsById.get(approval.id)])),
134
- },
135
- };
136
- }),
137
- stickyDecisions: resolved.stickyDecisions,
138
- },
139
- expectedVersion: record.version,
140
- ownership: options.ownership,
141
- fencingToken: options.fencingToken,
142
- });
143
- return {
144
- kind: "resuspend",
145
- session,
146
- interruption,
147
- version: resuspended.record.version,
148
- ownership: options.ownership,
149
- result: {
150
- sessionId: state.sessionId,
151
- runId: state.runId,
152
- status: "suspended",
153
- leafId: state.leafId,
154
- text: "",
155
- content: [],
156
- runState: publicState(resuspended.state),
157
- interruption,
158
- },
159
- };
160
- }
161
- if (resume.decision === "deny") {
162
- throwIfAbortedSignal(signal);
163
- const denied = await saveAgentRunState({
164
- checkpoints: options.checkpoints,
165
- state: {
166
- ...state,
167
- status: "denied",
168
- loopState: undefined,
169
- pendingCalls: undefined,
170
- nestedRuns: undefined,
171
- stickyDecisions: undefined,
172
- },
173
- expectedVersion: record.version,
174
- ownership: options.ownership,
175
- fencingToken: options.fencingToken,
176
- });
177
- return {
178
- kind: "deny",
179
- session,
180
- interruption: state.interruption,
181
- version: denied.record.version,
182
- ownership: options.ownership,
183
- result: {
184
- sessionId: state.sessionId,
185
- runId: state.runId,
186
- status: "denied",
187
- leafId: state.leafId,
188
- text: "",
189
- content: [],
190
- runState: publicState(denied.state),
191
- interruption: state.interruption,
192
- },
193
- };
194
- }
195
- if (state.pending?.status === "dispatched" || state.pendingCalls?.some((entry) => entry.status === "dispatched")) {
196
- throw new AgentRunStateError("Ambiguous dispatched tool requires operator resolution");
197
- }
198
- const configured = agent.config.runState;
199
- if (configured && (configured.checkpoints !== options.checkpoints || configured.definitionRevision !== options.definitionRevision)) {
200
- throw new AgentRunStateError("Agent durable run-state configuration mismatch on resume");
201
- }
202
- throwIfAbortedSignal(signal);
203
- const claimed = await saveAgentRunState({
204
- checkpoints: options.checkpoints,
205
- state: {
206
- ...state,
207
- status: "running",
208
- interruption: undefined,
209
- stickyDecisions: resolved?.stickyDecisions ?? state.stickyDecisions,
210
- },
211
- expectedVersion: record.version,
212
- ownership: options.ownership,
213
- fencingToken: options.fencingToken,
214
- });
215
- return {
216
- kind: "approve",
217
- session,
218
- state: claimed.state,
219
- decisions: resolved?.decisionsById,
220
- ownership: options.ownership,
221
- runState: configured ?? {
222
- checkpoints: options.checkpoints,
223
- definitionRevision: options.definitionRevision,
224
- interruptBeforeTool: state.interruptBeforeTool,
225
- fencingToken: options.fencingToken,
226
- resumeNestedRun: options.resumeNestedRun,
227
- },
228
- };
229
- }
230
- /** Pending decisions of a suspended state, synthesizing the legacy single-approval shape. */
231
- function pendingDecisionsOf(state) {
232
- if (state.interruption?.pendingDecisions)
233
- return state.interruption.pendingDecisions;
234
- if (state.pending) {
235
- return [
236
- {
237
- approvalId: state.pending.call.id,
238
- kind: "tool_approval",
239
- toolCallId: state.pending.call.id,
240
- scope: { toolName: state.pending.call.name },
241
- reason: state.interruption?.reason ?? "Tool side effect requires approval",
242
- },
243
- ];
244
- }
245
- return undefined;
246
- }
247
1
  /**
248
- * Validate one decision batch against the suspended state. Fail-closed and atomic: any
249
- * invalid entry rejects the whole batch before any CAS, leaving state and version untouched.
250
- * Unknown and foreign approval ids share one non-enumerating error.
2
+ * Barrel for the agent runtime split at 0.1.4 (compat-preserving internal reorg).
3
+ * The four public exports (createAgent, createAgentSession, resumeAgentRun,
4
+ * resumeAgentRunStream) stay reachable through this barrel; RuntimeAgentSession,
5
+ * EventSubscriber, and the shared helpers live in agent-session.ts; the
6
+ * approval/pending-decision helpers in agent-approval.ts; the tool-dispatch
7
+ * helpers in agent-tool-dispatch.ts; the resume free functions in
8
+ * agent-run-lifecycle.ts; the fingerprint helpers in agent-run-state.ts.
9
+ *
10
+ * Deliberately NOT `export *`: star re-exports would surface the split modules'
11
+ * internal helpers (RuntimeAgentSession, pendingDecisionsOf, …) through
12
+ * agents.d.ts and change the declared surface — the 0.1.4 promise is an
13
+ * identical surface.
251
14
  */
252
- async function resolveRunDecisions(input) {
253
- const { agent, state, decisions } = input;
254
- if (decisions.length === 0)
255
- throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Decision batch must not be empty");
256
- if (decisions.length > HARD_MAX_PENDING_DECISIONS) {
257
- throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Decision batch exceeds ${HARD_MAX_PENDING_DECISIONS} entries`);
258
- }
259
- const pending = pendingDecisionsOf(state);
260
- if (!pending?.length)
261
- throw new AgentDecisionError("ERR_PRISM_DECISION_UNKNOWN", "No pending approval decisions for this run");
262
- const byId = new Map(pending.map((entry) => [entry.approvalId, entry]));
263
- const seen = new Set();
264
- const decisionsById = new Map();
265
- const stickies = [];
266
- const decidedAt = new Date().toISOString();
267
- const { registry } = activeTools(agent.config.tools);
268
- for (const decision of decisions) {
269
- if (seen.has(decision.approvalId)) {
270
- throw new AgentDecisionError("ERR_PRISM_DECISION_DUPLICATE", "Duplicate approval decision in batch");
271
- }
272
- seen.add(decision.approvalId);
273
- const target = byId.get(decision.approvalId);
274
- if (!target)
275
- throw new AgentDecisionError("ERR_PRISM_DECISION_UNKNOWN", "Unknown approval decision");
276
- if (decision.reason !== undefined && Buffer.byteLength(decision.reason, "utf8") > MAX_DECISION_REASON_BYTES) {
277
- throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Decision reason exceeds ${MAX_DECISION_REASON_BYTES} bytes`);
278
- }
279
- if (decision.outcome !== "allow_once" &&
280
- decision.outcome !== "allow_for_run" &&
281
- decision.outcome !== "reject_once" &&
282
- decision.outcome !== "reject_for_run") {
283
- throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Unknown approval outcome");
284
- }
285
- if (decision.modifiedArguments !== undefined) {
286
- if (target.kind !== "tool_approval" || !target.toolCallId) {
287
- throw new AgentDecisionError("ERR_PRISM_DECISION_SCOPE", "Modified arguments apply only to tool approvals");
288
- }
289
- await validateModifiedArguments(agent, registry, state, target, decision.modifiedArguments, input.signal);
290
- }
291
- if (decision.elicitation !== undefined) {
292
- if (target.kind !== "elicitation") {
293
- throw new AgentDecisionError("ERR_PRISM_DECISION_SCOPE", "Elicitation payload applies only to elicitation decisions");
294
- }
295
- await validateElicitationPayload(agent, state, target, decision.elicitation, input.signal);
296
- }
297
- decisionsById.set(decision.approvalId, decision);
298
- if (decision.outcome === "allow_for_run" || decision.outcome === "reject_for_run") {
299
- stickies.push({
300
- // A decision with modified arguments must not stick to the original arguments hash:
301
- // the modification is one-off, so the sticky scope matches by name/effect/identity only.
302
- scope: decision.modifiedArguments !== undefined ? { ...target.scope, argumentsHash: undefined } : target.scope,
303
- outcome: decision.outcome,
304
- ...(decision.reason !== undefined ? { reason: decision.reason } : {}),
305
- // Root-owned sticky scope includes the delegation path for nested decisions.
306
- ...(target.attribution ? { attribution: target.attribution } : {}),
307
- decidedAt,
308
- });
309
- }
310
- }
311
- const stickyDecisions = [...(state.stickyDecisions ?? []), ...stickies];
312
- if (stickyDecisions.length > DEFAULT_MAX_STICKY_DECISIONS) {
313
- throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Sticky decisions exceed ${DEFAULT_MAX_STICKY_DECISIONS} per run`);
314
- }
315
- return {
316
- decisionsById,
317
- stickyDecisions,
318
- remaining: pending.filter((entry) => !seen.has(entry.approvalId)),
319
- };
320
- }
321
- /** Decision-time revalidation of modified arguments: schema, then input guardrails. Policy and trust re-run at dispatch. */
322
- async function validateModifiedArguments(agent, registry, state, target, modified, signal) {
323
- const invalid = (message, cause) => new AgentDecisionError("ERR_PRISM_DECISION_INVALID", message, { cause });
324
- if (JSON.stringify(modified) === undefined || Buffer.byteLength(JSON.stringify(modified), "utf8") > MAX_ELICITATION_BYTES) {
325
- throw invalid("Modified arguments must be a bounded JSON object");
326
- }
327
- const call = state.pendingCalls?.find((entry) => entry.approvalId === target.approvalId)?.call ??
328
- (state.pending && state.pending.call.id === target.toolCallId ? state.pending.call : undefined);
329
- const toolName = target.scope.toolName ?? call?.name ?? "";
330
- const tool = registry.get(toolName);
331
- const context = { sessionId: state.sessionId, runId: state.runId, toolCallId: target.toolCallId ?? "", signal };
332
- if (agent.config.validator && tool) {
333
- const validation = await agent.config.validator(tool, modified, context);
334
- if (validation)
335
- throw invalid("Modified arguments failed schema validation");
336
- }
337
- const value = call
338
- ? { ...call, arguments: modified }
339
- : { type: "tool_call", id: target.toolCallId ?? "", name: toolName, arguments: modified };
340
- const guarded = await runGuardrails({
341
- stage: "tool_input",
342
- guardrails: agent.config.guardrails,
343
- value,
344
- context: {
345
- sessionId: state.sessionId,
346
- runId: state.runId,
347
- toolCallId: target.toolCallId,
348
- toolName,
349
- metadata: {},
350
- signal,
351
- },
352
- redactor: agent.config.redactor,
353
- });
354
- if (guarded.terminal)
355
- throw invalid("Modified arguments blocked by guardrail");
356
- }
357
- /**
358
- * Resolve a tool's declared elicitation contract for a gated call. A throwing hook falls back to
359
- * plain tool approval: malformed model args then surface as a tool error after approval, never
360
- * as a run failure at the gate. Output is bounded before it enters the pending-decision record.
361
- */
362
- function toolElicitationRequest(tool, args, context) {
363
- if (!tool?.elicitation)
364
- return undefined;
365
- let request;
366
- try {
367
- request = tool.elicitation(args, context);
368
- }
369
- catch {
370
- return undefined;
371
- }
372
- if (!request)
373
- return undefined;
374
- const schemaText = JSON.stringify(request.schema);
375
- if (schemaText === undefined || Buffer.byteLength(schemaText, "utf8") > HARD_MAX_ELICITATION_BYTES)
376
- return undefined;
377
- const reason = request.reason;
378
- if (reason !== undefined && Buffer.byteLength(reason, "utf8") > MAX_DECISION_REASON_BYTES)
379
- return { schema: request.schema };
380
- return { schema: request.schema, ...(reason !== undefined ? { reason } : {}) };
381
- }
382
- /** Elicitation payload check: bounded JSON object, schema-required keys, host validator when configured. */
383
- async function validateElicitationPayload(agent, state, target, payload, signal) {
384
- const invalid = (message) => new AgentDecisionError("ERR_PRISM_DECISION_INVALID", message);
385
- const text = JSON.stringify(payload);
386
- if (text === undefined || Buffer.byteLength(text, "utf8") > MAX_ELICITATION_BYTES) {
387
- throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Elicitation payload exceeds ${MAX_ELICITATION_BYTES} bytes`);
388
- }
389
- const schema = target.elicitationSchema;
390
- if (schema) {
391
- const required = schema.required;
392
- if (Array.isArray(required)) {
393
- for (const key of required) {
394
- if (typeof key === "string" && !Object.hasOwn(payload, key))
395
- throw invalid(`Elicitation payload missing required key ${key}`);
396
- }
397
- }
398
- if (agent.config.validator) {
399
- const tool = {
400
- name: target.scope.toolName ?? "elicitation",
401
- parameters: schema,
402
- execute: () => ({ toolCallId: "", name: "elicitation" }),
403
- };
404
- const context = { sessionId: state.sessionId, runId: state.runId, toolCallId: target.toolCallId ?? "elicitation", signal };
405
- const validation = await agent.config.validator(tool, payload, context);
406
- if (validation)
407
- throw invalid("Elicitation payload failed schema validation");
408
- }
409
- }
410
- // Tool-declared answer-shape validation, re-derived from the current registry (never persisted).
411
- const call = state.pendingCalls?.find((entry) => entry.call.id === target.toolCallId)?.call;
412
- const tool = call ? activeTools(agent.config.tools).registry.get(call.name) : undefined;
413
- const validate = tool?.elicitation && call
414
- ? safeToolElicitationValidate(tool, call.arguments, {
415
- sessionId: state.sessionId,
416
- runId: state.runId,
417
- toolCallId: target.toolCallId ?? "elicitation",
418
- signal,
419
- })
420
- : undefined;
421
- if (validate) {
422
- try {
423
- validate(payload);
424
- }
425
- catch (error) {
426
- throw invalid(error instanceof Error ? error.message : "Elicitation payload rejected by tool validation");
427
- }
428
- }
429
- }
430
- function safeToolElicitationValidate(tool, args, context) {
431
- try {
432
- return tool.elicitation(args, context)?.validate;
433
- }
434
- catch {
435
- return undefined;
436
- }
437
- }
438
- async function executePreparedAgentRunResume(prepared, signal) {
439
- if (prepared.kind === "deny") {
440
- await prepared.session.recordDurableDenial(prepared.result.runId, prepared.interruption, prepared.version, prepared.ownership);
441
- return prepared.result;
442
- }
443
- if (prepared.kind === "resuspend") {
444
- await prepared.session.recordDurableResumption(prepared.result.runId, prepared.interruption, prepared.version, prepared.ownership);
445
- return prepared.result;
446
- }
447
- return prepared.session.resumeDurable(prepared.state, prepared.runState, prepared.ownership, signal, prepared.decisions);
448
- }
449
- class AgentRunSuspended extends Error {
450
- state;
451
- interruption;
452
- code = "ERR_PRISM_AGENT_RUN_SUSPENDED";
453
- constructor(state, interruption) {
454
- super("Agent run suspended");
455
- this.state = state;
456
- this.interruption = interruption;
457
- this.name = "AgentRunSuspended";
458
- }
459
- }
460
- /** Root-visible nested approval id: hashed so it stays bounded and non-enumerating at any depth. */
461
- function nestedApprovalId(runId, childApprovalId) {
462
- return `sub_${createHash("sha256").update(`${runId}:${childApprovalId}`).digest("hex")}`;
463
- }
464
- function pathsEqual(a, b) {
465
- if (a === undefined || b === undefined)
466
- return a === b;
467
- return a.length === b.length && a.every((value, index) => value === b[index]);
468
- }
469
- function decisionScopesEqual(a, b) {
470
- if (a.toolName !== b.toolName || a.argumentsHash !== b.argumentsHash || a.effectKind !== b.effectKind || a.identity !== b.identity)
471
- return false;
472
- if (a.actionConstraints === undefined || b.actionConstraints === undefined)
473
- return a.actionConstraints === b.actionConstraints;
474
- const keys = Object.keys(a.actionConstraints);
475
- return (keys.length === Object.keys(b.actionConstraints).length &&
476
- keys.every((key) => key in b.actionConstraints &&
477
- canonicalToolEffectJson(a.actionConstraints[key]) === canonicalToolEffectJson(b.actionConstraints[key])));
478
- }
479
- function nestedOutcomeToolResult(outcome, toolCallId, name) {
480
- return outcome.status === "completed"
481
- ? { toolCallId, name, ...(outcome.value !== undefined ? { value: outcome.value } : {}) }
482
- : { toolCallId, name, error: { code: outcome.code, message: outcome.message } };
483
- }
484
- class RuntimeAgentSession {
485
- id;
486
- agent;
487
- metadata;
488
- store;
489
- subscribers = new Set();
490
- currentLeafId;
491
- history = [];
492
- activeRun;
493
- activeRunId;
494
- activeProviderTurnAbort;
495
- pendingSoftInterrupt = false;
496
- pendingSteers = [];
497
- pendingSteerBytes = 0;
498
- activeRedactor;
499
- activeProvider;
500
- activeLedger;
501
- activeEffectStore;
502
- activeOwnership;
503
- activeIdentity;
504
- activeIdempotencyKey;
505
- activeGuardrails;
506
- activeMetadata;
507
- activeLimits;
508
- activeLimitOutputBuffer = false;
509
- activeDurable;
510
- activeLoop;
511
- /** Gated calls of the current tool round awaiting one collected suspension. */
512
- activeGatedRound;
513
- activeLoopTurn = 1;
514
- loadedSkills = createLoadedSkillSet();
515
- /** Plan 015 Task 4: re-add persisted loaded-skill names (names only; bodies re-resolve on demand). */
516
- restoreLoadedSkills(names) {
517
- for (const name of names)
518
- this.loadedSkills.add(name);
519
- }
520
- ledgerChain = Promise.resolve();
521
- ledgerFailure;
522
- snapshotGeneration = 0;
523
- snapshotCache;
524
- constructor(config) {
525
- this.id = config.id ?? randomId("session");
526
- this.agent = config.agent;
527
- this.metadata = config.metadata;
528
- this.store = config.store ?? config.agent.config.store ?? createMemorySessionStore();
529
- this.currentLeafId = config.leafId;
530
- }
531
- get leafId() {
532
- return this.currentLeafId;
533
- }
534
- subscribe(options = {}) {
535
- const subscriber = new EventSubscriber(this.id, options, () => this.subscribers.delete(subscriber));
536
- this.subscribers.add(subscriber);
537
- return subscriber;
538
- }
539
- async run(input, options = {}) {
540
- return this.runInternal(input, options, randomId("run"));
541
- }
542
- steer(input, options = {}) {
543
- if (!this.activeRun || !this.activeRunId)
544
- throw new Error("Agent session has no active run to steer");
545
- const messages = inputToMessages(input).map((message) => this.redact(message));
546
- if (messages.length === 0)
547
- throw new Error("steer requires non-empty input");
548
- let addBytes = 0;
549
- for (const message of messages)
550
- addBytes += messageTextBytes(message);
551
- if (this.pendingSteers.length + messages.length > DEFAULT_MAX_PENDING_STEERS) {
552
- throw new Error(`steer queue exceeds max pending messages (${DEFAULT_MAX_PENDING_STEERS})`);
553
- }
554
- if (this.pendingSteerBytes + addBytes > DEFAULT_MAX_PENDING_STEER_BYTES) {
555
- throw new Error(`steer queue exceeds max pending bytes (${DEFAULT_MAX_PENDING_STEER_BYTES})`);
556
- }
557
- this.pendingSteers.push(...messages);
558
- this.pendingSteerBytes += addBytes;
559
- this.emit({ type: "queue_updated", sessionId: this.id, runId: this.activeRunId, size: this.pendingSteers.length });
560
- if (options.softInterrupt) {
561
- if (this.activeProviderTurnAbort)
562
- this.activeProviderTurnAbort.abort(new SteerSoftInterrupt());
563
- else
564
- this.pendingSoftInterrupt = true;
565
- }
566
- }
567
- async resumeDurable(state, runState, ownership, signal, decisions) {
568
- return this.runInternal(state.input ?? [], { runState, ownership, signal }, state.runId, {
569
- options: runState,
570
- state,
571
- version: state.version,
572
- decisions,
573
- });
574
- }
575
- async recordDurableResumption(runId, interruption, version, ownership) {
576
- this.activeLedger = this.agent.config.runLedger;
577
- this.activeOwnership = ownership ?? this.agent.config.ownership;
578
- this.activeRedactor = this.agent.config.redactor;
579
- try {
580
- this.emit({ type: "agent_suspended", sessionId: this.id, runId, interruption, version });
581
- await this.drainLedger();
582
- }
583
- finally {
584
- this.activeLedger = undefined;
585
- this.activeOwnership = undefined;
586
- this.activeRedactor = undefined;
587
- this.closeSubscribers();
588
- }
589
- }
590
- async recordDurableDenial(runId, interruption, version, ownership) {
591
- this.activeLedger = this.agent.config.runLedger;
592
- this.activeOwnership = ownership ?? this.agent.config.ownership;
593
- this.activeRedactor = this.agent.config.redactor;
594
- try {
595
- this.emit({ type: "agent_denied", sessionId: this.id, runId, interruption, version });
596
- await this.drainLedger();
597
- }
598
- finally {
599
- this.activeLedger = undefined;
600
- this.activeOwnership = undefined;
601
- this.activeRedactor = undefined;
602
- this.closeSubscribers();
603
- }
604
- }
605
- async runInternal(input, options, runId, resumed) {
606
- if (this.agent.config.secure &&
607
- (options.redactor !== undefined ||
608
- options.ownership !== undefined ||
609
- options.validate !== undefined ||
610
- options.effectStore !== undefined ||
611
- options.runState !== undefined)) {
612
- throw new AgentRunStateError("Secure agent defaults cannot be replaced per run");
613
- }
614
- const requestedLimits = options.maxToolRounds === undefined
615
- ? options.limits
616
- : { ...options.limits, maxToolRounds: Math.min(options.maxToolRounds, options.limits?.maxToolRounds ?? options.maxToolRounds) };
617
- const resolvedLimits = resolveRunLimits(this.agent.config.limits, requestedLimits);
618
- const durableOptions = options.runState ?? this.agent.config.runState;
619
- if (this.agent.config.runState && options.runState && this.agent.config.runState !== options.runState) {
620
- throw new AgentRunStateError("RunOptions cannot replace agent durable run-state configuration");
621
- }
622
- if (durableOptions) {
623
- validateRunStateOptions(durableOptions);
624
- if (options.model || options.guardrails || options.loop || options.effectStore)
625
- throw new AgentRunStateError("Durable runs require model, guardrails, loop, and effect store on AgentConfig for fingerprinting");
626
- const configuredLoop = this.agent.config.loop;
627
- if (configuredLoop && !isDurableLoop(configuredLoop)) {
628
- throw new AgentLoopStateError("ERR_PRISM_LOOP_NOT_DURABLE", "Custom AgentLoopStrategy on a durable run requires snapshot and restore hooks");
629
- }
630
- }
631
- if (this.activeRun) {
632
- const error = new Error("Agent session already has an active run");
633
- this.emit({ type: "error", sessionId: this.id, runId, error: errorToErrorInfo(error) });
634
- throw error;
635
- }
636
- const controller = new AbortController();
637
- const cleanupSignal = bridgeAbort(options.signal, controller);
638
- this.activeRun = controller;
639
- this.activeRunId = runId;
640
- this.pendingSteers = [];
641
- this.pendingSteerBytes = 0;
642
- this.pendingSoftInterrupt = false;
643
- this.activeRedactor = options.redactor ?? this.agent.config.redactor;
644
- this.activeLedger = options.runLedger ?? this.agent.config.runLedger;
645
- this.activeEffectStore = options.effectStore ?? this.agent.config.effectStore;
646
- this.activeOwnership = options.ownership ?? this.agent.config.ownership;
647
- this.activeIdentity = resolveRunIdentity(options.identity, this.agent.config.identity, this.activeOwnership);
648
- if (this.activeIdentity && !this.activeOwnership)
649
- this.activeOwnership = ownershipFromIdentity(this.activeIdentity);
650
- this.activeIdempotencyKey = options.idempotencyKey ?? this.agent.config.idempotencyKey;
651
- this.activeGuardrails = mergeGuardrails(this.agent.config.guardrails, options.guardrails);
652
- this.activeDurable = resumed ?? (durableOptions ? { options: durableOptions, version: 0 } : undefined);
653
- this.activeGatedRound = undefined;
654
- if (resumed)
655
- this.invalidateSnapshot();
656
- const model = options.model ?? this.agent.config.model;
657
- const startedAt = new Date().toISOString();
658
- let runError;
659
- let runStatus = "succeeded";
660
- const runUsage = createUsageAccumulator();
661
- let usage;
662
- const metadata = {
663
- ...this.agent.config.metadata,
664
- ...this.metadata,
665
- ...options.metadata,
666
- ...(this.activeIdentity ? identityTelemetryAttributes(this.activeIdentity) : {}),
667
- };
668
- this.activeMetadata = metadata;
669
- const limits = new RunLimitTracker(resolvedLimits, {
670
- onExceeded: (breach) => {
671
- this.emit({ type: "run_limit_exceeded", sessionId: this.id, runId, breach });
672
- controller.abort(new RunLimitError(breach));
673
- },
674
- snapshot: resumed?.state?.counters,
675
- deadlineAt: resumed?.state?.deadlineAt,
676
- });
677
- this.activeLimits = limits;
678
- this.activeLimitOutputBuffer = [this.agent.config.limits, requestedLimits].some((value) => value?.maxOutputTokens !== undefined || value?.maxTotalTokens !== undefined || value?.maxCost !== undefined);
679
- try {
680
- this.resolveRunProvider(options);
681
- throwIfAborted(controller.signal);
682
- this.emit({ type: "agent_started", sessionId: this.id, runId });
683
- if (resumed)
684
- this.emit({ type: "agent_resumed", sessionId: this.id, runId, version: resumed.version });
685
- const startRecord = {
686
- id: runId,
687
- sessionId: this.id,
688
- branchId: this.currentLeafId,
689
- model,
690
- provider: model.provider,
691
- idempotencyKey: this.activeIdempotencyKey,
692
- status: "running",
693
- startedAt,
694
- ...this.activeOwnership,
695
- };
696
- await this.activeLedger?.appendRun(redactRunLedgerRecord(startRecord, this.activeRedactor));
697
- await this.rebuildHistory();
698
- const { registry, tools } = activeTools(this.agent.config.tools);
699
- const activeSkills = this.resolveRunSkills(options, tools);
700
- if (options.model && JSON.stringify(options.model) !== JSON.stringify(this.agent.config.model)) {
701
- await this.appendEntry(createSessionEntry({
702
- sessionId: this.id,
703
- parentId: this.currentLeafId,
704
- runId,
705
- kind: "model_change",
706
- previousModel: this.agent.config.model,
707
- model: options.model,
708
- }));
709
- }
710
- const inputMessages = inputToMessages(input).map((message) => this.redact(message));
711
- const inputGuardrails = await runGuardrails({
712
- stage: "input",
713
- guardrails: this.activeGuardrails,
714
- value: inputMessages,
715
- context: { sessionId: this.id, runId, metadata, signal: controller.signal },
716
- redactor: this.activeRedactor,
717
- emit: (event) => this.emit(event),
718
- });
719
- // Input-guardrail decision table:
720
- // - interrupt + durable + fresh run → suspend for approval.
721
- // - interrupt + durable + resumed run → proceed: resuming IS the operator approval.
722
- // - interrupt without durable, or block/tripwire → fail via assertGuardrailsAllowed.
723
- const approvedByResume = resumed !== undefined && inputGuardrails.terminal?.action === "interrupt" && this.activeDurable !== undefined;
724
- if (inputGuardrails.terminal?.action === "interrupt" && this.activeDurable && !approvedByResume) {
725
- const interruption = { kind: "input_guardrail", reason: inputGuardrails.terminal.reason ?? "Input requires approval" };
726
- throw new AgentRunSuspended(await this.suspendDurable({ runId, model, limits, interruption, messages: inputMessages }), interruption);
727
- }
728
- if (inputGuardrails.terminal && !approvedByResume)
729
- assertGuardrailsAllowed(inputGuardrails);
730
- for (const message of inputMessages)
731
- await this.appendMessage(message, runId);
732
- await this.autoCompact(runId, options, controller.signal, inputMessages);
733
- const maxToolRounds = resolvedLimits.maxToolRounds;
734
- const systemInstructions = composeSystemPrompt(mergeSystemPromptConfig(this.agent.config.systemPrompt, options.systemPrompt), {
735
- base: this.agent.config.instructions,
736
- });
737
- const contextProviders = [
738
- ...(this.agent.config.context ?? []),
739
- // ponytail: skill context after host context; no per-skill token budget yet.
740
- ...activeSkills.flatMap((skill) => skill.context ?? []),
741
- ];
742
- const providerOptions = resolveRunProviderOptions(options, this.agent.config);
743
- assertStructuredOutputRequestSupported(options.model ?? this.agent.config.model, providerOptions);
744
- const validate = options.validate ?? this.agent.config.validator;
745
- // ponytail: RunOptions.instructionInjectors overrides AgentConfig.instructionInjectors (mirrors validate/loop).
746
- const instructionInjectors = options.instructionInjectors ?? this.agent.config.instructionInjectors ?? [];
747
- const inputLayout = options.inputLayout ?? this.agent.config.inputLayout;
748
- const loop = resolveLoop(options, this.agent.config);
749
- this.activeLoop = loop;
750
- const toolConcurrency = resolveToolConcurrency(options, this.agent.config);
751
- this.activeLoopTurn = 1;
752
- const recordProviderUsage = async (turnUsage, turn, attempt) => {
753
- limits.recordUsage(turnUsage);
754
- if (!turnUsage)
755
- return;
756
- runUsage.add(turnUsage);
757
- if (!this.activeLedger)
758
- return;
759
- const usageRecord = {
760
- id: randomId("usage"),
761
- sessionId: this.id,
762
- runId,
763
- scope: "provider_turn",
764
- turn,
765
- attempt,
766
- usage: turnUsage,
767
- recordedAt: new Date().toISOString(),
768
- ...this.activeOwnership,
769
- };
770
- await this.activeLedger.appendUsage(redactRunLedgerRecord(usageRecord, this.activeRedactor));
771
- };
772
- // Suspends the run when a round recorded gated calls. Fires at the next provider turn
773
- // (generate) or after the loop ends, so ungated round siblings dispatch first.
774
- const suspendGatedRound = async () => {
775
- const gated = this.activeGatedRound;
776
- if (!gated?.size)
777
- return;
778
- const entries = [...gated.values()];
779
- const decisions = entries.map((gatedCall) => gatedCall.decision);
780
- const single = decisions.length === 1 ? decisions[0] : undefined;
781
- const interruption = {
782
- kind: single?.kind === "elicitation" ? "elicitation" : "tool_approval",
783
- reason: single ? single.reason : `${decisions.length} tool side effects require approval`,
784
- ...(single?.toolCallId ? { toolCallId: single.toolCallId } : {}),
785
- ...(single?.scope.toolName ? { toolName: single.scope.toolName } : {}),
786
- pendingDecisions: decisions,
787
- };
788
- throw new AgentRunSuspended(await this.suspendDurable({ runId, model, limits, interruption, pendingCalls: entries.map((gatedCall) => gatedCall.entry) }), interruption);
789
- };
790
- // Suspends on a nested run's pending decisions, merging any still-ready round entries
791
- // (with their decisions attached) so a nested signal mid-replay never drops own work.
792
- const suspendNested = async (nested) => {
793
- const state = this.activeDurable?.state;
794
- const kept = (state?.pendingCalls ?? [])
795
- .filter((entry) => entry.status === "ready")
796
- .map((entry) => {
797
- const decision = entry.decision ?? resumed?.decisions?.get(entry.approvalId);
798
- return decision ? { ...entry, decision } : entry;
799
- });
800
- const gated = [...(this.activeGatedRound?.values() ?? [])];
801
- const pendingCalls = [
802
- ...kept,
803
- ...gated.map((gatedCall) => gatedCall.entry),
804
- { call: nested.toolCall, status: "ready", approvalId: `nr_${nested.entry.runId}` },
805
- ];
806
- const keptIds = new Set(kept.map((entry) => entry.approvalId));
807
- const decisions = [
808
- ...(state?.interruption?.pendingDecisions ?? []).filter((pending) => keptIds.has(pending.approvalId)),
809
- ...gated.map((gatedCall) => gatedCall.decision),
810
- ...nested.pending,
811
- ];
812
- if (decisions.length > HARD_MAX_PENDING_DECISIONS) {
813
- throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Pending decisions exceed ${HARD_MAX_PENDING_DECISIONS} per run`);
814
- }
815
- const single = decisions.length === 1 ? decisions[0] : undefined;
816
- const interruption = {
817
- kind: single?.kind ?? "tool_approval",
818
- reason: single ? single.reason : `${decisions.length} approval request(s) need a decision`,
819
- ...(single?.toolCallId ? { toolCallId: single.toolCallId } : {}),
820
- ...(single?.scope.toolName ? { toolName: single.scope.toolName } : {}),
821
- pendingDecisions: decisions,
822
- };
823
- throw new AgentRunSuspended(await this.suspendDurable({
824
- runId,
825
- model,
826
- limits,
827
- interruption,
828
- pendingCalls,
829
- nestedRuns: [...(state?.nestedRuns ?? []), nested.entry],
830
- }), interruption);
831
- };
832
- // Converts a nested-run suspension into either root-visible pending decisions (hashed,
833
- // attributed approval ids) or — when a root sticky covers every surfaced decision and a
834
- // hook is available — an immediate child resume loop ending in a synthesized tool result.
835
- const applyNestedRun = async (input) => {
836
- let current = input.pending;
837
- // ponytail: sticky auto-apply only when the whole surfaced set is covered; mixed sets
838
- // surface to the host. Hook round-trips capped at 4 per suspension event.
839
- for (let depth = 0;; depth += 1) {
840
- const attributed = current.map((decision) => {
841
- const id = nestedApprovalId(input.ref.runId, decision.approvalId);
842
- return {
843
- id,
844
- childApprovalId: decision.approvalId,
845
- decision: { ...decision, approvalId: id, attribution: decision.attribution ?? { path: input.path } },
846
- };
847
- });
848
- if (attributed.some(({ decision }) => (decision.attribution?.path.length ?? 0) > MAX_ATTRIBUTION_DEPTH)) {
849
- throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Attribution path exceeds ${MAX_ATTRIBUTION_DEPTH} entries`);
850
- }
851
- const allSticky = attributed.length > 0 && attributed.every(({ decision }) => this.matchNestedSticky(decision) !== undefined);
852
- if (!input.hook || !allSticky || depth >= 4) {
853
- return {
854
- entry: {
855
- runId: input.ref.runId,
856
- ...(input.ref.sessionId ? { sessionId: input.ref.sessionId } : {}),
857
- toolCallId: input.toolCall.id,
858
- path: attributed[0]?.decision.attribution?.path ?? input.path,
859
- approvals: attributed.map(({ id, childApprovalId }) => ({ id, childApprovalId })),
860
- },
861
- pending: attributed.map(({ decision }) => decision),
862
- };
863
- }
864
- const outcome = await input.hook({ ref: input.ref, toolCallId: input.toolCall.id, path: input.path }, attributed.map(({ childApprovalId, decision }) => {
865
- const sticky = this.matchNestedSticky(decision);
866
- return {
867
- approvalId: childApprovalId,
868
- outcome: sticky.outcome,
869
- ...(sticky.reason !== undefined ? { reason: sticky.reason } : {}),
870
- };
871
- }));
872
- if (outcome.status === "suspended") {
873
- current = outcome.pendingDecisions;
874
- continue;
875
- }
876
- return { toolResult: nestedOutcomeToolResult(outcome, input.toolCall.id, input.toolCall.name) };
877
- }
878
- };
879
- // ponytail: LoopContext binds existing private helpers; loop orchestrates only.
880
- let assembledTurn = false;
881
- let artifactFinished = false;
882
- let artifactFailedInfo;
883
- const ctx = {
884
- sessionId: this.id,
885
- runId,
886
- metadata,
887
- signal: controller.signal,
888
- history: this.history,
889
- input,
890
- inputMessages,
891
- maxToolRounds,
892
- toolConcurrency,
893
- restoredLoopState: resumed?.state?.loopState?.snapshot,
894
- assemble: async (nextInput, toolResults, turn) => {
895
- limits.charge("maxTurns");
896
- const request = await assembleProviderInput({
897
- model: options.model ?? this.agent.config.model,
898
- input: nextInput,
899
- history: this.history,
900
- summaries: (await this.snapshot()).summaries,
901
- toolResults: toolResults ?? [],
902
- turn,
903
- instructionInjectors,
904
- inputLayout,
905
- systemInstructions,
906
- inputBuilder: this.agent.config.inputBuilder,
907
- promptBuilder: this.agent.config.promptBuilder,
908
- contextProviders,
909
- skills: activeSkills,
910
- skillsDisclosure: resolveSkillsDisclosure(options.skillsDisclosure, this.agent.config.skillsDisclosure),
911
- toolResultFold: resolveToolResultFold(options.toolResultFold, this.agent.config.toolResultFold),
912
- loadedSkills: this.loadedSkills,
913
- tools,
914
- resourceLoader: this.agent.config.resourceLoader,
915
- permission: this.agent.config.permission,
916
- trust: this.agent.config.trust,
917
- providerOptions,
918
- redactor: this.activeRedactor,
919
- middleware: this.agent.config.middleware,
920
- sessionId: this.id,
921
- runId,
922
- metadata,
923
- signal: controller.signal,
924
- });
925
- assembledTurn = true;
926
- return request;
927
- },
928
- chargeToolRound: (calls) => {
929
- if (calls.length > 0)
930
- limits.charge("maxToolRounds");
931
- const durable = this.activeDurable;
932
- if (!durable || !durable.options.interruptBeforeTool || calls.length === 0)
933
- return;
934
- // Round-level gate: record one pending decision per uncovered gated call. Ungated
935
- // and sticky-allowed calls still dispatch; the suspension fires at the next provider
936
- // turn (or run end) via suspendGatedRound. Per-call beforeExecute is the backstop
937
- // for loops that dispatch without charging a round.
938
- for (const call of calls) {
939
- if (this.matchStickyDecision(call, registry))
940
- continue;
941
- const approvalId = randomId("approval");
942
- this.activeGatedRound ??= new Map();
943
- this.activeGatedRound.set(call.id, {
944
- entry: { call, status: "ready", approvalId },
945
- decision: this.buildPendingDecision(call, approvalId, registry, runId, metadata, controller.signal),
946
- });
947
- }
948
- if (this.activeGatedRound && this.activeGatedRound.size > DEFAULT_MAX_PENDING_DECISIONS) {
949
- throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Pending decisions exceed ${DEFAULT_MAX_PENDING_DECISIONS} per run`);
950
- }
951
- },
952
- generate: async (request) => {
953
- await suspendGatedRound();
954
- if (!assembledTurn)
955
- limits.charge("maxTurns");
956
- assembledTurn = false;
957
- const policyResult = await this.applyProviderRequestPolicies(request, runId, options, metadata, controller.signal);
958
- const middlewareRequest = (await this.agent.config.middleware?.run("provider_request", policyResult.request)) ?? policyResult.request;
959
- try {
960
- return await this.generateWithRetry(this.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, this.activeLoopTurn, recordProviderUsage);
961
- }
962
- catch (error) {
963
- if (isSteerSoftInterrupt(error)) {
964
- return { content: [], calls: [], started: false, usage: undefined };
965
- }
966
- throw error;
967
- }
968
- },
969
- isToolCallExclusive: (call) => registry.get(call.name)?.exclusive === true,
970
- dispatchToolCall: async (call) => {
971
- const sticky = this.matchStickyDecision(call, registry);
972
- if (sticky?.outcome === "reject_for_run") {
973
- return {
974
- toolCallId: call.id,
975
- name: call.name,
976
- error: { code: "approval_rejected", message: sticky.reason ?? "Rejected for this run" },
977
- };
978
- }
979
- if (this.activeGatedRound?.has(call.id)) {
980
- // Gated this round: never dispatched. The marker is skipped by
981
- // dispatchToolCallsInOrder so the transcript stays free of phantom results.
982
- return { toolCallId: call.id, name: call.name, metadata: { approvalPending: true } };
983
- }
984
- try {
985
- return await dispatchToolCall({
986
- call,
987
- registry,
988
- context: {
989
- sessionId: this.id,
990
- runId,
991
- toolCallId: call.id,
992
- signal: controller.signal,
993
- metadata: {
994
- ...metadata,
995
- loadedSkills: this.loadedSkills,
996
- activeTools: tools,
997
- activeSkillNames: activeSkills.map((skill) => skill.name),
998
- },
999
- identity: this.activeIdentity,
1000
- },
1001
- middleware: this.agent.config.middleware,
1002
- emit: (event) => this.emit(event),
1003
- permission: this.agent.config.permission,
1004
- trust: this.agent.config.trust,
1005
- redactor: this.activeRedactor,
1006
- ledger: this.activeLedger,
1007
- effectStore: this.activeEffectStore,
1008
- ownership: this.activeOwnership,
1009
- identity: this.activeIdentity,
1010
- guardrails: this.activeGuardrails,
1011
- limitTracker: limits,
1012
- beforeExecute: async (mediatedCall) => {
1013
- const durable = this.activeDurable;
1014
- if (!durable)
1015
- return;
1016
- const pendingCalls = durable.state?.pendingCalls;
1017
- const matched = pendingCalls?.find((entry) => entry.call.id === mediatedCall.id && entry.status === "ready");
1018
- if (matched) {
1019
- await this.persistDurable({
1020
- ...durable.state,
1021
- status: "running",
1022
- pendingCalls: pendingCalls.map((entry) => (entry === matched ? { ...entry, status: "dispatched" } : entry)),
1023
- interruption: undefined,
1024
- });
1025
- return;
1026
- }
1027
- const pending = durable.state?.pending;
1028
- if (pending?.call.id === mediatedCall.id && pending.status === "ready") {
1029
- await this.persistDurable({
1030
- ...durable.state,
1031
- status: "running",
1032
- pending: { ...pending, status: "dispatched" },
1033
- interruption: undefined,
1034
- });
1035
- return;
1036
- }
1037
- if (!durable.options.interruptBeforeTool)
1038
- return;
1039
- if (this.matchStickyDecision(mediatedCall, registry)?.outcome === "allow_for_run")
1040
- return;
1041
- // Backstop for loops that dispatch without awaiting chargeToolRound: suspend on
1042
- // the first uncovered gated call with a single pending decision.
1043
- const approvalId = randomId("approval");
1044
- const decision = this.buildPendingDecision(mediatedCall, approvalId, registry, runId, metadata, controller.signal);
1045
- const interruption = {
1046
- kind: "tool_approval",
1047
- reason: decision.reason,
1048
- toolCallId: mediatedCall.id,
1049
- toolName: mediatedCall.name,
1050
- pendingDecisions: [decision],
1051
- };
1052
- throw new AgentRunSuspended(await this.suspendDurable({
1053
- runId,
1054
- model,
1055
- limits,
1056
- interruption,
1057
- pending: { call: mediatedCall, status: "ready" },
1058
- pendingCalls: [{ call: mediatedCall, status: "ready", approvalId }],
1059
- }), interruption);
1060
- },
1061
- // ponytail: RunOptions wins; array-compose deferred (roadmap: compose-later).
1062
- validate,
1063
- });
1064
- }
1065
- catch (error) {
1066
- // Link the suspension signal to the hosting call so the root suspension can
1067
- // synthesize this call's tool_result when the nested run later terminates.
1068
- if (error instanceof AgentDelegationSuspendedError && !error.toolCall)
1069
- error.toolCall = call;
1070
- throw error;
1071
- }
1072
- },
1073
- appendMessage: (message) => this.appendMessage(message, runId),
1074
- hasPendingSteers: () => this.pendingSteers.length > 0,
1075
- applyPendingSteers: () => this.applyPendingSteers(runId, metadata, controller.signal),
1076
- emit: (event) => {
1077
- if (event.type === "turn_started")
1078
- this.activeLoopTurn = event.turn;
1079
- if (event.type === "artifact_finished")
1080
- artifactFinished = true;
1081
- if (event.type === "artifact_failed") {
1082
- const first = event.result.errors?.[0];
1083
- const reason = event.result.metadata?.reason;
1084
- artifactFailedInfo = {
1085
- message: first?.message ?? "artifact failed",
1086
- code: typeof reason === "string" || typeof reason === "number" ? reason : "artifact_failed",
1087
- };
1088
- }
1089
- this.emit(event);
1090
- },
1091
- };
1092
- const replayToolResult = async (result) => {
1093
- await ctx.appendMessage({
1094
- role: "tool",
1095
- content: [
1096
- { type: "tool_result", toolCallId: result.toolCallId, name: result.name, result: result.value, error: result.error },
1097
- ...(result.content ?? []),
1098
- ],
1099
- metadata: result.metadata,
1100
- });
1101
- };
1102
- // Handles a nested-run suspension signal: sticky auto-apply appends a synthesized
1103
- // tool_result and returns; anything else re-suspends the root (throws AgentRunSuspended).
1104
- const handleNestedSignal = async (error) => {
1105
- const durableOptions = this.activeDurable?.options;
1106
- if (!durableOptions || !error.toolCall) {
1107
- throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Nested-run suspension requires durable run state and a hosting tool call");
1108
- }
1109
- if (error.pendingDecisions.length === 0) {
1110
- throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Nested-run suspension carried no pending decisions");
1111
- }
1112
- const applied = await applyNestedRun({
1113
- ref: error.ref,
1114
- toolCall: error.toolCall,
1115
- path: error.path ?? [],
1116
- pending: error.pendingDecisions,
1117
- hook: durableOptions.resumeNestedRun,
1118
- });
1119
- if ("toolResult" in applied) {
1120
- await replayToolResult(applied.toolResult);
1121
- return;
1122
- }
1123
- await suspendNested({ entry: applied.entry, toolCall: error.toolCall, pending: applied.pending });
1124
- };
1125
- // Route decided nested-run approvals back to their children before replaying own calls.
1126
- // Undecided or re-suspended children re-suspend the root with the surfaced remainder.
1127
- let resumePendingCalls = resumed?.state?.pendingCalls;
1128
- if (resumed?.state?.nestedRuns?.length) {
1129
- const nestedRuns = resumed.state.nestedRuns;
1130
- const hook = this.activeDurable?.options.resumeNestedRun;
1131
- const oldPending = resumed.state.interruption?.pendingDecisions ?? [];
1132
- const nestedApprovalIds = new Set(nestedRuns.flatMap((entry) => entry.approvals.map((approval) => approval.id)));
1133
- const remainingNested = [];
1134
- const surfacedPending = [];
1135
- const resolvedToolCallIds = new Set();
1136
- for (const entry of nestedRuns) {
1137
- const grouped = [];
1138
- for (const approval of entry.approvals) {
1139
- const decision = entry.decisions?.[approval.id] ?? resumed.decisions?.get(approval.id);
1140
- if (decision)
1141
- grouped.push({ ...decision, approvalId: approval.childApprovalId });
1142
- }
1143
- if (grouped.length === 0) {
1144
- remainingNested.push(entry);
1145
- surfacedPending.push(...oldPending.filter((pending) => entry.approvals.some((approval) => approval.id === pending.approvalId)));
1146
- continue;
1147
- }
1148
- if (!hook) {
1149
- throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Nested-run decisions require a resumeNestedRun hook");
1150
- }
1151
- const toolCall = resumePendingCalls?.find((pending) => pending.call.id === entry.toolCallId)?.call;
1152
- if (!toolCall)
1153
- throw new AgentRunStateError("Nested run link is missing its tool call");
1154
- const ref = { runId: entry.runId, ...(entry.sessionId ? { sessionId: entry.sessionId } : {}) };
1155
- const outcome = await hook({ ref, toolCallId: entry.toolCallId, path: entry.path }, grouped);
1156
- if (outcome.status !== "suspended") {
1157
- await replayToolResult(nestedOutcomeToolResult(outcome, entry.toolCallId, toolCall.name));
1158
- resolvedToolCallIds.add(entry.toolCallId);
1159
- continue;
1160
- }
1161
- const applied = await applyNestedRun({ ref, toolCall, path: entry.path, pending: outcome.pendingDecisions, hook });
1162
- if ("toolResult" in applied) {
1163
- await replayToolResult(applied.toolResult);
1164
- resolvedToolCallIds.add(entry.toolCallId);
1165
- }
1166
- else {
1167
- remainingNested.push(applied.entry);
1168
- surfacedPending.push(...applied.pending);
1169
- }
1170
- }
1171
- resumePendingCalls = resumePendingCalls
1172
- ?.filter((entry) => !resolvedToolCallIds.has(entry.call.id))
1173
- .map((entry) => {
1174
- const decision = resumed.decisions?.get(entry.approvalId);
1175
- return decision && !entry.decision ? { ...entry, decision } : entry;
1176
- });
1177
- if (this.activeDurable?.state) {
1178
- this.activeDurable.state = {
1179
- ...this.activeDurable.state,
1180
- pendingCalls: resumePendingCalls?.length ? resumePendingCalls : undefined,
1181
- nestedRuns: remainingNested.length ? remainingNested : undefined,
1182
- };
1183
- }
1184
- const remainingOwn = oldPending.filter((pending) => !nestedApprovalIds.has(pending.approvalId) &&
1185
- !resumed.decisions?.has(pending.approvalId) &&
1186
- !resumePendingCalls?.some((entry) => entry.approvalId === pending.approvalId && entry.decision !== undefined));
1187
- if (remainingOwn.length > 0 || surfacedPending.length > 0) {
1188
- const pendingDecisions = [...remainingOwn, ...surfacedPending];
1189
- const single = pendingDecisions.length === 1 ? pendingDecisions[0] : undefined;
1190
- const interruption = {
1191
- kind: single?.kind ?? "tool_approval",
1192
- reason: `${pendingDecisions.length} approval request(s) remain`,
1193
- ...(single?.toolCallId ? { toolCallId: single.toolCallId } : {}),
1194
- ...(single?.scope.toolName ? { toolName: single.scope.toolName } : {}),
1195
- pendingDecisions,
1196
- };
1197
- throw new AgentRunSuspended(await this.suspendDurable({
1198
- runId,
1199
- model,
1200
- limits,
1201
- interruption,
1202
- pendingCalls: resumePendingCalls,
1203
- nestedRuns: remainingNested,
1204
- }), interruption);
1205
- }
1206
- }
1207
- if (resumePendingCalls?.length) {
1208
- for (const entry of resumePendingCalls) {
1209
- if (entry.status !== "ready")
1210
- continue;
1211
- const decision = entry.decision ?? resumed?.decisions?.get(entry.approvalId);
1212
- if (decision && (decision.outcome === "reject_once" || decision.outcome === "reject_for_run")) {
1213
- await replayToolResult({
1214
- toolCallId: entry.call.id,
1215
- name: entry.call.name,
1216
- error: { code: "approval_rejected", message: decision.reason ?? "Approval rejected" },
1217
- });
1218
- continue;
1219
- }
1220
- if (decision?.elicitation !== undefined) {
1221
- // Elicitation acceptance resolves the suspended call with the validated payload.
1222
- await replayToolResult({ toolCallId: entry.call.id, name: entry.call.name, value: decision.elicitation });
1223
- continue;
1224
- }
1225
- const call = decision?.modifiedArguments ? { ...entry.call, arguments: decision.modifiedArguments } : entry.call;
1226
- try {
1227
- await replayToolResult(await ctx.dispatchToolCall(call));
1228
- }
1229
- catch (error) {
1230
- if (!(error instanceof AgentDelegationSuspendedError))
1231
- throw error;
1232
- await handleNestedSignal(error);
1233
- }
1234
- }
1235
- }
1236
- else if (resumed?.state?.pending?.status === "ready") {
1237
- await replayToolResult(await ctx.dispatchToolCall(resumed.state.pending.call));
1238
- }
1239
- const resumedLoopState = resumed?.state?.loopState;
1240
- if (resumedLoopState) {
1241
- if (loop.name !== resumedLoopState.name || (loop.revision ?? "1") !== resumedLoopState.revision) {
1242
- throw new AgentLoopStateError("ERR_PRISM_LOOP_REVISION", `Loop ${resumedLoopState.name} revision ${resumedLoopState.revision} does not match the resumed durable run`);
1243
- }
1244
- loop.restore?.(resumedLoopState.snapshot);
1245
- }
1246
- let loopUsage;
1247
- while (true) {
1248
- try {
1249
- loopUsage = await loop.run(ctx);
1250
- await suspendGatedRound();
1251
- break;
1252
- }
1253
- catch (error) {
1254
- if (!(error instanceof AgentDelegationSuspendedError))
1255
- throw error;
1256
- await handleNestedSignal(error);
1257
- }
1258
- }
1259
- if (loop.name === "generate-validate-revise" && !artifactFinished) {
1260
- throw Object.assign(new Error(artifactFailedInfo?.message ?? "artifact loop ended without a validated artifact"), {
1261
- name: "ArtifactFailed",
1262
- code: artifactFailedInfo?.code ?? "artifact_failed",
1263
- });
1264
- }
1265
- usage = runUsage.value() ?? loopUsage;
1266
- if (usage && this.activeLedger) {
1267
- const usageRecord = {
1268
- id: randomId("usage"),
1269
- sessionId: this.id,
1270
- runId,
1271
- scope: "run_total",
1272
- usage,
1273
- recordedAt: new Date().toISOString(),
1274
- ...this.activeOwnership,
1275
- };
1276
- await this.activeLedger.appendUsage(redactRunLedgerRecord(usageRecord, this.activeRedactor));
1277
- }
1278
- await this.drainLedger();
1279
- const runState = this.activeDurable?.state
1280
- ? await this.persistDurable({
1281
- ...this.activeDurable.state,
1282
- status: "succeeded",
1283
- pending: undefined,
1284
- pendingCalls: undefined,
1285
- nestedRuns: undefined,
1286
- stickyDecisions: undefined,
1287
- interruption: undefined,
1288
- loopState: undefined,
1289
- })
1290
- : undefined;
1291
- this.emit({ type: "agent_finished", sessionId: this.id, runId, usage });
1292
- return this.buildRunResult({ runId, status: "succeeded", usage, runState });
1293
- }
1294
- catch (error) {
1295
- if (error instanceof AgentRunSuspended) {
1296
- runStatus = "suspended";
1297
- const version = error.state.version;
1298
- this.emit({ type: "agent_suspended", sessionId: this.id, runId, interruption: error.interruption, version });
1299
- return this.buildRunResult({ runId, status: "suspended", runState: error.state, interruption: error.interruption });
1300
- }
1301
- runError = errorToErrorInfo(error);
1302
- this.emit({ type: "error", sessionId: this.id, runId, error: runError });
1303
- const breach = error instanceof RunLimitError ? error.breach : limits.breach;
1304
- runStatus = breach ? "failed" : controller.signal.aborted ? "aborted" : "failed";
1305
- const runState = this.activeDurable?.state
1306
- ? await this.persistDurable({
1307
- ...this.activeDurable.state,
1308
- status: runStatus,
1309
- interruption: undefined,
1310
- loopState: undefined,
1311
- pendingCalls: undefined,
1312
- nestedRuns: undefined,
1313
- stickyDecisions: undefined,
1314
- })
1315
- : undefined;
1316
- const result = this.buildRunResult({
1317
- runId,
1318
- status: runStatus,
1319
- usage: runUsage.value() ?? usage,
1320
- limit: breach,
1321
- error: runError,
1322
- abortReason: !breach && controller.signal.aborted ? String(controller.signal.reason) : undefined,
1323
- runState,
1324
- });
1325
- throw new AgentRunError(result, { cause: error });
1326
- }
1327
- finally {
1328
- if (this.activeRun === controller)
1329
- this.activeRun = undefined;
1330
- this.activeRunId = undefined;
1331
- this.activeLoop = undefined;
1332
- this.activeGatedRound = undefined;
1333
- this.activeProviderTurnAbort = undefined;
1334
- this.pendingSoftInterrupt = false;
1335
- this.pendingSteers = [];
1336
- this.pendingSteerBytes = 0;
1337
- try {
1338
- await this.drainLedger();
1339
- if (this.activeLedger) {
1340
- const status = runStatus;
1341
- const finishRecord = {
1342
- id: runId,
1343
- sessionId: this.id,
1344
- branchId: this.currentLeafId,
1345
- model,
1346
- provider: model.provider,
1347
- idempotencyKey: this.activeIdempotencyKey,
1348
- status,
1349
- startedAt,
1350
- finishedAt: new Date().toISOString(),
1351
- abortReason: controller.signal.aborted ? String(controller.signal.reason) : undefined,
1352
- error: runError,
1353
- ...this.activeOwnership,
1354
- };
1355
- await this.activeLedger.appendRun(redactRunLedgerRecord(finishRecord, this.activeRedactor));
1356
- if (isFlushableRunLedger(this.activeLedger) && this.activeLedger.durability === "flush_on_terminal")
1357
- await this.activeLedger.flush();
1358
- }
1359
- }
1360
- finally {
1361
- this.activeLedger = undefined;
1362
- this.activeEffectStore = undefined;
1363
- this.activeOwnership = undefined;
1364
- this.activeIdentity = undefined;
1365
- this.activeIdempotencyKey = undefined;
1366
- this.activeGuardrails = undefined;
1367
- this.activeMetadata = undefined;
1368
- this.activeLimits?.dispose();
1369
- this.activeLimits = undefined;
1370
- this.activeLimitOutputBuffer = false;
1371
- this.activeRedactor = undefined;
1372
- this.activeProvider = undefined;
1373
- cleanupSignal();
1374
- this.closeSubscribers();
1375
- }
1376
- }
1377
- }
1378
- prompt(input, options) {
1379
- return this.run(input, options);
1380
- }
1381
- async *stream(input, options = {}) {
1382
- const { maxQueuedEvents, overflow, ...runOptions } = options;
1383
- const subscription = this.subscribe({ maxQueuedEvents, overflow });
1384
- let runOwnedId;
1385
- let settled = false;
1386
- const runPromise = this.run(input, runOptions).finally(() => {
1387
- settled = true;
1388
- });
1389
- try {
1390
- for await (const event of subscription) {
1391
- if ("runId" in event && typeof event.runId === "string") {
1392
- if (runOwnedId === undefined && event.type === "agent_started")
1393
- runOwnedId = event.runId;
1394
- if (runOwnedId !== undefined && event.runId !== runOwnedId)
1395
- continue;
1396
- }
1397
- yield event;
1398
- }
1399
- await runPromise;
1400
- }
1401
- finally {
1402
- if (!settled) {
1403
- this.abort(new Error("stream consumer closed"));
1404
- await runPromise.catch(() => undefined);
1405
- }
1406
- }
1407
- }
1408
- buildRunResult(input) {
1409
- const final = finalAssistantMessage(this.history);
1410
- return {
1411
- sessionId: this.id,
1412
- runId: input.runId,
1413
- status: input.status,
1414
- leafId: this.currentLeafId,
1415
- text: final.text,
1416
- content: final.content,
1417
- message: final.message,
1418
- usage: input.usage,
1419
- limit: input.limit,
1420
- error: input.error,
1421
- abortReason: input.abortReason,
1422
- runState: input.runState,
1423
- interruption: input.interruption,
1424
- };
1425
- }
1426
- async suspendDurable(input) {
1427
- const durable = this.activeDurable;
1428
- if (!durable)
1429
- throw new AgentRunStateError("Durable interruption is not configured");
1430
- // Capture loop-local state before persisting the suspension. Undefined before the loop
1431
- // starts (input-guardrail suspensions) and for snapshot-less built-ins.
1432
- const loop = this.activeLoop;
1433
- const loopState = loop?.snapshot ? boundedLoopSnapshot(loop.name, loop.revision ?? "1", loop.snapshot()) : undefined;
1434
- const state = durable.state ??
1435
- initialAgentRunState({
1436
- agent: this.agent,
1437
- options: durable.options,
1438
- runId: input.runId,
1439
- sessionId: this.id,
1440
- leafId: this.currentLeafId,
1441
- model: input.model,
1442
- counters: input.limits.snapshot(),
1443
- deadlineAt: input.limits.deadlineAt,
1444
- status: "suspended",
1445
- interruption: input.interruption,
1446
- messages: input.messages,
1447
- pending: input.pending,
1448
- pendingCalls: input.pendingCalls,
1449
- interruptBeforeTool: durable.options.interruptBeforeTool,
1450
- });
1451
- return this.persistDurable({
1452
- ...state,
1453
- leafId: this.currentLeafId,
1454
- status: "suspended",
1455
- interruption: input.interruption,
1456
- ...(input.messages ? { input: input.messages } : {}),
1457
- ...(input.pending ? { pending: input.pending } : {}),
1458
- ...(input.pendingCalls ? { pendingCalls: input.pendingCalls } : {}),
1459
- nestedRuns: input.nestedRuns ?? state.nestedRuns,
1460
- ...(loopState ? { loopState } : {}),
1461
- counters: input.limits.snapshot(),
1462
- });
1463
- }
1464
- /** First attributed sticky whose scope and delegation path exactly match a nested decision. */
1465
- matchNestedSticky(decision) {
1466
- const stickies = this.activeDurable?.state?.stickyDecisions;
1467
- return stickies?.find((sticky) => sticky.attribution !== undefined &&
1468
- pathsEqual(sticky.attribution.path, decision.attribution?.path) &&
1469
- decisionScopesEqual(sticky.scope, decision.scope));
1470
- }
1471
- /** First sticky decision whose scope exactly matches this call, if any. */
1472
- matchStickyDecision(call, registry) {
1473
- const stickies = this.activeDurable?.state?.stickyDecisions;
1474
- if (!stickies?.length)
1475
- return undefined;
1476
- const identityRef = decisionIdentityRef(this.activeIdentity);
1477
- let argumentsHash;
1478
- let effectKind;
1479
- let effectResolved = false;
1480
- return stickies.find((sticky) => {
1481
- if (sticky.attribution !== undefined)
1482
- return false; // nested-run stickies match decisions, not calls
1483
- const scope = sticky.scope;
1484
- if (scope.toolName !== undefined && scope.toolName !== call.name)
1485
- return false;
1486
- if (scope.identity !== undefined && scope.identity !== identityRef)
1487
- return false;
1488
- if (scope.argumentsHash !== undefined) {
1489
- argumentsHash ??= toolEffectArgumentsHash(call.arguments);
1490
- if (scope.argumentsHash !== argumentsHash)
1491
- return false;
1492
- }
1493
- if (scope.effectKind !== undefined) {
1494
- if (!effectResolved) {
1495
- effectResolved = true;
1496
- const tool = registry.get(call.name);
1497
- effectKind = tool?.effect
1498
- ? resolveToolEffectDeclaration(tool, call.arguments, { sessionId: this.id, runId: "", toolCallId: call.id })?.kind
1499
- : undefined;
1500
- }
1501
- if (scope.effectKind !== effectKind)
1502
- return false;
1503
- }
1504
- if (scope.actionConstraints) {
1505
- for (const [key, value] of Object.entries(scope.actionConstraints)) {
1506
- const actual = call.arguments[key];
1507
- if (canonicalToolEffectJson(actual === undefined ? null : actual) !== canonicalToolEffectJson(value))
1508
- return false;
1509
- }
1510
- }
1511
- return true;
1512
- });
1513
- }
1514
- /** Redacted pending-decision descriptor for one gated call; never carries raw arguments. */
1515
- buildPendingDecision(call, approvalId, registry, runId, metadata, signal) {
1516
- const tool = registry.get(call.name);
1517
- const declaration = tool?.effect
1518
- ? resolveToolEffectDeclaration(tool, call.arguments, {
1519
- sessionId: this.id,
1520
- runId,
1521
- toolCallId: call.id,
1522
- signal,
1523
- metadata,
1524
- })
1525
- : undefined;
1526
- const identityRef = decisionIdentityRef(this.activeIdentity);
1527
- const elicitation = toolElicitationRequest(tool, call.arguments, {
1528
- sessionId: this.id,
1529
- runId,
1530
- toolCallId: call.id,
1531
- signal,
1532
- metadata,
1533
- });
1534
- return {
1535
- approvalId,
1536
- kind: elicitation ? "elicitation" : "tool_approval",
1537
- toolCallId: call.id,
1538
- scope: {
1539
- toolName: call.name,
1540
- argumentsHash: toolEffectArgumentsHash(call.arguments),
1541
- ...(declaration && declaration.kind !== "none" ? { effectKind: declaration.kind } : {}),
1542
- ...(identityRef ? { identity: identityRef } : {}),
1543
- },
1544
- reason: elicitation?.reason ?? "Tool side effect requires approval",
1545
- ...(elicitation ? { elicitationSchema: elicitation.schema } : {}),
1546
- };
1547
- }
1548
- async persistDurable(state) {
1549
- const durable = this.activeDurable;
1550
- if (!durable)
1551
- throw new AgentRunStateError("Durable run state is not configured");
1552
- const persisted = durable.options.persistSessionState
1553
- ? { ...state, sessionState: { loadedSkillNames: this.loadedSkills.list() } }
1554
- : state;
1555
- const saved = await saveAgentRunState({
1556
- checkpoints: durable.options.checkpoints,
1557
- state: persisted,
1558
- expectedVersion: durable.version,
1559
- ownership: this.activeOwnership,
1560
- fencingToken: durable.options.fencingToken,
1561
- redactor: this.activeRedactor,
1562
- maxStateBytes: durable.options.maxStateBytes,
1563
- });
1564
- durable.state = saved.state;
1565
- durable.version = saved.record.version;
1566
- return publicState(saved.state);
1567
- }
1568
- async compact(options = {}) {
1569
- if (this.activeRun)
1570
- throw new Error("Agent session already has an active run");
1571
- return this.compactBranch(options, undefined, options.signal, "manual");
1572
- }
1573
- abort(reason) {
1574
- this.activeRun?.abort(reason);
1575
- }
1576
- async entries() {
1577
- const reader = this.branchReader();
1578
- return reader
1579
- ? getSessionBranchEntries(reader, { sessionId: this.id, leafId: this.currentLeafId })
1580
- : getSessionBranchEntries(await this.store.list(this.id), { leafId: this.currentLeafId });
1581
- }
1582
- async checkout(leafId) {
1583
- this.currentLeafId = leafId;
1584
- this.invalidateSnapshot();
1585
- await this.rebuildHistory();
1586
- }
1587
- fork(options = {}) {
1588
- return createAgentSession({
1589
- agent: this.agent,
1590
- id: this.id,
1591
- store: this.store,
1592
- leafId: options.leafId ?? this.currentLeafId,
1593
- metadata: this.metadata,
1594
- });
1595
- }
1596
- async clone(options = {}) {
1597
- const id = options.id ?? randomId("session");
1598
- const leafId = options.leafId ?? this.currentLeafId;
1599
- const reader = this.branchReader();
1600
- const branch = reader
1601
- ? await getSessionBranchEntries(reader, { sessionId: this.id, leafId })
1602
- : getSessionBranchEntries(await this.store.list(this.id), { leafId });
1603
- const remap = new Map();
1604
- for (const entry of branch) {
1605
- const nextId = randomId("entry");
1606
- remap.set(entry.id, nextId);
1607
- const { id: _oldId, parentId: _oldParentId, sessionId: _oldSessionId, ...rest } = entry;
1608
- await this.store.append({ ...rest, id: nextId, parentId: entry.parentId ? remap.get(entry.parentId) : undefined, sessionId: id });
1609
- }
1610
- return createAgentSession({
1611
- agent: this.agent,
1612
- id,
1613
- store: this.store,
1614
- leafId: branch.length ? remap.get(branch[branch.length - 1].id) : undefined,
1615
- metadata: this.metadata,
1616
- });
1617
- }
1618
- branchReader() {
1619
- // ponytail: prefer the store's readBranchPath (one ancestor-chain query) when present so a
1620
- // DB-backed store never loads the full session; else fall back to list() + in-memory walk.
1621
- const read = this.store.readBranchPath;
1622
- return read ? (query) => read.call(this.store, query) : undefined;
1623
- }
1624
- resolveRunProvider(options) {
1625
- const model = options.model ?? this.agent.config.model;
1626
- // Provider precedence: an explicit `AgentConfig.provider` wins and bypasses
1627
- // the resolver entirely; otherwise `RunOptions.providerSource` overrides
1628
- // `AgentConfig.providerSource` for this run. A miss on every source fails
1629
- // closed with `Unknown provider: ${model.provider}` before any provider turn.
1630
- const provider = this.agent.config.provider ?? options.providerSource?.(model) ?? this.agent.config.providerSource?.(model);
1631
- if (!provider)
1632
- throw new Error(`Unknown provider: ${model.provider}`);
1633
- this.activeProvider = provider;
1634
- }
1635
- resolveRunSkills(options, tools) {
1636
- const configured = this.agent.config.skills;
1637
- if (configured && typeof configured === "object" && "list" in configured) {
1638
- if (options.activeSkills)
1639
- return resolveActiveSkills({ registry: configured, names: options.activeSkills, tools });
1640
- if (options.skills !== undefined)
1641
- return options.skills;
1642
- if (options.activateAllSkills ?? this.agent.config.activateAllSkills)
1643
- return configured.list();
1644
- return [];
1645
- }
1646
- const arr = options.skills ?? (Array.isArray(configured) ? configured : []);
1647
- return arr;
1648
- }
1649
- emit(event) {
1650
- const redacted = redactAgentEvent(event, this.activeRedactor);
1651
- for (const subscriber of this.subscribers)
1652
- subscriber.push(redacted);
1653
- if (this.activeLedger) {
1654
- const record = {
1655
- id: randomId("event"),
1656
- sessionId: event.sessionId ?? this.id,
1657
- runId: event.runId,
1658
- type: event.type,
1659
- timestamp: new Date().toISOString(),
1660
- event: redacted,
1661
- redacted: Boolean(this.activeRedactor),
1662
- ...this.activeOwnership,
1663
- };
1664
- const ledger = this.activeLedger;
1665
- this.ledgerChain = this.ledgerChain.then(async () => {
1666
- if (this.ledgerFailure)
1667
- return;
1668
- try {
1669
- await ledger.appendEvent(redactRunLedgerRecord(record, this.activeRedactor));
1670
- }
1671
- catch (error) {
1672
- this.ledgerFailure = error;
1673
- }
1674
- });
1675
- }
1676
- }
1677
- closeSubscribers() {
1678
- for (const subscriber of this.subscribers)
1679
- subscriber.close();
1680
- this.subscribers.clear();
1681
- }
1682
- async drainLedger() {
1683
- await this.ledgerChain;
1684
- const failure = this.ledgerFailure;
1685
- this.ledgerChain = Promise.resolve();
1686
- this.ledgerFailure = undefined;
1687
- if (failure)
1688
- throw failure;
1689
- }
1690
- async generateWithRetry(request, runId, options, signal, requestSecrets = [], turn = 1, recordUsage) {
1691
- const retry = mergeRetry(this.agent.config.retry, options.retry);
1692
- const secrets = [...requestSecrets, ...(retry?.secrets ?? [])];
1693
- const policy = retry?.policy ?? (retry ? createDefaultRetryPolicy(retry) : undefined);
1694
- for (let attempt = 1;; attempt += 1) {
1695
- try {
1696
- return await this.generateProviderTurn(request, runId, signal, secrets, turn, attempt, recordUsage);
1697
- }
1698
- catch (error) {
1699
- if (error instanceof GuardrailError || isSteerSoftInterrupt(error))
1700
- throw error;
1701
- const failure = error instanceof ProviderTurnFailure ? error : undefined;
1702
- const info = failure ? redactSecrets(failure.info, secrets) : errorToErrorInfo(error, secrets);
1703
- if (!policy || failure?.observable)
1704
- throw errorFromInfo(info);
1705
- const context = { sessionId: this.id, runId, attempt, error: info, metadata: retry?.metadata, signal };
1706
- let decision = await policy.decide(context);
1707
- const payload = (await this.agent.config.middleware?.run("retry", { context, decision })) ?? {
1708
- context,
1709
- decision,
1710
- };
1711
- decision = payload.decision;
1712
- if (!decision.retry)
1713
- throw errorFromInfo(info);
1714
- const delayMs = decision.delayMs ?? 0;
1715
- this.emit({ type: "retry_scheduled", sessionId: this.id, runId, attempt, delayMs, error: info });
1716
- await waitForRetry(decision, signal);
1717
- }
1718
- }
1719
- }
1720
- async generateProviderTurn(request, runId, signal, secrets = [], turn = 1, attempt = 1, recordUsage) {
1721
- this.activeLimits.charge("maxProviderAttempts");
1722
- this.activeLimits.charge("maxRequestBytes", jsonBytes(request));
1723
- const startedAt = performance.now();
1724
- const providerId = this.activeProvider?.id ?? request.model.provider;
1725
- const buildMetadata = (extra = {}) => createProviderTurnMetadata(request, providerId, { attempt, ...extra });
1726
- this.emit({
1727
- type: "provider_turn_started",
1728
- sessionId: this.id,
1729
- runId,
1730
- turn,
1731
- metadata: buildMetadata(),
1732
- });
1733
- const content = [];
1734
- const calls = [];
1735
- const toolDeltas = [];
1736
- let messageId;
1737
- let started = false;
1738
- let usage;
1739
- let usageRecorded = false;
1740
- const bufferedOutput = [];
1741
- const bufferOutput = Boolean(this.activeGuardrails?.output?.length || this.activeLimitOutputBuffer);
1742
- const emitOutput = (event) => {
1743
- if (bufferOutput)
1744
- bufferedOutput.push(event);
1745
- else
1746
- this.emit(event);
1747
- };
1748
- const recordTurnUsage = async () => {
1749
- if (usageRecorded)
1750
- return;
1751
- usageRecorded = true;
1752
- await recordUsage?.(usage, turn, attempt);
1753
- };
1754
- const turnAbort = new AbortController();
1755
- const cleanupTurn = bridgeAbort(signal, turnAbort);
1756
- this.activeProviderTurnAbort = turnAbort;
1757
- if (this.pendingSoftInterrupt) {
1758
- this.pendingSoftInterrupt = false;
1759
- turnAbort.abort(new SteerSoftInterrupt());
1760
- }
1761
- const turnRequest = { ...request, signal: turnAbort.signal };
1762
- try {
1763
- throwIfAborted(turnAbort.signal);
1764
- for await (const event of this.activeProvider.generate(turnRequest)) {
1765
- throwIfAborted(turnAbort.signal);
1766
- this.activeLimits.charge("maxResponseBytes", jsonBytes(event));
1767
- if (event.type === "error")
1768
- throw new ProviderTurnFailure(event.error, started);
1769
- if (event.type === "usage")
1770
- usage = event.usage;
1771
- if (event.type === "done") {
1772
- usage = event.usage ?? usage;
1773
- break;
1774
- }
1775
- if (event.type === "message_start") {
1776
- started = true;
1777
- messageId = event.messageId;
1778
- emitOutput({ type: "message_started", sessionId: this.id, runId, message: { id: messageId, role: "assistant", content: [] } });
1779
- continue;
1780
- }
1781
- if (event.type === "content_delta" || event.type === "tool_call" || event.type === "tool_call_delta") {
1782
- if (!started) {
1783
- started = true;
1784
- emitOutput({ type: "message_started", sessionId: this.id, runId, message: { role: "assistant", content: [] } });
1785
- }
1786
- if (event.type === "tool_call_delta") {
1787
- toolDeltas.push(event);
1788
- emitOutput({ type: "message_delta", sessionId: this.id, runId, content: providerToolCallDeltaContent(event) });
1789
- continue;
1790
- }
1791
- const block = providerContent(event);
1792
- content.push(block);
1793
- if (block.type === "tool_call")
1794
- calls.push(block);
1795
- emitOutput({ type: "message_delta", sessionId: this.id, runId, content: block });
1796
- }
1797
- }
1798
- for (const call of reconstructMissingToolCalls(toolDeltas, calls)) {
1799
- content.push(call);
1800
- calls.push(call);
1801
- emitOutput({ type: "message_delta", sessionId: this.id, runId, content: call });
1802
- }
1803
- await recordTurnUsage();
1804
- if (this.activeGuardrails?.output?.length) {
1805
- assertGuardrailsAllowed(await runGuardrails({
1806
- stage: "output",
1807
- guardrails: this.activeGuardrails,
1808
- value: { content, calls, messageId, started, usage },
1809
- context: { sessionId: this.id, runId, metadata: this.activeMetadata ?? {}, signal: turnAbort.signal },
1810
- redactor: this.activeRedactor,
1811
- emit: (event) => this.emit(event),
1812
- }));
1813
- }
1814
- if (bufferOutput)
1815
- for (const event of bufferedOutput)
1816
- this.emit(event);
1817
- const latencyMs = Math.round(performance.now() - startedAt);
1818
- this.emit({
1819
- type: "provider_turn_finished",
1820
- sessionId: this.id,
1821
- runId,
1822
- turn,
1823
- metadata: buildMetadata({ latencyMs }),
1824
- usage,
1825
- });
1826
- return { content, calls, messageId, started, usage };
1827
- }
1828
- catch (error) {
1829
- if (isSteerSoftInterrupt(error) || isSteerSoftInterrupt(turnAbort.signal.reason)) {
1830
- await recordTurnUsage();
1831
- const latencyMs = Math.round(performance.now() - startedAt);
1832
- this.emit({
1833
- type: "provider_turn_finished",
1834
- sessionId: this.id,
1835
- runId,
1836
- turn,
1837
- metadata: buildMetadata({ latencyMs }),
1838
- usage,
1839
- });
1840
- throw new SteerSoftInterrupt();
1841
- }
1842
- const latencyMs = Math.round(performance.now() - startedAt);
1843
- const info = error instanceof ProviderTurnFailure ? redactSecrets(error.info, secrets) : errorToErrorInfo(error, secrets);
1844
- await recordTurnUsage();
1845
- this.emit({
1846
- type: "provider_turn_finished",
1847
- sessionId: this.id,
1848
- runId,
1849
- turn,
1850
- metadata: buildMetadata({ latencyMs, httpStatus: readProviderHttpStatus(info) }),
1851
- usage,
1852
- error: info,
1853
- });
1854
- if (error instanceof GuardrailError || error instanceof ProviderTurnFailure)
1855
- throw error;
1856
- throw new ProviderTurnFailure(info, started);
1857
- }
1858
- finally {
1859
- cleanupTurn();
1860
- if (this.activeProviderTurnAbort === turnAbort)
1861
- this.activeProviderTurnAbort = undefined;
1862
- }
1863
- }
1864
- async applyPendingSteers(runId, metadata, signal) {
1865
- if (this.pendingSteers.length === 0)
1866
- return false;
1867
- const drained = this.pendingSteers.splice(0);
1868
- this.pendingSteerBytes = 0;
1869
- this.emit({ type: "queue_updated", sessionId: this.id, runId, size: 0 });
1870
- for (const message of drained) {
1871
- throwIfAborted(signal);
1872
- const inputGuardrails = await runGuardrails({
1873
- stage: "input",
1874
- guardrails: this.activeGuardrails,
1875
- value: [message],
1876
- context: { sessionId: this.id, runId, metadata, signal },
1877
- redactor: this.activeRedactor,
1878
- emit: (event) => this.emit(event),
1879
- });
1880
- // Mid-run steer: a terminal decision drops the message (never enters history or
1881
- // the session store) and the run continues. Run-start input blocking still fails
1882
- // the run — only the blast radius of steered input is narrowed.
1883
- const terminal = inputGuardrails.terminal;
1884
- if (terminal) {
1885
- if (terminal.action === "interrupt")
1886
- throw new GuardrailError(terminal);
1887
- this.emit({
1888
- type: "steer_rejected",
1889
- sessionId: this.id,
1890
- runId,
1891
- message: this.activeRedactor ? this.activeRedactor.redact(message) : message,
1892
- record: terminal,
1893
- });
1894
- continue;
1895
- }
1896
- this.history.push(message);
1897
- await this.appendMessage(message, runId);
1898
- }
1899
- return true;
1900
- }
1901
- async applyProviderRequestPolicies(request, runId, options, metadata, signal) {
1902
- const policies = [...policyList(this.agent.config.providerRequestPolicies), ...policyList(options.providerRequestPolicies)];
1903
- if (policies.length === 0)
1904
- return { request, secrets: [] };
1905
- const result = await createProviderRequestPolicyChain(policies).apply({ request, sessionId: this.id, runId, metadata, signal });
1906
- return normalizeProviderRequestPolicyResult(result);
1907
- }
1908
- async appendMessage(message, runId) {
1909
- await this.appendEntry(createSessionEntry({ sessionId: this.id, parentId: this.currentLeafId, runId, kind: "message", message }));
1910
- }
1911
- async autoCompact(runId, options, signal, inputMessages) {
1912
- const compaction = mergeCompaction(this.agent.config.compaction, options.compaction);
1913
- if (!compaction || compaction.thresholdEntries === undefined)
1914
- return;
1915
- const snapshot = await this.snapshot();
1916
- if (snapshot.entries.length <= compaction.thresholdEntries || snapshot.entries.at(-1)?.kind === "compaction")
1917
- return;
1918
- await this.compactBranch(compaction, runId, signal, "auto");
1919
- const compacted = await this.snapshot();
1920
- this.history = withoutTrailingInput(compacted.messages, inputMessages);
1921
- }
1922
- async compactBranch(options, runId, signal, trigger) {
1923
- throwIfAbortedSignal(signal);
1924
- const entries = await this.entries();
1925
- const secrets = options.secrets ?? [];
1926
- const strategy = options.strategy ??
1927
- createDefaultCompactionStrategy({ keepRecentEntries: options.keepRecentEntries, maxSummaryChars: options.maxSummaryChars, secrets });
1928
- const context = {
1929
- sessionId: this.id,
1930
- entries,
1931
- keepRecentEntries: options.keepRecentEntries,
1932
- trigger,
1933
- secrets,
1934
- metadata: options.metadata,
1935
- signal,
1936
- };
1937
- this.emit({ type: "compaction_started", sessionId: this.id, runId });
1938
- let result = await strategy.compact(context);
1939
- result = { ...result, summary: redactSecrets(result.summary, secrets) };
1940
- const payload = (await this.agent.config.middleware?.run("compaction", { context, result })) ?? {
1941
- context,
1942
- result,
1943
- };
1944
- result = { ...payload.result, summary: redactSecrets(payload.result.summary, secrets) };
1945
- const source = result.entries?.find((entry) => entry.kind === "compaction");
1946
- const data = isCompactionEntryData(source?.data) ? source.data : undefined;
1947
- const entry = createSessionEntry({
1948
- sessionId: this.id,
1949
- parentId: this.currentLeafId,
1950
- runId,
1951
- kind: "compaction",
1952
- summary: result.summary,
1953
- data,
1954
- });
1955
- await this.appendEntry(entry);
1956
- const finalResult = { ...result, entries: [entry] };
1957
- this.emit({ type: "compaction_finished", sessionId: this.id, runId, summary: finalResult.summary });
1958
- await this.rebuildHistory();
1959
- return finalResult;
1960
- }
1961
- async appendEntry(entry) {
1962
- const redacted = redactSessionEntry(entry, this.activeRedactor);
1963
- await this.store.append(redacted, {
1964
- expectedParentId: this.currentLeafId,
1965
- idempotencyKey: this.activeIdempotencyKey,
1966
- });
1967
- this.currentLeafId = redacted.id;
1968
- this.invalidateSnapshot();
1969
- }
1970
- invalidateSnapshot() {
1971
- this.snapshotGeneration += 1;
1972
- this.snapshotCache = undefined;
1973
- }
1974
- redact(value) {
1975
- return this.activeRedactor?.redact(value) ?? value;
1976
- }
1977
- redactProviderRequest(request) {
1978
- return redactProviderRequest(request, this.activeRedactor);
1979
- }
1980
- async rebuildHistory() {
1981
- this.history = (await this.snapshot()).messages.slice();
1982
- }
1983
- async snapshot() {
1984
- const now = performance.now();
1985
- const cached = this.snapshotCache;
1986
- if (cached && cached.leafId === this.currentLeafId && cached.generation === this.snapshotGeneration && cached.expiresAt > now)
1987
- return cached.value;
1988
- const reader = this.branchReader();
1989
- const value = reader
1990
- ? await rebuildSessionContext(reader, { sessionId: this.id, leafId: this.currentLeafId })
1991
- : rebuildSessionContext(await this.store.list(this.id), { leafId: this.currentLeafId });
1992
- this.snapshotCache = { leafId: this.currentLeafId, generation: this.snapshotGeneration, expiresAt: now + 1_000, value };
1993
- return value;
1994
- }
1995
- }
1996
- class EventSubscriber {
1997
- sessionId;
1998
- onClose;
1999
- queue = [];
2000
- waiters = [];
2001
- maxQueuedEvents;
2002
- overflow;
2003
- closed = false;
2004
- constructor(sessionId, options, onClose) {
2005
- this.sessionId = sessionId;
2006
- this.onClose = onClose;
2007
- const maxQueuedEvents = options.maxQueuedEvents ?? 1024;
2008
- this.maxQueuedEvents = Number.isFinite(maxQueuedEvents) ? Math.max(1, Math.floor(maxQueuedEvents)) : 1024;
2009
- this.overflow = options.overflow ?? "close";
2010
- }
2011
- [Symbol.asyncIterator]() {
2012
- return this;
2013
- }
2014
- next() {
2015
- const event = this.queue.shift();
2016
- if (event)
2017
- return Promise.resolve({ value: event, done: false });
2018
- if (this.closed)
2019
- return Promise.resolve({ value: undefined, done: true });
2020
- return new Promise((resolve) => this.waiters.push(resolve));
2021
- }
2022
- return() {
2023
- this.close();
2024
- this.onClose();
2025
- return Promise.resolve({ value: undefined, done: true });
2026
- }
2027
- push(event) {
2028
- const waiter = this.waiters.shift();
2029
- if (waiter) {
2030
- waiter({ value: event, done: false });
2031
- return;
2032
- }
2033
- if (this.closed)
2034
- return;
2035
- if (this.queue.length < this.maxQueuedEvents) {
2036
- this.queue.push(event);
2037
- return;
2038
- }
2039
- if (this.overflow === "drop_oldest") {
2040
- this.queue.shift();
2041
- this.queue.push(event);
2042
- return;
2043
- }
2044
- if (this.overflow === "drop_newest")
2045
- return;
2046
- const droppedEvents = this.queue.length + 1;
2047
- this.queue.splice(0, this.queue.length, {
2048
- type: "event_subscriber_overflow",
2049
- sessionId: this.sessionId,
2050
- droppedEvents,
2051
- maxQueuedEvents: this.maxQueuedEvents,
2052
- overflow: this.overflow,
2053
- });
2054
- this.close();
2055
- this.onClose();
2056
- }
2057
- close() {
2058
- this.closed = true;
2059
- for (const waiter of this.waiters.splice(0))
2060
- waiter({ value: undefined, done: true });
2061
- }
2062
- }
2063
- function providerContent(event) {
2064
- return event.type === "content_delta" ? event.content : event.call;
2065
- }
2066
- function reconstructMissingToolCalls(deltas, calls) {
2067
- if (deltas.length === 0)
2068
- return [];
2069
- const seen = new Set(calls.map((call) => call.id));
2070
- return reconstructToolCallDeltas(deltas).filter((call) => !seen.has(call.id));
2071
- }
2072
- function inputToMessages(input) {
2073
- if (typeof input === "string")
2074
- return [{ role: "user", content: [{ type: "text", text: input }] }];
2075
- if ("role" in input)
2076
- return [input];
2077
- return [...input];
2078
- }
2079
- const steerTextEncoder = new TextEncoder();
2080
- function messageTextBytes(message) {
2081
- let total = 0;
2082
- for (const block of message.content) {
2083
- if (block.type === "text")
2084
- total += steerTextEncoder.encode(block.text).byteLength;
2085
- }
2086
- return total;
2087
- }
2088
- const STEER_SOFT_INTERRUPT_CODE = "steer_soft_interrupt";
2089
- class SteerSoftInterrupt extends Error {
2090
- code = STEER_SOFT_INTERRUPT_CODE;
2091
- constructor() {
2092
- super("Provider turn soft-interrupted by steer");
2093
- this.name = "SteerSoftInterrupt";
2094
- }
2095
- }
2096
- function isSteerSoftInterrupt(error) {
2097
- return (error instanceof SteerSoftInterrupt ||
2098
- (typeof error === "object" && error !== null && error.code === STEER_SOFT_INTERRUPT_CODE));
2099
- }
2100
- function finalAssistantMessage(history) {
2101
- for (let index = history.length - 1; index >= 0; index -= 1) {
2102
- const message = history[index];
2103
- if (message.role !== "assistant")
2104
- continue;
2105
- const text = message.content
2106
- .filter((block) => block.type === "text")
2107
- .map((block) => block.text)
2108
- .join("");
2109
- return { message, content: message.content, text };
2110
- }
2111
- return { content: [], text: "" };
2112
- }
2113
- function activeTools(tools) {
2114
- if (!tools)
2115
- return { registry: createToolRegistry(), tools: [] };
2116
- if ("list" in tools)
2117
- return { registry: tools, tools: tools.list() };
2118
- const registry = createToolRegistry(tools);
2119
- return { registry, tools };
2120
- }
2121
- function policyList(policies) {
2122
- if (!policies)
2123
- return [];
2124
- return "apply" in policies ? [policies] : policies;
2125
- }
2126
- function errorFromInfo(error) {
2127
- return Object.assign(new Error(error.message), { name: error.name ?? "Error", cause: error.cause, code: error.code });
2128
- }
2129
- class ProviderTurnFailure extends Error {
2130
- info;
2131
- observable;
2132
- constructor(info, observable) {
2133
- super(info.message);
2134
- this.info = info;
2135
- this.observable = observable;
2136
- }
2137
- }
2138
- function mergeRetry(agent, run) {
2139
- if (run === false)
2140
- return undefined;
2141
- if (run)
2142
- return { ...(agent || {}), ...run };
2143
- return agent || undefined;
2144
- }
2145
- function mergeCompaction(agent, run) {
2146
- if (run === false)
2147
- return undefined;
2148
- if (run)
2149
- return { ...(agent || {}), ...run };
2150
- return agent || undefined;
2151
- }
2152
- /** Compact redacted principal reference used in decision scopes; never a credential. */
2153
- function decisionIdentityRef(identity) {
2154
- return identity ? `${identity.tenantId}:${identity.principal.kind}:${identity.principal.id}` : undefined;
2155
- }
2156
- /**
2157
- * Durable-run gate: built-in option forms and the single-shot singleton are durable via the
2158
- * pending-call mechanism; a custom strategy must declare both snapshot and restore hooks.
2159
- */
2160
- function isDurableLoop(loop) {
2161
- if (typeof loop !== "object" || loop === null)
2162
- return true;
2163
- if ("strategy" in loop)
2164
- return true;
2165
- if (loop === singleShotLoop)
2166
- return true;
2167
- return typeof loop.snapshot === "function" && typeof loop.restore === "function";
2168
- }
2169
- function mergeGuardrails(agent, run) {
2170
- if (!agent && !run)
2171
- return undefined;
2172
- return {
2173
- input: [...(agent?.input ?? []), ...(run?.input ?? [])],
2174
- output: [...(agent?.output ?? []), ...(run?.output ?? [])],
2175
- toolInput: [...(agent?.toolInput ?? []), ...(run?.toolInput ?? [])],
2176
- toolOutput: [...(agent?.toolOutput ?? []), ...(run?.toolOutput ?? [])],
2177
- maxConcurrency: run?.maxConcurrency ?? agent?.maxConcurrency,
2178
- };
2179
- }
2180
- function withoutTrailingInput(messages, input) {
2181
- const next = [...messages];
2182
- for (let i = input.length - 1; i >= 0; i -= 1) {
2183
- const last = next.at(-1);
2184
- if (last && stableMessageKey(last) === stableMessageKey(input[i]))
2185
- next.pop();
2186
- }
2187
- return next;
2188
- }
2189
- // Key-order-insensitive comparison: a redacted-then-reassembled message with reordered
2190
- // keys must still dedupe against the trailing input, or auto-compaction duplicates it.
2191
- function stableMessageKey(value) {
2192
- if (Array.isArray(value))
2193
- return `[${value.map(stableMessageKey).join(",")}]`;
2194
- if (value !== null && typeof value === "object") {
2195
- const record = value;
2196
- return `{${Object.keys(record)
2197
- .sort()
2198
- .map((key) => `${JSON.stringify(key)}:${stableMessageKey(record[key])}`)
2199
- .join(",")}}`;
2200
- }
2201
- return JSON.stringify(value) ?? "null";
2202
- }
2203
- function bridgeAbort(signal, controller) {
2204
- if (!signal)
2205
- return () => undefined;
2206
- const abort = () => controller.abort(signal.reason);
2207
- if (signal.aborted)
2208
- abort();
2209
- else
2210
- signal.addEventListener("abort", abort, { once: true });
2211
- return () => signal.removeEventListener("abort", abort);
2212
- }
2213
- function throwIfAborted(signal) {
2214
- throwIfAbortedSignal(signal);
2215
- }
2216
- function throwIfAbortedSignal(signal) {
2217
- if (signal?.aborted)
2218
- throw signal.reason instanceof Error ? signal.reason : new Error("Agent run aborted");
2219
- }
2220
- const jsonTextEncoder = new TextEncoder();
2221
- function jsonBytes(value) {
2222
- try {
2223
- return jsonTextEncoder.encode(JSON.stringify(value)).byteLength;
2224
- }
2225
- catch {
2226
- throw new TypeError("Provider request or event must be JSON-serializable for run limits");
2227
- }
2228
- }
2229
- function createUsageAccumulator() {
2230
- const sums = new Map();
2231
- let costCurrency;
2232
- let costCompatible = true;
2233
- return {
2234
- add(usage) {
2235
- for (const key of ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"]) {
2236
- const value = usage[key];
2237
- if (value !== undefined)
2238
- sums.set(key, (sums.get(key) ?? 0) + value);
2239
- }
2240
- const total = usage.totalTokens ??
2241
- (usage.inputTokens !== undefined || usage.outputTokens !== undefined
2242
- ? (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0)
2243
- : undefined);
2244
- if (total !== undefined)
2245
- sums.set("totalTokens", (sums.get("totalTokens") ?? 0) + total);
2246
- if (usage.cost !== undefined && costCompatible) {
2247
- if (!sums.has("cost"))
2248
- costCurrency = usage.currency;
2249
- else if (usage.currency !== costCurrency)
2250
- costCompatible = false;
2251
- if (costCompatible)
2252
- sums.set("cost", (sums.get("cost") ?? 0) + usage.cost);
2253
- }
2254
- },
2255
- value() {
2256
- if (sums.size === 0)
2257
- return undefined;
2258
- const usage = {};
2259
- for (const [key, value] of sums) {
2260
- if (key !== "cost" || costCompatible)
2261
- usage[key] = value;
2262
- }
2263
- if (costCompatible && sums.has("cost") && costCurrency !== undefined)
2264
- usage.currency = costCurrency;
2265
- return Object.keys(usage).length > 0 ? usage : undefined;
2266
- },
2267
- };
2268
- }
2269
- const randomId = createId;
15
+ export { createAgent, createAgentSession } from "./agent-session.js";
16
+ export { resumeAgentRun, resumeAgentRunStream } from "./agent-run-lifecycle.js";
2270
17
  //# sourceMappingURL=agents.js.map