@arnilo/prism 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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";
@@ -12,6 +13,8 @@ import { composeSystemPrompt, mergeSystemPromptConfig } from "./system-prompts.j
12
13
  import { createDefaultRetryPolicy, waitForRetry } from "./retry.js";
13
14
  import { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, rebuildSessionContext } from "./session-stores.js";
14
15
  import { createToolRegistry, dispatchToolCall } from "./tools.js";
16
+ import { RunLimitError, RunLimitTracker, resolveRunLimits } from "./run-limits.js";
17
+ import { agentFingerprint, initialAgentRunState, loadAgentRunState, publicState, saveAgentRunState, validateRunStateOptions } from "./agent-run-state.js";
15
18
  import { resolveActiveSkills } from "./skills.js";
16
19
  export function createAgent(config) {
17
20
  return {
@@ -24,6 +27,60 @@ export function createAgent(config) {
24
27
  export function createAgentSession(config) {
25
28
  return new RuntimeAgentSession(config);
26
29
  }
30
+ /** Resume a persisted built-in run. A claimed/dispatched tool is never replayed automatically. */
31
+ export async function resumeAgentRun(agent, ref, resume, options) {
32
+ const { record, state } = await loadAgentRunState(options.checkpoints, ref, options.ownership);
33
+ if (state.definitionRevision !== options.definitionRevision || state.agentId !== (agent.config.id ?? agent.config.name) || state.fingerprint !== agentFingerprint(agent, options.definitionRevision)) {
34
+ throw new AgentRunStateError("Agent definition revision or fingerprint mismatch on resume");
35
+ }
36
+ if (record.version !== resume.expectedVersion || state.status !== "suspended") {
37
+ throw new AgentRunStateError("Stale or non-suspended agent run resume");
38
+ }
39
+ if (resume.decision === "deny") {
40
+ const denied = await saveAgentRunState({
41
+ checkpoints: options.checkpoints,
42
+ state: { ...state, status: "denied" },
43
+ expectedVersion: record.version,
44
+ ownership: options.ownership,
45
+ fencingToken: options.fencingToken,
46
+ });
47
+ const session = new RuntimeAgentSession({ agent, id: state.sessionId, leafId: state.leafId });
48
+ await session.recordDurableDenial(state.runId, state.interruption, denied.record.version, options.ownership);
49
+ const runState = publicState(denied.state);
50
+ return { sessionId: state.sessionId, runId: state.runId, status: "denied", leafId: state.leafId, text: "", content: [], runState, interruption: state.interruption };
51
+ }
52
+ if (state.pending?.status === "dispatched")
53
+ throw new AgentRunStateError("Ambiguous dispatched tool requires operator resolution");
54
+ const configured = agent.config.runState;
55
+ if (configured && (configured.checkpoints !== options.checkpoints || configured.definitionRevision !== options.definitionRevision)) {
56
+ throw new AgentRunStateError("Agent durable run-state configuration mismatch on resume");
57
+ }
58
+ const claimed = await saveAgentRunState({
59
+ checkpoints: options.checkpoints,
60
+ state: { ...state, status: "running", interruption: undefined },
61
+ expectedVersion: record.version,
62
+ ownership: options.ownership,
63
+ fencingToken: options.fencingToken,
64
+ });
65
+ const session = new RuntimeAgentSession({ agent, id: state.sessionId, leafId: state.leafId });
66
+ return session.resumeDurable(claimed.state, configured ?? {
67
+ checkpoints: options.checkpoints,
68
+ definitionRevision: options.definitionRevision,
69
+ interruptBeforeTool: state.interruptBeforeTool,
70
+ fencingToken: options.fencingToken,
71
+ }, options.ownership);
72
+ }
73
+ class AgentRunSuspended extends Error {
74
+ state;
75
+ interruption;
76
+ code = "ERR_PRISM_AGENT_RUN_SUSPENDED";
77
+ constructor(state, interruption) {
78
+ super("Agent run suspended");
79
+ this.state = state;
80
+ this.interruption = interruption;
81
+ this.name = "AgentRunSuspended";
82
+ }
83
+ }
27
84
  class RuntimeAgentSession {
28
85
  id;
29
86
  agent;
@@ -38,6 +95,11 @@ class RuntimeAgentSession {
38
95
  activeLedger;
39
96
  activeOwnership;
40
97
  activeIdempotencyKey;
98
+ activeGuardrails;
99
+ activeMetadata;
100
+ activeLimits;
101
+ activeLimitOutputBuffer = false;
102
+ activeDurable;
41
103
  activeLoopTurn = 1;
42
104
  ledgerChain = Promise.resolve();
43
105
  ledgerFailure;
@@ -57,7 +119,41 @@ class RuntimeAgentSession {
57
119
  return subscriber;
58
120
  }
59
121
  async run(input, options = {}) {
60
- const runId = randomId("run");
122
+ return this.runInternal(input, options, randomId("run"));
123
+ }
124
+ async resumeDurable(state, runState, ownership) {
125
+ return this.runInternal(state.input ?? [], { runState, ownership }, state.runId, { options: runState, state, version: state.version });
126
+ }
127
+ async recordDurableDenial(runId, interruption, version, ownership) {
128
+ this.activeLedger = this.agent.config.runLedger;
129
+ this.activeOwnership = ownership ?? this.agent.config.ownership;
130
+ this.activeRedactor = this.agent.config.redactor;
131
+ this.emit({ type: "agent_denied", sessionId: this.id, runId, interruption, version });
132
+ await this.drainLedger();
133
+ this.activeLedger = undefined;
134
+ this.activeOwnership = undefined;
135
+ this.activeRedactor = undefined;
136
+ }
137
+ async runInternal(input, options, runId, resumed) {
138
+ if (this.agent.config.secure && (options.redactor !== undefined || options.ownership !== undefined || options.validate !== undefined || options.runState !== undefined)) {
139
+ throw new AgentRunStateError("Secure agent defaults cannot be replaced per run");
140
+ }
141
+ const requestedLimits = options.maxToolRounds === undefined
142
+ ? options.limits
143
+ : { ...options.limits, maxToolRounds: Math.min(options.maxToolRounds, options.limits?.maxToolRounds ?? options.maxToolRounds) };
144
+ const resolvedLimits = resolveRunLimits(this.agent.config.limits, requestedLimits);
145
+ const durableOptions = options.runState ?? this.agent.config.runState;
146
+ if (this.agent.config.runState && options.runState && this.agent.config.runState !== options.runState) {
147
+ throw new AgentRunStateError("RunOptions cannot replace agent durable run-state configuration");
148
+ }
149
+ if (durableOptions) {
150
+ validateRunStateOptions(durableOptions);
151
+ if (options.model || options.guardrails || options.loop)
152
+ throw new AgentRunStateError("Durable runs require model, guardrails, and loop on AgentConfig for fingerprinting");
153
+ const configuredLoop = this.agent.config.loop;
154
+ if (configuredLoop && !isBuiltInLoop(configuredLoop))
155
+ throw new AgentRunStateError("Custom AgentLoopStrategy is not durable");
156
+ }
61
157
  if (this.activeRun) {
62
158
  const error = new Error("Agent session already has an active run");
63
159
  this.emit({ type: "error", sessionId: this.id, runId, error: errorToErrorInfo(error) });
@@ -70,15 +166,32 @@ class RuntimeAgentSession {
70
166
  this.activeLedger = options.runLedger ?? this.agent.config.runLedger;
71
167
  this.activeOwnership = options.ownership ?? this.agent.config.ownership;
72
168
  this.activeIdempotencyKey = options.idempotencyKey ?? this.agent.config.idempotencyKey;
169
+ this.activeGuardrails = mergeGuardrails(this.agent.config.guardrails, options.guardrails);
170
+ this.activeDurable = resumed ?? (durableOptions ? { options: durableOptions, version: 0 } : undefined);
73
171
  const model = options.model ?? this.agent.config.model;
74
172
  const startedAt = new Date().toISOString();
75
173
  let runError;
174
+ let runStatus = "succeeded";
76
175
  const runUsage = createUsageAccumulator();
77
176
  let usage;
177
+ const metadata = { ...this.agent.config.metadata, ...this.metadata, ...options.metadata };
178
+ this.activeMetadata = metadata;
179
+ const limits = new RunLimitTracker(resolvedLimits, {
180
+ onExceeded: (breach) => {
181
+ this.emit({ type: "run_limit_exceeded", sessionId: this.id, runId, breach });
182
+ controller.abort(new RunLimitError(breach));
183
+ },
184
+ snapshot: resumed?.state?.counters,
185
+ deadlineAt: resumed?.state?.deadlineAt,
186
+ });
187
+ this.activeLimits = limits;
188
+ this.activeLimitOutputBuffer = [this.agent.config.limits, requestedLimits].some((value) => value?.maxOutputTokens !== undefined || value?.maxTotalTokens !== undefined || value?.maxCost !== undefined);
78
189
  try {
79
190
  this.resolveRunProvider(options);
80
191
  throwIfAborted(controller.signal);
81
192
  this.emit({ type: "agent_started", sessionId: this.id, runId });
193
+ if (resumed)
194
+ this.emit({ type: "agent_resumed", sessionId: this.id, runId, version: resumed.version });
82
195
  const startRecord = {
83
196
  id: runId,
84
197
  sessionId: this.id,
@@ -98,11 +211,27 @@ class RuntimeAgentSession {
98
211
  await this.appendEntry(createSessionEntry({ sessionId: this.id, parentId: this.currentLeafId, runId, kind: "model_change", previousModel: this.agent.config.model, model: options.model }));
99
212
  }
100
213
  const inputMessages = inputToMessages(input).map((message) => this.redact(message));
214
+ const inputGuardrails = await runGuardrails({
215
+ stage: "input",
216
+ guardrails: this.activeGuardrails,
217
+ value: inputMessages,
218
+ context: { sessionId: this.id, runId, metadata, signal: controller.signal },
219
+ redactor: this.activeRedactor,
220
+ emit: (event) => this.emit(event),
221
+ });
222
+ if (inputGuardrails.terminal?.action === "interrupt" && this.activeDurable) {
223
+ if (!resumed) {
224
+ const interruption = { kind: "input_guardrail", reason: inputGuardrails.terminal.reason ?? "Input requires approval" };
225
+ throw new AgentRunSuspended(await this.suspendDurable({ runId, model, limits, interruption, messages: inputMessages }), interruption);
226
+ }
227
+ }
228
+ else {
229
+ assertGuardrailsAllowed(inputGuardrails);
230
+ }
101
231
  for (const message of inputMessages)
102
232
  await this.appendMessage(message, runId);
103
233
  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;
234
+ const maxToolRounds = resolvedLimits.maxToolRounds;
106
235
  const systemInstructions = composeSystemPrompt(mergeSystemPromptConfig(this.agent.config.systemPrompt, options.systemPrompt), { base: this.agent.config.instructions });
107
236
  const contextProviders = [
108
237
  ...(this.agent.config.context ?? []),
@@ -119,6 +248,9 @@ class RuntimeAgentSession {
119
248
  const toolConcurrency = resolveToolConcurrency(options, this.agent.config);
120
249
  this.activeLoopTurn = 1;
121
250
  const recordProviderUsage = async (turnUsage, turn, attempt) => {
251
+ limits.recordUsage(turnUsage);
252
+ if (!turnUsage)
253
+ return;
122
254
  runUsage.add(turnUsage);
123
255
  if (!this.activeLedger)
124
256
  return;
@@ -136,6 +268,7 @@ class RuntimeAgentSession {
136
268
  await this.activeLedger.appendUsage(redactRunLedgerRecord(usageRecord, this.activeRedactor));
137
269
  };
138
270
  // ponytail: LoopContext binds existing private helpers; loop orchestrates only.
271
+ let assembledTurn = false;
139
272
  const ctx = {
140
273
  sessionId: this.id,
141
274
  runId,
@@ -146,31 +279,45 @@ class RuntimeAgentSession {
146
279
  inputMessages,
147
280
  maxToolRounds,
148
281
  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
- }),
282
+ assemble: async (nextInput, toolResults, turn) => {
283
+ limits.charge("maxTurns");
284
+ const request = await assembleProviderInput({
285
+ model: options.model ?? this.agent.config.model,
286
+ input: nextInput,
287
+ history: this.history,
288
+ summaries: (await this.snapshot()).summaries,
289
+ toolResults: toolResults ?? [],
290
+ turn,
291
+ instructionInjectors,
292
+ inputLayout,
293
+ systemInstructions,
294
+ inputBuilder: this.agent.config.inputBuilder,
295
+ promptBuilder: this.agent.config.promptBuilder,
296
+ contextProviders,
297
+ skills: activeSkills,
298
+ tools,
299
+ resourceLoader: this.agent.config.resourceLoader,
300
+ permission: this.agent.config.permission,
301
+ trust: this.agent.config.trust,
302
+ providerOptions,
303
+ redactor: this.activeRedactor,
304
+ middleware: this.agent.config.middleware,
305
+ sessionId: this.id,
306
+ runId,
307
+ metadata,
308
+ signal: controller.signal,
309
+ });
310
+ assembledTurn = true;
311
+ return request;
312
+ },
313
+ chargeToolRound: (calls) => {
314
+ if (calls.length > 0)
315
+ limits.charge("maxToolRounds");
316
+ },
173
317
  generate: async (request) => {
318
+ if (!assembledTurn)
319
+ limits.charge("maxTurns");
320
+ assembledTurn = false;
174
321
  const policyResult = await this.applyProviderRequestPolicies(request, runId, options, metadata, controller.signal);
175
322
  const middlewareRequest = await this.agent.config.middleware?.run("provider_request", policyResult.request) ?? policyResult.request;
176
323
  return this.generateWithRetry(this.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, this.activeLoopTurn, recordProviderUsage);
@@ -183,9 +330,32 @@ class RuntimeAgentSession {
183
330
  middleware: this.agent.config.middleware,
184
331
  emit: (event) => this.emit(event),
185
332
  permission: this.agent.config.permission,
333
+ trust: this.agent.config.trust,
186
334
  redactor: this.activeRedactor,
187
335
  ledger: this.activeLedger,
188
336
  ownership: this.activeOwnership,
337
+ guardrails: this.activeGuardrails,
338
+ limitTracker: limits,
339
+ beforeExecute: async (mediatedCall) => {
340
+ const durable = this.activeDurable;
341
+ if (!durable)
342
+ return;
343
+ const pending = durable.state?.pending;
344
+ if (pending?.call.id === mediatedCall.id && pending.status === "ready") {
345
+ await this.persistDurable({ ...durable.state, status: "running", pending: { ...pending, status: "dispatched" }, interruption: undefined });
346
+ return;
347
+ }
348
+ if (!durable.options.interruptBeforeTool)
349
+ return;
350
+ const interruption = { kind: "tool_approval", reason: "Tool side effect requires approval", toolCallId: mediatedCall.id, toolName: mediatedCall.name };
351
+ throw new AgentRunSuspended(await this.suspendDurable({
352
+ runId,
353
+ model,
354
+ limits,
355
+ interruption,
356
+ pending: { call: mediatedCall, status: "ready" },
357
+ }), interruption);
358
+ },
189
359
  // ponytail: RunOptions wins; array-compose deferred (roadmap: compose-later).
190
360
  validate,
191
361
  }),
@@ -196,6 +366,14 @@ class RuntimeAgentSession {
196
366
  this.emit(event);
197
367
  },
198
368
  };
369
+ if (resumed?.state?.pending?.status === "ready") {
370
+ const result = await ctx.dispatchToolCall(resumed.state.pending.call);
371
+ await ctx.appendMessage({
372
+ role: "tool",
373
+ content: [{ type: "tool_result", toolCallId: result.toolCallId, name: result.name, result: result.value, error: result.error }, ...(result.content ?? [])],
374
+ metadata: result.metadata,
375
+ });
376
+ }
199
377
  const loopUsage = await loop.run(ctx);
200
378
  usage = runUsage.value() ?? loopUsage;
201
379
  if (usage && this.activeLedger) {
@@ -211,22 +389,34 @@ class RuntimeAgentSession {
211
389
  await this.activeLedger.appendUsage(redactRunLedgerRecord(usageRecord, this.activeRedactor));
212
390
  }
213
391
  await this.drainLedger();
392
+ const runState = this.activeDurable?.state
393
+ ? await this.persistDurable({ ...this.activeDurable.state, status: "succeeded", pending: undefined, interruption: undefined })
394
+ : undefined;
214
395
  this.emit({ type: "agent_finished", sessionId: this.id, runId, usage });
215
- return this.buildRunResult({
216
- runId,
217
- status: "succeeded",
218
- usage,
219
- });
396
+ return this.buildRunResult({ runId, status: "succeeded", usage, runState });
220
397
  }
221
398
  catch (error) {
399
+ if (error instanceof AgentRunSuspended) {
400
+ runStatus = "suspended";
401
+ const version = error.state.version;
402
+ this.emit({ type: "agent_suspended", sessionId: this.id, runId, interruption: error.interruption, version });
403
+ return this.buildRunResult({ runId, status: "suspended", runState: error.state, interruption: error.interruption });
404
+ }
222
405
  runError = errorToErrorInfo(error);
223
406
  this.emit({ type: "error", sessionId: this.id, runId, error: runError });
407
+ const breach = error instanceof RunLimitError ? error.breach : limits.breach;
408
+ runStatus = breach ? "failed" : controller.signal.aborted ? "aborted" : "failed";
409
+ const runState = this.activeDurable?.state
410
+ ? await this.persistDurable({ ...this.activeDurable.state, status: runStatus, interruption: undefined })
411
+ : undefined;
224
412
  const result = this.buildRunResult({
225
413
  runId,
226
- status: controller.signal.aborted ? "aborted" : "failed",
414
+ status: runStatus,
227
415
  usage: runUsage.value() ?? usage,
416
+ limit: breach,
228
417
  error: runError,
229
- abortReason: controller.signal.aborted ? String(controller.signal.reason) : undefined,
418
+ abortReason: !breach && controller.signal.aborted ? String(controller.signal.reason) : undefined,
419
+ runState,
230
420
  });
231
421
  throw new AgentRunError(result, { cause: error });
232
422
  }
@@ -236,7 +426,7 @@ class RuntimeAgentSession {
236
426
  try {
237
427
  await this.drainLedger();
238
428
  if (this.activeLedger) {
239
- const status = controller.signal.aborted ? "aborted" : runError ? "failed" : "succeeded";
429
+ const status = runStatus;
240
430
  const finishRecord = {
241
431
  id: runId,
242
432
  sessionId: this.id,
@@ -258,6 +448,11 @@ class RuntimeAgentSession {
258
448
  this.activeLedger = undefined;
259
449
  this.activeOwnership = undefined;
260
450
  this.activeIdempotencyKey = undefined;
451
+ this.activeGuardrails = undefined;
452
+ this.activeMetadata = undefined;
453
+ this.activeLimits?.dispose();
454
+ this.activeLimits = undefined;
455
+ this.activeLimitOutputBuffer = false;
261
456
  this.activeRedactor = undefined;
262
457
  this.activeProvider = undefined;
263
458
  cleanupSignal();
@@ -306,10 +501,59 @@ class RuntimeAgentSession {
306
501
  content: final.content,
307
502
  message: final.message,
308
503
  usage: input.usage,
504
+ limit: input.limit,
309
505
  error: input.error,
310
506
  abortReason: input.abortReason,
507
+ runState: input.runState,
508
+ interruption: input.interruption,
311
509
  };
312
510
  }
511
+ async suspendDurable(input) {
512
+ const durable = this.activeDurable;
513
+ if (!durable)
514
+ throw new AgentRunStateError("Durable interruption is not configured");
515
+ const state = durable.state ?? initialAgentRunState({
516
+ agent: this.agent,
517
+ options: durable.options,
518
+ runId: input.runId,
519
+ sessionId: this.id,
520
+ leafId: this.currentLeafId,
521
+ model: input.model,
522
+ counters: input.limits.snapshot(),
523
+ deadlineAt: input.limits.deadlineAt,
524
+ status: "suspended",
525
+ interruption: input.interruption,
526
+ messages: input.messages,
527
+ pending: input.pending,
528
+ interruptBeforeTool: durable.options.interruptBeforeTool,
529
+ });
530
+ return this.persistDurable({
531
+ ...state,
532
+ leafId: this.currentLeafId,
533
+ status: "suspended",
534
+ interruption: input.interruption,
535
+ ...(input.messages ? { input: input.messages } : {}),
536
+ ...(input.pending ? { pending: input.pending } : {}),
537
+ counters: input.limits.snapshot(),
538
+ });
539
+ }
540
+ async persistDurable(state) {
541
+ const durable = this.activeDurable;
542
+ if (!durable)
543
+ throw new AgentRunStateError("Durable run state is not configured");
544
+ const saved = await saveAgentRunState({
545
+ checkpoints: durable.options.checkpoints,
546
+ state,
547
+ expectedVersion: durable.version,
548
+ ownership: this.activeOwnership,
549
+ fencingToken: durable.options.fencingToken,
550
+ redactor: this.activeRedactor,
551
+ maxStateBytes: durable.options.maxStateBytes,
552
+ });
553
+ durable.state = saved.state;
554
+ durable.version = saved.record.version;
555
+ return publicState(saved.state);
556
+ }
313
557
  async compact(options = {}) {
314
558
  if (this.activeRun)
315
559
  throw new Error("Agent session already has an active run");
@@ -426,6 +670,8 @@ class RuntimeAgentSession {
426
670
  return await this.generateProviderTurn(request, runId, signal, secrets, turn, attempt, recordUsage);
427
671
  }
428
672
  catch (error) {
673
+ if (error instanceof GuardrailError)
674
+ throw error;
429
675
  const failure = error instanceof ProviderTurnFailure ? error : undefined;
430
676
  const info = failure ? redactSecrets(failure.info, secrets) : errorToErrorInfo(error, secrets);
431
677
  if (!policy || failure?.observable)
@@ -443,6 +689,8 @@ class RuntimeAgentSession {
443
689
  }
444
690
  }
445
691
  async generateProviderTurn(request, runId, signal, secrets = [], turn = 1, attempt = 1, recordUsage) {
692
+ this.activeLimits.charge("maxProviderAttempts");
693
+ this.activeLimits.charge("maxRequestBytes", jsonBytes(request));
446
694
  const startedAt = performance.now();
447
695
  const providerId = this.activeProvider?.id ?? request.model.provider;
448
696
  const buildMetadata = (extra = {}) => createProviderTurnMetadata(request, providerId, { attempt, ...extra });
@@ -460,8 +708,16 @@ class RuntimeAgentSession {
460
708
  let started = false;
461
709
  let usage;
462
710
  let usageRecorded = false;
711
+ const bufferedOutput = [];
712
+ const bufferOutput = Boolean(this.activeGuardrails?.output?.length || this.activeLimitOutputBuffer);
713
+ const emitOutput = (event) => {
714
+ if (bufferOutput)
715
+ bufferedOutput.push(event);
716
+ else
717
+ this.emit(event);
718
+ };
463
719
  const recordTurnUsage = async () => {
464
- if (!usage || usageRecorded)
720
+ if (usageRecorded)
465
721
  return;
466
722
  usageRecorded = true;
467
723
  await recordUsage?.(usage, turn, attempt);
@@ -469,6 +725,7 @@ class RuntimeAgentSession {
469
725
  try {
470
726
  for await (const event of this.activeProvider.generate(request)) {
471
727
  throwIfAborted(signal);
728
+ this.activeLimits.charge("maxResponseBytes", jsonBytes(event));
472
729
  if (event.type === "error")
473
730
  throw new ProviderTurnFailure(event.error, started);
474
731
  if (event.type === "usage")
@@ -480,32 +737,45 @@ class RuntimeAgentSession {
480
737
  if (event.type === "message_start") {
481
738
  started = true;
482
739
  messageId = event.messageId;
483
- this.emit({ type: "message_started", sessionId: this.id, runId, message: { id: messageId, role: "assistant", content: [] } });
740
+ emitOutput({ type: "message_started", sessionId: this.id, runId, message: { id: messageId, role: "assistant", content: [] } });
484
741
  continue;
485
742
  }
486
743
  if (event.type === "content_delta" || event.type === "tool_call" || event.type === "tool_call_delta") {
487
744
  if (!started) {
488
745
  started = true;
489
- this.emit({ type: "message_started", sessionId: this.id, runId, message: { role: "assistant", content: [] } });
746
+ emitOutput({ type: "message_started", sessionId: this.id, runId, message: { role: "assistant", content: [] } });
490
747
  }
491
748
  if (event.type === "tool_call_delta") {
492
749
  toolDeltas.push(event);
493
- this.emit({ type: "message_delta", sessionId: this.id, runId, content: providerToolCallDeltaContent(event) });
750
+ emitOutput({ type: "message_delta", sessionId: this.id, runId, content: providerToolCallDeltaContent(event) });
494
751
  continue;
495
752
  }
496
753
  const block = providerContent(event);
497
754
  content.push(block);
498
755
  if (block.type === "tool_call")
499
756
  calls.push(block);
500
- this.emit({ type: "message_delta", sessionId: this.id, runId, content: block });
757
+ emitOutput({ type: "message_delta", sessionId: this.id, runId, content: block });
501
758
  }
502
759
  }
503
760
  for (const call of reconstructMissingToolCalls(toolDeltas, calls)) {
504
761
  content.push(call);
505
762
  calls.push(call);
506
- this.emit({ type: "message_delta", sessionId: this.id, runId, content: call });
763
+ emitOutput({ type: "message_delta", sessionId: this.id, runId, content: call });
507
764
  }
508
765
  await recordTurnUsage();
766
+ if (this.activeGuardrails?.output?.length) {
767
+ assertGuardrailsAllowed(await runGuardrails({
768
+ stage: "output",
769
+ guardrails: this.activeGuardrails,
770
+ value: { content, calls, messageId, started, usage },
771
+ context: { sessionId: this.id, runId, metadata: this.activeMetadata ?? {}, signal },
772
+ redactor: this.activeRedactor,
773
+ emit: (event) => this.emit(event),
774
+ }));
775
+ }
776
+ if (bufferOutput)
777
+ for (const event of bufferedOutput)
778
+ this.emit(event);
509
779
  const latencyMs = Math.round(performance.now() - startedAt);
510
780
  this.emit({
511
781
  type: "provider_turn_finished",
@@ -530,7 +800,7 @@ class RuntimeAgentSession {
530
800
  usage,
531
801
  error: info,
532
802
  });
533
- if (error instanceof ProviderTurnFailure)
803
+ if (error instanceof GuardrailError || error instanceof ProviderTurnFailure)
534
804
  throw error;
535
805
  throw new ProviderTurnFailure(info, started);
536
806
  }
@@ -735,6 +1005,20 @@ function mergeCompaction(agent, run) {
735
1005
  return { ...(agent || {}), ...run };
736
1006
  return agent || undefined;
737
1007
  }
1008
+ function isBuiltInLoop(loop) {
1009
+ return typeof loop === "object" && loop !== null && "strategy" in loop;
1010
+ }
1011
+ function mergeGuardrails(agent, run) {
1012
+ if (!agent && !run)
1013
+ return undefined;
1014
+ return {
1015
+ input: [...(agent?.input ?? []), ...(run?.input ?? [])],
1016
+ output: [...(agent?.output ?? []), ...(run?.output ?? [])],
1017
+ toolInput: [...(agent?.toolInput ?? []), ...(run?.toolInput ?? [])],
1018
+ toolOutput: [...(agent?.toolOutput ?? []), ...(run?.toolOutput ?? [])],
1019
+ maxConcurrency: run?.maxConcurrency ?? agent?.maxConcurrency,
1020
+ };
1021
+ }
738
1022
  function withoutTrailingInput(messages, input) {
739
1023
  const next = [...messages];
740
1024
  for (let i = input.length - 1; i >= 0; i -= 1) {
@@ -761,6 +1045,14 @@ function throwIfAbortedSignal(signal) {
761
1045
  if (signal?.aborted)
762
1046
  throw signal.reason instanceof Error ? signal.reason : new Error("Agent run aborted");
763
1047
  }
1048
+ function jsonBytes(value) {
1049
+ try {
1050
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
1051
+ }
1052
+ catch {
1053
+ throw new TypeError("Provider request or event must be JSON-serializable for run limits");
1054
+ }
1055
+ }
764
1056
  function createUsageAccumulator() {
765
1057
  const sums = new Map();
766
1058
  let costCurrency;