@arnilo/prism 0.0.6 → 0.0.8

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 (54) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +3 -1
  3. package/dist/agent-loops.js +8 -5
  4. package/dist/agent-run-lifecycle.d.ts +28 -0
  5. package/dist/agent-run-lifecycle.js +33 -0
  6. package/dist/agent-run-state.d.ts +53 -0
  7. package/dist/agent-run-state.js +127 -0
  8. package/dist/agents.d.ts +3 -1
  9. package/dist/agents.js +356 -45
  10. package/dist/contracts.d.ts +218 -3
  11. package/dist/contracts.js +4 -0
  12. package/dist/guardrails.d.ts +25 -0
  13. package/dist/guardrails.js +133 -0
  14. package/dist/index.d.ts +15 -3
  15. package/dist/index.js +9 -3
  16. package/dist/input.js +2 -0
  17. package/dist/resources.js +2 -1
  18. package/dist/run-ledger.d.ts +21 -0
  19. package/dist/run-ledger.js +115 -0
  20. package/dist/run-limits.d.ts +34 -0
  21. package/dist/run-limits.js +163 -0
  22. package/dist/secure-agent.d.ts +3 -0
  23. package/dist/secure-agent.js +63 -0
  24. package/dist/tools.d.ts +10 -2
  25. package/dist/tools.js +54 -4
  26. package/docs/a2a.md +61 -42
  27. package/docs/agent-events.md +15 -3
  28. package/docs/agent-loops.md +12 -4
  29. package/docs/agent-session-runtime.md +34 -1
  30. package/docs/credential-storage.md +9 -0
  31. package/docs/database-persistence.md +1 -1
  32. package/docs/evaluations.md +26 -3
  33. package/docs/guardrails.md +75 -0
  34. package/docs/host-security.md +31 -4
  35. package/docs/index.md +17 -14
  36. package/docs/mcp-tools.md +36 -8
  37. package/docs/migration.md +54 -0
  38. package/docs/observability.md +26 -14
  39. package/docs/performance.md +25 -0
  40. package/docs/postgres-persistence.md +1 -0
  41. package/docs/providers/kimi.md +16 -2
  42. package/docs/providers/opencode-go.md +43 -2
  43. package/docs/release-and-install.md +73 -62
  44. package/docs/resource-loading.md +4 -0
  45. package/docs/review-coverage-2026-07-19-phase-3.md +174 -0
  46. package/docs/run-ledger-conformance.md +1 -0
  47. package/docs/runs-and-usage.md +46 -4
  48. package/docs/server.md +5 -2
  49. package/docs/sqlite-persistence.md +1 -0
  50. package/docs/supervisors.md +2 -2
  51. package/docs/tools.md +8 -2
  52. package/docs/web-tools.md +78 -0
  53. package/docs/workflows.md +2 -0
  54. package/package.json +2 -1
package/dist/agents.js CHANGED
@@ -1,6 +1,7 @@
1
- import { AgentRunError } from "./contracts.js";
1
+ import { AgentRunError, AgentRunStateError } from "./contracts.js";
2
2
  import { resolveLoop, resolveToolConcurrency } from "./agent-loops.js";
3
3
  import { createId } from "./ids.js";
4
+ import { assertGuardrailsAllowed, GuardrailError, runGuardrails } from "./guardrails.js";
4
5
  import { createProviderTurnMetadata, readProviderHttpStatus } from "./observability.js";
5
6
  import { createDefaultCompactionStrategy, isCompactionEntryData } from "./compaction.js";
6
7
  import { assembleProviderInput } from "./input.js";
@@ -11,7 +12,10 @@ import { errorToErrorInfo, redactAgentEvent, redactProviderRequest, redactRunLed
11
12
  import { composeSystemPrompt, mergeSystemPromptConfig } from "./system-prompts.js";
12
13
  import { createDefaultRetryPolicy, waitForRetry } from "./retry.js";
13
14
  import { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, rebuildSessionContext } from "./session-stores.js";
15
+ import { isFlushableRunLedger } from "./run-ledger.js";
14
16
  import { createToolRegistry, dispatchToolCall } from "./tools.js";
17
+ import { RunLimitError, RunLimitTracker, resolveRunLimits } from "./run-limits.js";
18
+ import { agentFingerprint, initialAgentRunState, loadAgentRunState, publicState, saveAgentRunState, validateRunStateOptions } from "./agent-run-state.js";
15
19
  import { resolveActiveSkills } from "./skills.js";
16
20
  export function createAgent(config) {
17
21
  return {
@@ -24,6 +28,60 @@ export function createAgent(config) {
24
28
  export function createAgentSession(config) {
25
29
  return new RuntimeAgentSession(config);
26
30
  }
31
+ /** Resume a persisted built-in run. A claimed/dispatched tool is never replayed automatically. */
32
+ export async function resumeAgentRun(agent, ref, resume, options) {
33
+ const { record, state } = await loadAgentRunState(options.checkpoints, ref, options.ownership);
34
+ if (state.definitionRevision !== options.definitionRevision || state.agentId !== (agent.config.id ?? agent.config.name) || state.fingerprint !== agentFingerprint(agent, options.definitionRevision)) {
35
+ throw new AgentRunStateError("Agent definition revision or fingerprint mismatch on resume");
36
+ }
37
+ if (record.version !== resume.expectedVersion || state.status !== "suspended") {
38
+ throw new AgentRunStateError("Stale or non-suspended agent run resume");
39
+ }
40
+ if (resume.decision === "deny") {
41
+ const denied = await saveAgentRunState({
42
+ checkpoints: options.checkpoints,
43
+ state: { ...state, status: "denied" },
44
+ expectedVersion: record.version,
45
+ ownership: options.ownership,
46
+ fencingToken: options.fencingToken,
47
+ });
48
+ const session = new RuntimeAgentSession({ agent, id: state.sessionId, leafId: state.leafId });
49
+ await session.recordDurableDenial(state.runId, state.interruption, denied.record.version, options.ownership);
50
+ const runState = publicState(denied.state);
51
+ return { sessionId: state.sessionId, runId: state.runId, status: "denied", leafId: state.leafId, text: "", content: [], runState, interruption: state.interruption };
52
+ }
53
+ if (state.pending?.status === "dispatched")
54
+ throw new AgentRunStateError("Ambiguous dispatched tool requires operator resolution");
55
+ const configured = agent.config.runState;
56
+ if (configured && (configured.checkpoints !== options.checkpoints || configured.definitionRevision !== options.definitionRevision)) {
57
+ throw new AgentRunStateError("Agent durable run-state configuration mismatch on resume");
58
+ }
59
+ const claimed = await saveAgentRunState({
60
+ checkpoints: options.checkpoints,
61
+ state: { ...state, status: "running", interruption: undefined },
62
+ expectedVersion: record.version,
63
+ ownership: options.ownership,
64
+ fencingToken: options.fencingToken,
65
+ });
66
+ const session = new RuntimeAgentSession({ agent, id: state.sessionId, leafId: state.leafId });
67
+ return session.resumeDurable(claimed.state, configured ?? {
68
+ checkpoints: options.checkpoints,
69
+ definitionRevision: options.definitionRevision,
70
+ interruptBeforeTool: state.interruptBeforeTool,
71
+ fencingToken: options.fencingToken,
72
+ }, options.ownership);
73
+ }
74
+ class AgentRunSuspended extends Error {
75
+ state;
76
+ interruption;
77
+ code = "ERR_PRISM_AGENT_RUN_SUSPENDED";
78
+ constructor(state, interruption) {
79
+ super("Agent run suspended");
80
+ this.state = state;
81
+ this.interruption = interruption;
82
+ this.name = "AgentRunSuspended";
83
+ }
84
+ }
27
85
  class RuntimeAgentSession {
28
86
  id;
29
87
  agent;
@@ -38,9 +96,16 @@ class RuntimeAgentSession {
38
96
  activeLedger;
39
97
  activeOwnership;
40
98
  activeIdempotencyKey;
99
+ activeGuardrails;
100
+ activeMetadata;
101
+ activeLimits;
102
+ activeLimitOutputBuffer = false;
103
+ activeDurable;
41
104
  activeLoopTurn = 1;
42
105
  ledgerChain = Promise.resolve();
43
106
  ledgerFailure;
107
+ snapshotGeneration = 0;
108
+ snapshotCache;
44
109
  constructor(config) {
45
110
  this.id = config.id ?? randomId("session");
46
111
  this.agent = config.agent;
@@ -57,7 +122,41 @@ class RuntimeAgentSession {
57
122
  return subscriber;
58
123
  }
59
124
  async run(input, options = {}) {
60
- const runId = randomId("run");
125
+ return this.runInternal(input, options, randomId("run"));
126
+ }
127
+ async resumeDurable(state, runState, ownership) {
128
+ return this.runInternal(state.input ?? [], { runState, ownership }, state.runId, { options: runState, state, version: state.version });
129
+ }
130
+ async recordDurableDenial(runId, interruption, version, ownership) {
131
+ this.activeLedger = this.agent.config.runLedger;
132
+ this.activeOwnership = ownership ?? this.agent.config.ownership;
133
+ this.activeRedactor = this.agent.config.redactor;
134
+ this.emit({ type: "agent_denied", sessionId: this.id, runId, interruption, version });
135
+ await this.drainLedger();
136
+ this.activeLedger = undefined;
137
+ this.activeOwnership = undefined;
138
+ this.activeRedactor = undefined;
139
+ }
140
+ async runInternal(input, options, runId, resumed) {
141
+ if (this.agent.config.secure && (options.redactor !== undefined || options.ownership !== undefined || options.validate !== undefined || options.runState !== undefined)) {
142
+ throw new AgentRunStateError("Secure agent defaults cannot be replaced per run");
143
+ }
144
+ const requestedLimits = options.maxToolRounds === undefined
145
+ ? options.limits
146
+ : { ...options.limits, maxToolRounds: Math.min(options.maxToolRounds, options.limits?.maxToolRounds ?? options.maxToolRounds) };
147
+ const resolvedLimits = resolveRunLimits(this.agent.config.limits, requestedLimits);
148
+ const durableOptions = options.runState ?? this.agent.config.runState;
149
+ if (this.agent.config.runState && options.runState && this.agent.config.runState !== options.runState) {
150
+ throw new AgentRunStateError("RunOptions cannot replace agent durable run-state configuration");
151
+ }
152
+ if (durableOptions) {
153
+ validateRunStateOptions(durableOptions);
154
+ if (options.model || options.guardrails || options.loop)
155
+ throw new AgentRunStateError("Durable runs require model, guardrails, and loop on AgentConfig for fingerprinting");
156
+ const configuredLoop = this.agent.config.loop;
157
+ if (configuredLoop && !isBuiltInLoop(configuredLoop))
158
+ throw new AgentRunStateError("Custom AgentLoopStrategy is not durable");
159
+ }
61
160
  if (this.activeRun) {
62
161
  const error = new Error("Agent session already has an active run");
63
162
  this.emit({ type: "error", sessionId: this.id, runId, error: errorToErrorInfo(error) });
@@ -70,15 +169,34 @@ class RuntimeAgentSession {
70
169
  this.activeLedger = options.runLedger ?? this.agent.config.runLedger;
71
170
  this.activeOwnership = options.ownership ?? this.agent.config.ownership;
72
171
  this.activeIdempotencyKey = options.idempotencyKey ?? this.agent.config.idempotencyKey;
172
+ this.activeGuardrails = mergeGuardrails(this.agent.config.guardrails, options.guardrails);
173
+ this.activeDurable = resumed ?? (durableOptions ? { options: durableOptions, version: 0 } : undefined);
174
+ if (resumed)
175
+ this.invalidateSnapshot();
73
176
  const model = options.model ?? this.agent.config.model;
74
177
  const startedAt = new Date().toISOString();
75
178
  let runError;
179
+ let runStatus = "succeeded";
76
180
  const runUsage = createUsageAccumulator();
77
181
  let usage;
182
+ const metadata = { ...this.agent.config.metadata, ...this.metadata, ...options.metadata };
183
+ this.activeMetadata = metadata;
184
+ const limits = new RunLimitTracker(resolvedLimits, {
185
+ onExceeded: (breach) => {
186
+ this.emit({ type: "run_limit_exceeded", sessionId: this.id, runId, breach });
187
+ controller.abort(new RunLimitError(breach));
188
+ },
189
+ snapshot: resumed?.state?.counters,
190
+ deadlineAt: resumed?.state?.deadlineAt,
191
+ });
192
+ this.activeLimits = limits;
193
+ this.activeLimitOutputBuffer = [this.agent.config.limits, requestedLimits].some((value) => value?.maxOutputTokens !== undefined || value?.maxTotalTokens !== undefined || value?.maxCost !== undefined);
78
194
  try {
79
195
  this.resolveRunProvider(options);
80
196
  throwIfAborted(controller.signal);
81
197
  this.emit({ type: "agent_started", sessionId: this.id, runId });
198
+ if (resumed)
199
+ this.emit({ type: "agent_resumed", sessionId: this.id, runId, version: resumed.version });
82
200
  const startRecord = {
83
201
  id: runId,
84
202
  sessionId: this.id,
@@ -98,11 +216,27 @@ class RuntimeAgentSession {
98
216
  await this.appendEntry(createSessionEntry({ sessionId: this.id, parentId: this.currentLeafId, runId, kind: "model_change", previousModel: this.agent.config.model, model: options.model }));
99
217
  }
100
218
  const inputMessages = inputToMessages(input).map((message) => this.redact(message));
219
+ const inputGuardrails = await runGuardrails({
220
+ stage: "input",
221
+ guardrails: this.activeGuardrails,
222
+ value: inputMessages,
223
+ context: { sessionId: this.id, runId, metadata, signal: controller.signal },
224
+ redactor: this.activeRedactor,
225
+ emit: (event) => this.emit(event),
226
+ });
227
+ if (inputGuardrails.terminal?.action === "interrupt" && this.activeDurable) {
228
+ if (!resumed) {
229
+ const interruption = { kind: "input_guardrail", reason: inputGuardrails.terminal.reason ?? "Input requires approval" };
230
+ throw new AgentRunSuspended(await this.suspendDurable({ runId, model, limits, interruption, messages: inputMessages }), interruption);
231
+ }
232
+ }
233
+ else {
234
+ assertGuardrailsAllowed(inputGuardrails);
235
+ }
101
236
  for (const message of inputMessages)
102
237
  await this.appendMessage(message, runId);
103
238
  await this.autoCompact(runId, options, controller.signal, inputMessages);
104
- const metadata = { ...this.agent.config.metadata, ...this.metadata, ...options.metadata };
105
- const maxToolRounds = options.maxToolRounds ?? 1;
239
+ const maxToolRounds = resolvedLimits.maxToolRounds;
106
240
  const systemInstructions = composeSystemPrompt(mergeSystemPromptConfig(this.agent.config.systemPrompt, options.systemPrompt), { base: this.agent.config.instructions });
107
241
  const contextProviders = [
108
242
  ...(this.agent.config.context ?? []),
@@ -119,6 +253,9 @@ class RuntimeAgentSession {
119
253
  const toolConcurrency = resolveToolConcurrency(options, this.agent.config);
120
254
  this.activeLoopTurn = 1;
121
255
  const recordProviderUsage = async (turnUsage, turn, attempt) => {
256
+ limits.recordUsage(turnUsage);
257
+ if (!turnUsage)
258
+ return;
122
259
  runUsage.add(turnUsage);
123
260
  if (!this.activeLedger)
124
261
  return;
@@ -136,6 +273,7 @@ class RuntimeAgentSession {
136
273
  await this.activeLedger.appendUsage(redactRunLedgerRecord(usageRecord, this.activeRedactor));
137
274
  };
138
275
  // ponytail: LoopContext binds existing private helpers; loop orchestrates only.
276
+ let assembledTurn = false;
139
277
  const ctx = {
140
278
  sessionId: this.id,
141
279
  runId,
@@ -146,31 +284,45 @@ class RuntimeAgentSession {
146
284
  inputMessages,
147
285
  maxToolRounds,
148
286
  toolConcurrency,
149
- assemble: async (nextInput, toolResults, turn) => assembleProviderInput({
150
- model: options.model ?? this.agent.config.model,
151
- input: nextInput,
152
- history: this.history,
153
- summaries: (await this.snapshot()).summaries,
154
- toolResults: toolResults ?? [],
155
- turn,
156
- instructionInjectors,
157
- inputLayout,
158
- systemInstructions,
159
- inputBuilder: this.agent.config.inputBuilder,
160
- promptBuilder: this.agent.config.promptBuilder,
161
- contextProviders,
162
- skills: activeSkills,
163
- tools,
164
- resourceLoader: this.agent.config.resourceLoader,
165
- providerOptions,
166
- redactor: this.activeRedactor,
167
- middleware: this.agent.config.middleware,
168
- sessionId: this.id,
169
- runId,
170
- metadata,
171
- signal: controller.signal,
172
- }),
287
+ assemble: async (nextInput, toolResults, turn) => {
288
+ limits.charge("maxTurns");
289
+ const request = await assembleProviderInput({
290
+ model: options.model ?? this.agent.config.model,
291
+ input: nextInput,
292
+ history: this.history,
293
+ summaries: (await this.snapshot()).summaries,
294
+ toolResults: toolResults ?? [],
295
+ turn,
296
+ instructionInjectors,
297
+ inputLayout,
298
+ systemInstructions,
299
+ inputBuilder: this.agent.config.inputBuilder,
300
+ promptBuilder: this.agent.config.promptBuilder,
301
+ contextProviders,
302
+ skills: activeSkills,
303
+ tools,
304
+ resourceLoader: this.agent.config.resourceLoader,
305
+ permission: this.agent.config.permission,
306
+ trust: this.agent.config.trust,
307
+ providerOptions,
308
+ redactor: this.activeRedactor,
309
+ middleware: this.agent.config.middleware,
310
+ sessionId: this.id,
311
+ runId,
312
+ metadata,
313
+ signal: controller.signal,
314
+ });
315
+ assembledTurn = true;
316
+ return request;
317
+ },
318
+ chargeToolRound: (calls) => {
319
+ if (calls.length > 0)
320
+ limits.charge("maxToolRounds");
321
+ },
173
322
  generate: async (request) => {
323
+ if (!assembledTurn)
324
+ limits.charge("maxTurns");
325
+ assembledTurn = false;
174
326
  const policyResult = await this.applyProviderRequestPolicies(request, runId, options, metadata, controller.signal);
175
327
  const middlewareRequest = await this.agent.config.middleware?.run("provider_request", policyResult.request) ?? policyResult.request;
176
328
  return this.generateWithRetry(this.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, this.activeLoopTurn, recordProviderUsage);
@@ -183,9 +335,32 @@ class RuntimeAgentSession {
183
335
  middleware: this.agent.config.middleware,
184
336
  emit: (event) => this.emit(event),
185
337
  permission: this.agent.config.permission,
338
+ trust: this.agent.config.trust,
186
339
  redactor: this.activeRedactor,
187
340
  ledger: this.activeLedger,
188
341
  ownership: this.activeOwnership,
342
+ guardrails: this.activeGuardrails,
343
+ limitTracker: limits,
344
+ beforeExecute: async (mediatedCall) => {
345
+ const durable = this.activeDurable;
346
+ if (!durable)
347
+ return;
348
+ const pending = durable.state?.pending;
349
+ if (pending?.call.id === mediatedCall.id && pending.status === "ready") {
350
+ await this.persistDurable({ ...durable.state, status: "running", pending: { ...pending, status: "dispatched" }, interruption: undefined });
351
+ return;
352
+ }
353
+ if (!durable.options.interruptBeforeTool)
354
+ return;
355
+ const interruption = { kind: "tool_approval", reason: "Tool side effect requires approval", toolCallId: mediatedCall.id, toolName: mediatedCall.name };
356
+ throw new AgentRunSuspended(await this.suspendDurable({
357
+ runId,
358
+ model,
359
+ limits,
360
+ interruption,
361
+ pending: { call: mediatedCall, status: "ready" },
362
+ }), interruption);
363
+ },
189
364
  // ponytail: RunOptions wins; array-compose deferred (roadmap: compose-later).
190
365
  validate,
191
366
  }),
@@ -196,6 +371,14 @@ class RuntimeAgentSession {
196
371
  this.emit(event);
197
372
  },
198
373
  };
374
+ if (resumed?.state?.pending?.status === "ready") {
375
+ const result = await ctx.dispatchToolCall(resumed.state.pending.call);
376
+ await ctx.appendMessage({
377
+ role: "tool",
378
+ content: [{ type: "tool_result", toolCallId: result.toolCallId, name: result.name, result: result.value, error: result.error }, ...(result.content ?? [])],
379
+ metadata: result.metadata,
380
+ });
381
+ }
199
382
  const loopUsage = await loop.run(ctx);
200
383
  usage = runUsage.value() ?? loopUsage;
201
384
  if (usage && this.activeLedger) {
@@ -211,22 +394,34 @@ class RuntimeAgentSession {
211
394
  await this.activeLedger.appendUsage(redactRunLedgerRecord(usageRecord, this.activeRedactor));
212
395
  }
213
396
  await this.drainLedger();
397
+ const runState = this.activeDurable?.state
398
+ ? await this.persistDurable({ ...this.activeDurable.state, status: "succeeded", pending: undefined, interruption: undefined })
399
+ : undefined;
214
400
  this.emit({ type: "agent_finished", sessionId: this.id, runId, usage });
215
- return this.buildRunResult({
216
- runId,
217
- status: "succeeded",
218
- usage,
219
- });
401
+ return this.buildRunResult({ runId, status: "succeeded", usage, runState });
220
402
  }
221
403
  catch (error) {
404
+ if (error instanceof AgentRunSuspended) {
405
+ runStatus = "suspended";
406
+ const version = error.state.version;
407
+ this.emit({ type: "agent_suspended", sessionId: this.id, runId, interruption: error.interruption, version });
408
+ return this.buildRunResult({ runId, status: "suspended", runState: error.state, interruption: error.interruption });
409
+ }
222
410
  runError = errorToErrorInfo(error);
223
411
  this.emit({ type: "error", sessionId: this.id, runId, error: runError });
412
+ const breach = error instanceof RunLimitError ? error.breach : limits.breach;
413
+ runStatus = breach ? "failed" : controller.signal.aborted ? "aborted" : "failed";
414
+ const runState = this.activeDurable?.state
415
+ ? await this.persistDurable({ ...this.activeDurable.state, status: runStatus, interruption: undefined })
416
+ : undefined;
224
417
  const result = this.buildRunResult({
225
418
  runId,
226
- status: controller.signal.aborted ? "aborted" : "failed",
419
+ status: runStatus,
227
420
  usage: runUsage.value() ?? usage,
421
+ limit: breach,
228
422
  error: runError,
229
- abortReason: controller.signal.aborted ? String(controller.signal.reason) : undefined,
423
+ abortReason: !breach && controller.signal.aborted ? String(controller.signal.reason) : undefined,
424
+ runState,
230
425
  });
231
426
  throw new AgentRunError(result, { cause: error });
232
427
  }
@@ -236,7 +431,7 @@ class RuntimeAgentSession {
236
431
  try {
237
432
  await this.drainLedger();
238
433
  if (this.activeLedger) {
239
- const status = controller.signal.aborted ? "aborted" : runError ? "failed" : "succeeded";
434
+ const status = runStatus;
240
435
  const finishRecord = {
241
436
  id: runId,
242
437
  sessionId: this.id,
@@ -252,12 +447,19 @@ class RuntimeAgentSession {
252
447
  ...this.activeOwnership,
253
448
  };
254
449
  await this.activeLedger.appendRun(redactRunLedgerRecord(finishRecord, this.activeRedactor));
450
+ if (isFlushableRunLedger(this.activeLedger) && this.activeLedger.durability === "flush_on_terminal")
451
+ await this.activeLedger.flush();
255
452
  }
256
453
  }
257
454
  finally {
258
455
  this.activeLedger = undefined;
259
456
  this.activeOwnership = undefined;
260
457
  this.activeIdempotencyKey = undefined;
458
+ this.activeGuardrails = undefined;
459
+ this.activeMetadata = undefined;
460
+ this.activeLimits?.dispose();
461
+ this.activeLimits = undefined;
462
+ this.activeLimitOutputBuffer = false;
261
463
  this.activeRedactor = undefined;
262
464
  this.activeProvider = undefined;
263
465
  cleanupSignal();
@@ -306,10 +508,59 @@ class RuntimeAgentSession {
306
508
  content: final.content,
307
509
  message: final.message,
308
510
  usage: input.usage,
511
+ limit: input.limit,
309
512
  error: input.error,
310
513
  abortReason: input.abortReason,
514
+ runState: input.runState,
515
+ interruption: input.interruption,
311
516
  };
312
517
  }
518
+ async suspendDurable(input) {
519
+ const durable = this.activeDurable;
520
+ if (!durable)
521
+ throw new AgentRunStateError("Durable interruption is not configured");
522
+ const state = durable.state ?? initialAgentRunState({
523
+ agent: this.agent,
524
+ options: durable.options,
525
+ runId: input.runId,
526
+ sessionId: this.id,
527
+ leafId: this.currentLeafId,
528
+ model: input.model,
529
+ counters: input.limits.snapshot(),
530
+ deadlineAt: input.limits.deadlineAt,
531
+ status: "suspended",
532
+ interruption: input.interruption,
533
+ messages: input.messages,
534
+ pending: input.pending,
535
+ interruptBeforeTool: durable.options.interruptBeforeTool,
536
+ });
537
+ return this.persistDurable({
538
+ ...state,
539
+ leafId: this.currentLeafId,
540
+ status: "suspended",
541
+ interruption: input.interruption,
542
+ ...(input.messages ? { input: input.messages } : {}),
543
+ ...(input.pending ? { pending: input.pending } : {}),
544
+ counters: input.limits.snapshot(),
545
+ });
546
+ }
547
+ async persistDurable(state) {
548
+ const durable = this.activeDurable;
549
+ if (!durable)
550
+ throw new AgentRunStateError("Durable run state is not configured");
551
+ const saved = await saveAgentRunState({
552
+ checkpoints: durable.options.checkpoints,
553
+ state,
554
+ expectedVersion: durable.version,
555
+ ownership: this.activeOwnership,
556
+ fencingToken: durable.options.fencingToken,
557
+ redactor: this.activeRedactor,
558
+ maxStateBytes: durable.options.maxStateBytes,
559
+ });
560
+ durable.state = saved.state;
561
+ durable.version = saved.record.version;
562
+ return publicState(saved.state);
563
+ }
313
564
  async compact(options = {}) {
314
565
  if (this.activeRun)
315
566
  throw new Error("Agent session already has an active run");
@@ -326,6 +577,7 @@ class RuntimeAgentSession {
326
577
  }
327
578
  async checkout(leafId) {
328
579
  this.currentLeafId = leafId;
580
+ this.invalidateSnapshot();
329
581
  await this.rebuildHistory();
330
582
  }
331
583
  fork(options = {}) {
@@ -426,6 +678,8 @@ class RuntimeAgentSession {
426
678
  return await this.generateProviderTurn(request, runId, signal, secrets, turn, attempt, recordUsage);
427
679
  }
428
680
  catch (error) {
681
+ if (error instanceof GuardrailError)
682
+ throw error;
429
683
  const failure = error instanceof ProviderTurnFailure ? error : undefined;
430
684
  const info = failure ? redactSecrets(failure.info, secrets) : errorToErrorInfo(error, secrets);
431
685
  if (!policy || failure?.observable)
@@ -443,6 +697,8 @@ class RuntimeAgentSession {
443
697
  }
444
698
  }
445
699
  async generateProviderTurn(request, runId, signal, secrets = [], turn = 1, attempt = 1, recordUsage) {
700
+ this.activeLimits.charge("maxProviderAttempts");
701
+ this.activeLimits.charge("maxRequestBytes", jsonBytes(request));
446
702
  const startedAt = performance.now();
447
703
  const providerId = this.activeProvider?.id ?? request.model.provider;
448
704
  const buildMetadata = (extra = {}) => createProviderTurnMetadata(request, providerId, { attempt, ...extra });
@@ -460,8 +716,16 @@ class RuntimeAgentSession {
460
716
  let started = false;
461
717
  let usage;
462
718
  let usageRecorded = false;
719
+ const bufferedOutput = [];
720
+ const bufferOutput = Boolean(this.activeGuardrails?.output?.length || this.activeLimitOutputBuffer);
721
+ const emitOutput = (event) => {
722
+ if (bufferOutput)
723
+ bufferedOutput.push(event);
724
+ else
725
+ this.emit(event);
726
+ };
463
727
  const recordTurnUsage = async () => {
464
- if (!usage || usageRecorded)
728
+ if (usageRecorded)
465
729
  return;
466
730
  usageRecorded = true;
467
731
  await recordUsage?.(usage, turn, attempt);
@@ -469,6 +733,7 @@ class RuntimeAgentSession {
469
733
  try {
470
734
  for await (const event of this.activeProvider.generate(request)) {
471
735
  throwIfAborted(signal);
736
+ this.activeLimits.charge("maxResponseBytes", jsonBytes(event));
472
737
  if (event.type === "error")
473
738
  throw new ProviderTurnFailure(event.error, started);
474
739
  if (event.type === "usage")
@@ -480,32 +745,45 @@ class RuntimeAgentSession {
480
745
  if (event.type === "message_start") {
481
746
  started = true;
482
747
  messageId = event.messageId;
483
- this.emit({ type: "message_started", sessionId: this.id, runId, message: { id: messageId, role: "assistant", content: [] } });
748
+ emitOutput({ type: "message_started", sessionId: this.id, runId, message: { id: messageId, role: "assistant", content: [] } });
484
749
  continue;
485
750
  }
486
751
  if (event.type === "content_delta" || event.type === "tool_call" || event.type === "tool_call_delta") {
487
752
  if (!started) {
488
753
  started = true;
489
- this.emit({ type: "message_started", sessionId: this.id, runId, message: { role: "assistant", content: [] } });
754
+ emitOutput({ type: "message_started", sessionId: this.id, runId, message: { role: "assistant", content: [] } });
490
755
  }
491
756
  if (event.type === "tool_call_delta") {
492
757
  toolDeltas.push(event);
493
- this.emit({ type: "message_delta", sessionId: this.id, runId, content: providerToolCallDeltaContent(event) });
758
+ emitOutput({ type: "message_delta", sessionId: this.id, runId, content: providerToolCallDeltaContent(event) });
494
759
  continue;
495
760
  }
496
761
  const block = providerContent(event);
497
762
  content.push(block);
498
763
  if (block.type === "tool_call")
499
764
  calls.push(block);
500
- this.emit({ type: "message_delta", sessionId: this.id, runId, content: block });
765
+ emitOutput({ type: "message_delta", sessionId: this.id, runId, content: block });
501
766
  }
502
767
  }
503
768
  for (const call of reconstructMissingToolCalls(toolDeltas, calls)) {
504
769
  content.push(call);
505
770
  calls.push(call);
506
- this.emit({ type: "message_delta", sessionId: this.id, runId, content: call });
771
+ emitOutput({ type: "message_delta", sessionId: this.id, runId, content: call });
507
772
  }
508
773
  await recordTurnUsage();
774
+ if (this.activeGuardrails?.output?.length) {
775
+ assertGuardrailsAllowed(await runGuardrails({
776
+ stage: "output",
777
+ guardrails: this.activeGuardrails,
778
+ value: { content, calls, messageId, started, usage },
779
+ context: { sessionId: this.id, runId, metadata: this.activeMetadata ?? {}, signal },
780
+ redactor: this.activeRedactor,
781
+ emit: (event) => this.emit(event),
782
+ }));
783
+ }
784
+ if (bufferOutput)
785
+ for (const event of bufferedOutput)
786
+ this.emit(event);
509
787
  const latencyMs = Math.round(performance.now() - startedAt);
510
788
  this.emit({
511
789
  type: "provider_turn_finished",
@@ -530,7 +808,7 @@ class RuntimeAgentSession {
530
808
  usage,
531
809
  error: info,
532
810
  });
533
- if (error instanceof ProviderTurnFailure)
811
+ if (error instanceof GuardrailError || error instanceof ProviderTurnFailure)
534
812
  throw error;
535
813
  throw new ProviderTurnFailure(info, started);
536
814
  }
@@ -583,6 +861,11 @@ class RuntimeAgentSession {
583
861
  idempotencyKey: this.activeIdempotencyKey,
584
862
  });
585
863
  this.currentLeafId = redacted.id;
864
+ this.invalidateSnapshot();
865
+ }
866
+ invalidateSnapshot() {
867
+ this.snapshotGeneration += 1;
868
+ this.snapshotCache = undefined;
586
869
  }
587
870
  redact(value) {
588
871
  return this.activeRedactor?.redact(value) ?? value;
@@ -594,10 +877,16 @@ class RuntimeAgentSession {
594
877
  this.history = (await this.snapshot()).messages.slice();
595
878
  }
596
879
  async snapshot() {
880
+ const now = performance.now();
881
+ const cached = this.snapshotCache;
882
+ if (cached && cached.leafId === this.currentLeafId && cached.generation === this.snapshotGeneration && cached.expiresAt > now)
883
+ return cached.value;
597
884
  const reader = this.branchReader();
598
- return reader
599
- ? rebuildSessionContext(reader, { sessionId: this.id, leafId: this.currentLeafId })
885
+ const value = reader
886
+ ? await rebuildSessionContext(reader, { sessionId: this.id, leafId: this.currentLeafId })
600
887
  : rebuildSessionContext(await this.store.list(this.id), { leafId: this.currentLeafId });
888
+ this.snapshotCache = { leafId: this.currentLeafId, generation: this.snapshotGeneration, expiresAt: now + 1_000, value };
889
+ return value;
601
890
  }
602
891
  }
603
892
  class EventSubscriber {
@@ -735,6 +1024,20 @@ function mergeCompaction(agent, run) {
735
1024
  return { ...(agent || {}), ...run };
736
1025
  return agent || undefined;
737
1026
  }
1027
+ function isBuiltInLoop(loop) {
1028
+ return typeof loop === "object" && loop !== null && "strategy" in loop;
1029
+ }
1030
+ function mergeGuardrails(agent, run) {
1031
+ if (!agent && !run)
1032
+ return undefined;
1033
+ return {
1034
+ input: [...(agent?.input ?? []), ...(run?.input ?? [])],
1035
+ output: [...(agent?.output ?? []), ...(run?.output ?? [])],
1036
+ toolInput: [...(agent?.toolInput ?? []), ...(run?.toolInput ?? [])],
1037
+ toolOutput: [...(agent?.toolOutput ?? []), ...(run?.toolOutput ?? [])],
1038
+ maxConcurrency: run?.maxConcurrency ?? agent?.maxConcurrency,
1039
+ };
1040
+ }
738
1041
  function withoutTrailingInput(messages, input) {
739
1042
  const next = [...messages];
740
1043
  for (let i = input.length - 1; i >= 0; i -= 1) {
@@ -761,6 +1064,14 @@ function throwIfAbortedSignal(signal) {
761
1064
  if (signal?.aborted)
762
1065
  throw signal.reason instanceof Error ? signal.reason : new Error("Agent run aborted");
763
1066
  }
1067
+ function jsonBytes(value) {
1068
+ try {
1069
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
1070
+ }
1071
+ catch {
1072
+ throw new TypeError("Provider request or event must be JSON-serializable for run limits");
1073
+ }
1074
+ }
764
1075
  function createUsageAccumulator() {
765
1076
  const sums = new Map();
766
1077
  let costCurrency;