@arnilo/prism 0.0.10 → 0.0.12

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 (60) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/agent-loops.d.ts +1 -0
  3. package/dist/agent-loops.js +37 -2
  4. package/dist/agent-run-lifecycle.d.ts +5 -1
  5. package/dist/agent-run-lifecycle.js +17 -1
  6. package/dist/agents.d.ts +3 -1
  7. package/dist/agents.js +202 -24
  8. package/dist/context-budget.d.ts +63 -0
  9. package/dist/context-budget.js +235 -0
  10. package/dist/contracts.d.ts +109 -0
  11. package/dist/contracts.js +77 -0
  12. package/dist/index.d.ts +8 -6
  13. package/dist/index.js +5 -4
  14. package/dist/input.d.ts +3 -0
  15. package/dist/input.js +71 -28
  16. package/dist/node/session-store-jsonl.js +4 -1
  17. package/dist/rpc.js +13 -2
  18. package/dist/session-stores.d.ts +7 -2
  19. package/dist/session-stores.js +174 -4
  20. package/dist/structured-output.d.ts +5 -1
  21. package/dist/structured-output.js +18 -0
  22. package/dist/testing/persistence-schema.d.ts +1 -1
  23. package/dist/testing/persistence-schema.js +8 -2
  24. package/dist/testing/session-store-conformance.d.ts +6 -0
  25. package/dist/testing/session-store-conformance.js +36 -1
  26. package/docs/a2a.md +1 -0
  27. package/docs/ag-ui.md +123 -0
  28. package/docs/agent-events.md +2 -1
  29. package/docs/agent-loops.md +8 -1
  30. package/docs/agent-session-runtime.md +10 -2
  31. package/docs/cli-rpc.md +2 -1
  32. package/docs/coding-agent-tools.md +70 -1
  33. package/docs/compaction-and-retry.md +2 -1
  34. package/docs/compaction-llm.md +20 -1
  35. package/docs/credential-storage.md +2 -1
  36. package/docs/credentials-and-redaction.md +8 -0
  37. package/docs/evaluations.md +1 -1
  38. package/docs/host-security.md +2 -0
  39. package/docs/index.md +20 -17
  40. package/docs/input-and-prompt-assembly.md +4 -1
  41. package/docs/migration.md +32 -0
  42. package/docs/node-jsonl-session-store.md +1 -1
  43. package/docs/performance.md +36 -0
  44. package/docs/postgres-persistence.md +3 -3
  45. package/docs/provider-packages.md +11 -1
  46. package/docs/providers/anthropic.md +93 -0
  47. package/docs/providers/google.md +88 -0
  48. package/docs/providers/openai.md +2 -2
  49. package/docs/public-contracts.md +6 -0
  50. package/docs/release-and-install.md +232 -65
  51. package/docs/review-coverage-2026-07-22-phase-6.md +209 -0
  52. package/docs/review-coverage-2026-07-22-phase-7.md +173 -0
  53. package/docs/runs-and-usage.md +1 -0
  54. package/docs/server.md +1 -0
  55. package/docs/session-store-conformance.md +2 -0
  56. package/docs/session-stores.md +40 -1
  57. package/docs/sqlite-persistence.md +3 -3
  58. package/docs/structured-output.md +7 -1
  59. package/docs/workflows.md +2 -0
  60. package/package.json +3 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5
5
 
6
6
  All notable changes to this project will be documented in this file.
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.0.12] - 2026-07-22
11
+
12
+ ### Added
13
+
14
+ - Optional `@arnilo/prism-ag-ui` package with bounded AG-UI mapper/authorized handler/replay and stable `./acp` sibling, built over shared durable resume streams.
15
+ - `createCodingCompactionStrategy()` preset for bounded coding-session handoff.
16
+
17
+ ### Changed
18
+
19
+ - Versioned all 35 first-party manifests and exact internal ranges to `0.0.12`; `@arnilo/prism-all` includes AG-UI while `@arnilo/prism-code` and `@arnilo/prism-sdk` remain free of UI protocol dependencies.
20
+ - Added network-free interoperability/compaction evidence: `scripts/benchmark-0.0.12.mjs`.
21
+
22
+
23
+ ## [0.0.11] - 2026-07-22
24
+
25
+ ### Added
26
+
27
+ - Coding harness fundamentals for 0.0.11 (Plan 074): bounded `SessionIndex`/`searchSessions` (SQLite/Postgres FTS migration 004; memory linear|unsupported; JSONL unsupported), assembler `contextBudget` + omission reports, `@arnilo/prism-provider-anthropic` + `@arnilo/prism-provider-google`, mid-run `AgentSession.steer` / RPC steer, coding-agent `runCodingGoalVerify` and opt-in `ask_user_decision` (multi/free-text/durable suspend glue).
28
+ - Opt-in `structuredOutputTiming: "final-turn-only"` on `generate-validate-revise` (default `"every-turn"`): tool-eligible turns omit native schema so models can call tools; artifact/revision turns attach schema and withdraw tools.
29
+
30
+ ### Changed
31
+
32
+ - Versioned all 34 first-party manifests and exact internal ranges to `0.0.11` (adds `@arnilo/prism-provider-anthropic` + `@arnilo/prism-provider-google` to the publishable graph and `@arnilo/prism-providers` umbrella).
33
+ - Network-free search/budget evidence: `scripts/benchmark-0.0.11.mjs`.
34
+
8
35
  ## [0.0.10] - 2026-07-21
9
36
 
10
37
  ### Changed
@@ -7,6 +7,7 @@ export declare function generateValidateReviseLoop(opts: {
7
7
  readonly repairer?: ArtifactRepairer<unknown>;
8
8
  readonly maxRevisions?: number;
9
9
  readonly toolCalls?: "disabled" | "bounded";
10
+ readonly structuredOutputTiming?: "every-turn" | "final-turn-only";
10
11
  }): AgentLoopStrategy;
11
12
  export declare function resolveToolConcurrency(options: {
12
13
  loop?: AgentLoopStrategy | AgentLoopOptions;
@@ -1,5 +1,6 @@
1
1
  import { inputMessages } from "./input.js";
2
2
  import { createId } from "./ids.js";
3
+ import { artifactStructuredOutputRequest, withoutStructuredOutput } from "./structured-output.js";
3
4
  function throwIfAborted(signal) {
4
5
  if (signal.aborted)
5
6
  throw signal.reason instanceof Error ? signal.reason : new Error("Agent run aborted");
@@ -23,6 +24,7 @@ export const singleShotLoop = {
23
24
  let nextInput = ctx.input;
24
25
  for (let turn = 1;; turn += 1) {
25
26
  throwIfAborted(ctx.signal);
27
+ await ctx.applyPendingSteers?.();
26
28
  ctx.emit({ type: "turn_started", sessionId: ctx.sessionId, runId: ctx.runId, turn });
27
29
  const request = await ctx.assemble(nextInput, undefined, turn);
28
30
  throwIfAborted(ctx.signal);
@@ -37,8 +39,14 @@ export const singleShotLoop = {
37
39
  ctx.emit({ type: "message_finished", sessionId: ctx.sessionId, runId: ctx.runId, message });
38
40
  }
39
41
  ctx.emit({ type: "turn_finished", sessionId: ctx.sessionId, runId: ctx.runId, turn });
40
- if (calls.length === 0 || toolRounds >= ctx.maxToolRounds)
42
+ if (calls.length === 0 || toolRounds >= ctx.maxToolRounds) {
43
+ // Soft-interrupt / late steer: keep same run going when queue still has text.
44
+ if (await ctx.applyPendingSteers?.()) {
45
+ nextInput = [];
46
+ continue;
47
+ }
41
48
  break;
49
+ }
42
50
  toolRounds += 1;
43
51
  await dispatchToolCallsInOrder(calls, ctx);
44
52
  nextInput = [];
@@ -61,6 +69,7 @@ function defaultRepairer() {
61
69
  export function generateValidateReviseLoop(opts) {
62
70
  const max = opts.maxRevisions ?? 3;
63
71
  const repairer = opts.repairer ?? defaultRepairer();
72
+ const finalOnly = opts.structuredOutputTiming === "final-turn-only" && opts.toolCalls === "bounded";
64
73
  return {
65
74
  name: "generate-validate-revise",
66
75
  async run(ctx) {
@@ -69,10 +78,24 @@ export function generateValidateReviseLoop(opts) {
69
78
  let pendingHistory = [];
70
79
  let toolRounds = 0;
71
80
  let attempts = 0;
81
+ let artifactPhase = !finalOnly;
82
+ let savedSchema;
72
83
  for (let turn = 1; attempts <= max; turn += 1) {
73
84
  throwIfAborted(ctx.signal);
85
+ await ctx.applyPendingSteers?.();
74
86
  ctx.emit({ type: "turn_started", sessionId: ctx.sessionId, runId: ctx.runId, turn });
75
- const request = await ctx.assemble(nextInput, undefined, turn);
87
+ let request = await ctx.assemble(nextInput, undefined, turn);
88
+ if (request.options?.structuredOutput)
89
+ savedSchema ??= request.options.structuredOutput;
90
+ if (finalOnly) {
91
+ if (!artifactPhase && toolRounds < ctx.maxToolRounds) {
92
+ request = withoutStructuredOutput(request);
93
+ }
94
+ else {
95
+ artifactPhase = true;
96
+ request = artifactStructuredOutputRequest(request, savedSchema);
97
+ }
98
+ }
76
99
  throwIfAborted(ctx.signal);
77
100
  const { content, calls, messageId, started, usage: turnUsage } = await ctx.generate(request);
78
101
  usage = turnUsage ?? usage;
@@ -100,6 +123,17 @@ export function generateValidateReviseLoop(opts) {
100
123
  nextInput = [];
101
124
  continue;
102
125
  }
126
+ // Soft-interrupt / late steer before treating empty/final output as artifact work.
127
+ if (await ctx.applyPendingSteers?.()) {
128
+ nextInput = [];
129
+ continue;
130
+ }
131
+ // Call-free during tool phase → one more turn with schema on / tools off.
132
+ if (finalOnly && !artifactPhase) {
133
+ artifactPhase = true;
134
+ nextInput = [];
135
+ continue;
136
+ }
103
137
  const artifactCtx = {
104
138
  sessionId: ctx.sessionId,
105
139
  runId: ctx.runId,
@@ -214,6 +248,7 @@ export function resolveLoop(options, config) {
214
248
  repairer: loop.repairer,
215
249
  maxRevisions: loop.maxRevisions,
216
250
  toolCalls: loop.toolCalls,
251
+ structuredOutputTiming: loop.structuredOutputTiming,
217
252
  });
218
253
  }
219
254
  throw new Error(`Unknown agent loop strategy: ${strategy}`);
@@ -1,4 +1,4 @@
1
- import type { Agent, AgentRunRef, AgentRunResult, AgentRunResume, AgentRunStatusResult, OwnershipScope } from "./contracts.js";
1
+ import type { Agent, AgentEvent, AgentRunRef, AgentRunResult, AgentRunResume, AgentRunStatusResult, OwnershipScope, SubscribeOptions } from "./contracts.js";
2
2
  import type { CheckpointStore } from "./contracts.js";
3
3
  export interface AgentRunLifecycleAgent {
4
4
  readonly agent: Agent;
@@ -20,9 +20,13 @@ export interface AgentRunLifecycleRequest {
20
20
  /** Adapter-selected capability; stored runs for another agent are non-enumerable. */
21
21
  readonly agentId?: string;
22
22
  }
23
+ /** Bounded live-event options for a durable lifecycle resume. */
24
+ export interface AgentRunLifecycleStreamRequest extends AgentRunLifecycleRequest, SubscribeOptions {
25
+ }
23
26
  export interface AgentRunLifecycle {
24
27
  status(ref: AgentRunRef, options?: AgentRunLifecycleRequest): Promise<AgentRunStatusResult>;
25
28
  resume(ref: AgentRunRef, resume: AgentRunResume, options?: AgentRunLifecycleRequest): Promise<AgentRunResult>;
29
+ resumeStream(ref: AgentRunRef, resume: AgentRunResume, options?: AgentRunLifecycleStreamRequest): AsyncIterable<AgentEvent>;
26
30
  }
27
31
  /** Host capability for durable agent status/resume. Adapters supply authorized ownership only. */
28
32
  export declare function createAgentRunLifecycle(options: AgentRunLifecycleOptions): AgentRunLifecycle;
@@ -1,6 +1,6 @@
1
1
  import { AgentRunStateError } from "./contracts.js";
2
2
  import { loadAgentRunState, publicState } from "./agent-run-state.js";
3
- import { resumeAgentRun } from "./agents.js";
3
+ import { resumeAgentRun, resumeAgentRunStream } from "./agents.js";
4
4
  function assertAgentId(actual, expected) {
5
5
  if (expected !== undefined && actual !== expected)
6
6
  throw new AgentRunStateError("Agent run capability mismatch");
@@ -28,6 +28,22 @@ export function createAgentRunLifecycle(options) {
28
28
  definitionRevision: resolved.definitionRevision,
29
29
  });
30
30
  },
31
+ async *resumeStream(ref, resume, request = {}) {
32
+ request.signal?.throwIfAborted();
33
+ const { state } = await loadAgentRunState(options.checkpoints, ref, request.ownership);
34
+ assertAgentId(state.agentId, request.agentId);
35
+ const resolved = await options.resolveAgent({ agentId: state.agentId, ownership: request.ownership, signal: request.signal });
36
+ request.signal?.throwIfAborted();
37
+ yield* resumeAgentRunStream(resolved.agent, ref, resume, {
38
+ checkpoints: options.checkpoints,
39
+ ownership: request.ownership,
40
+ fencingToken: options.fencingToken,
41
+ definitionRevision: resolved.definitionRevision,
42
+ signal: request.signal,
43
+ maxQueuedEvents: request.maxQueuedEvents,
44
+ overflow: request.overflow,
45
+ });
46
+ },
31
47
  };
32
48
  }
33
49
  //# sourceMappingURL=agent-run-lifecycle.js.map
package/dist/agents.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- import type { Agent, AgentConfig, AgentRunResult, AgentRunResume, AgentRunResumeOptions, AgentRunRef, AgentSession, AgentSessionConfig } from "./contracts.js";
1
+ import type { Agent, AgentConfig, AgentEvent, AgentRunResult, AgentRunResume, AgentRunResumeOptions, AgentRunResumeStreamOptions, AgentRunRef, AgentSession, AgentSessionConfig } from "./contracts.js";
2
2
  export declare function createAgent(config: AgentConfig): Agent;
3
3
  export declare function createAgentSession(config: AgentSessionConfig & {
4
4
  readonly agent: Agent;
5
5
  }): AgentSession;
6
6
  /** Resume a persisted built-in run. A claimed/dispatched tool is never replayed automatically. */
7
7
  export declare function resumeAgentRun(agent: Agent, ref: AgentRunRef, resume: AgentRunResume, options: AgentRunResumeOptions): Promise<AgentRunResult>;
8
+ /** Subscribe before resuming one durable run. Early consumer return aborts that resumed execution. */
9
+ export declare function resumeAgentRunStream(agent: Agent, ref: AgentRunRef, resume: AgentRunResume, options: AgentRunResumeStreamOptions): AsyncGenerator<AgentEvent>;
package/dist/agents.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AgentRunError, AgentRunStateError } from "./contracts.js";
1
+ import { AgentRunError, AgentRunStateError, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, } from "./contracts.js";
2
2
  import { resolveLoop, resolveToolConcurrency } from "./agent-loops.js";
3
3
  import { createId } from "./ids.js";
4
4
  import { assertGuardrailsAllowed, GuardrailError, runGuardrails } from "./guardrails.js";
@@ -30,6 +30,32 @@ export function createAgentSession(config) {
30
30
  }
31
31
  /** Resume a persisted built-in run. A claimed/dispatched tool is never replayed automatically. */
32
32
  export async function resumeAgentRun(agent, ref, resume, options) {
33
+ return executePreparedAgentRunResume(await prepareAgentRunResume(agent, ref, resume, options));
34
+ }
35
+ /** Subscribe before resuming one durable run. Early consumer return aborts that resumed execution. */
36
+ export async function* resumeAgentRunStream(agent, ref, resume, options) {
37
+ throwIfAbortedSignal(options.signal);
38
+ const prepared = await prepareAgentRunResume(agent, ref, resume, options, options.signal);
39
+ const subscription = prepared.session.subscribe(options);
40
+ let settled = false;
41
+ const runPromise = executePreparedAgentRunResume(prepared, options.signal).finally(() => { settled = true; });
42
+ try {
43
+ for await (const event of subscription) {
44
+ if ("runId" in event && event.runId !== ref.runId)
45
+ continue;
46
+ yield event;
47
+ }
48
+ await runPromise;
49
+ }
50
+ finally {
51
+ if (!settled) {
52
+ prepared.session.abort(new Error("resume stream consumer closed"));
53
+ await runPromise.catch(() => undefined);
54
+ }
55
+ }
56
+ }
57
+ async function prepareAgentRunResume(agent, ref, resume, options, signal) {
58
+ throwIfAbortedSignal(signal);
33
59
  const { record, state } = await loadAgentRunState(options.checkpoints, ref, options.ownership);
34
60
  if (state.definitionRevision !== options.definitionRevision || state.agentId !== (agent.config.id ?? agent.config.name) || state.fingerprint !== agentFingerprint(agent, options.definitionRevision)) {
35
61
  throw new AgentRunStateError("Agent definition revision or fingerprint mismatch on resume");
@@ -37,7 +63,9 @@ export async function resumeAgentRun(agent, ref, resume, options) {
37
63
  if (record.version !== resume.expectedVersion || state.status !== "suspended") {
38
64
  throw new AgentRunStateError("Stale or non-suspended agent run resume");
39
65
  }
66
+ const session = new RuntimeAgentSession({ agent, id: state.sessionId, leafId: state.leafId });
40
67
  if (resume.decision === "deny") {
68
+ throwIfAbortedSignal(signal);
41
69
  const denied = await saveAgentRunState({
42
70
  checkpoints: options.checkpoints,
43
71
  state: { ...state, status: "denied" },
@@ -45,10 +73,23 @@ export async function resumeAgentRun(agent, ref, resume, options) {
45
73
  ownership: options.ownership,
46
74
  fencingToken: options.fencingToken,
47
75
  });
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 };
76
+ return {
77
+ kind: "deny",
78
+ session,
79
+ interruption: state.interruption,
80
+ version: denied.record.version,
81
+ ownership: options.ownership,
82
+ result: {
83
+ sessionId: state.sessionId,
84
+ runId: state.runId,
85
+ status: "denied",
86
+ leafId: state.leafId,
87
+ text: "",
88
+ content: [],
89
+ runState: publicState(denied.state),
90
+ interruption: state.interruption,
91
+ },
92
+ };
52
93
  }
53
94
  if (state.pending?.status === "dispatched")
54
95
  throw new AgentRunStateError("Ambiguous dispatched tool requires operator resolution");
@@ -56,6 +97,7 @@ export async function resumeAgentRun(agent, ref, resume, options) {
56
97
  if (configured && (configured.checkpoints !== options.checkpoints || configured.definitionRevision !== options.definitionRevision)) {
57
98
  throw new AgentRunStateError("Agent durable run-state configuration mismatch on resume");
58
99
  }
100
+ throwIfAbortedSignal(signal);
59
101
  const claimed = await saveAgentRunState({
60
102
  checkpoints: options.checkpoints,
61
103
  state: { ...state, status: "running", interruption: undefined },
@@ -63,13 +105,25 @@ export async function resumeAgentRun(agent, ref, resume, options) {
63
105
  ownership: options.ownership,
64
106
  fencingToken: options.fencingToken,
65
107
  });
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);
108
+ return {
109
+ kind: "approve",
110
+ session,
111
+ state: claimed.state,
112
+ ownership: options.ownership,
113
+ runState: configured ?? {
114
+ checkpoints: options.checkpoints,
115
+ definitionRevision: options.definitionRevision,
116
+ interruptBeforeTool: state.interruptBeforeTool,
117
+ fencingToken: options.fencingToken,
118
+ },
119
+ };
120
+ }
121
+ async function executePreparedAgentRunResume(prepared, signal) {
122
+ if (prepared.kind === "deny") {
123
+ await prepared.session.recordDurableDenial(prepared.result.runId, prepared.interruption, prepared.version, prepared.ownership);
124
+ return prepared.result;
125
+ }
126
+ return prepared.session.resumeDurable(prepared.state, prepared.runState, prepared.ownership, signal);
73
127
  }
74
128
  class AgentRunSuspended extends Error {
75
129
  state;
@@ -91,6 +145,11 @@ class RuntimeAgentSession {
91
145
  currentLeafId;
92
146
  history = [];
93
147
  activeRun;
148
+ activeRunId;
149
+ activeProviderTurnAbort;
150
+ pendingSoftInterrupt = false;
151
+ pendingSteers = [];
152
+ pendingSteerBytes = 0;
94
153
  activeRedactor;
95
154
  activeProvider;
96
155
  activeLedger;
@@ -124,18 +183,48 @@ class RuntimeAgentSession {
124
183
  async run(input, options = {}) {
125
184
  return this.runInternal(input, options, randomId("run"));
126
185
  }
127
- async resumeDurable(state, runState, ownership) {
128
- return this.runInternal(state.input ?? [], { runState, ownership }, state.runId, { options: runState, state, version: state.version });
186
+ steer(input, options = {}) {
187
+ if (!this.activeRun || !this.activeRunId)
188
+ throw new Error("Agent session has no active run to steer");
189
+ const messages = inputToMessages(input).map((message) => this.redact(message));
190
+ if (messages.length === 0)
191
+ throw new Error("steer requires non-empty input");
192
+ let addBytes = 0;
193
+ for (const message of messages)
194
+ addBytes += messageTextBytes(message);
195
+ if (this.pendingSteers.length + messages.length > DEFAULT_MAX_PENDING_STEERS) {
196
+ throw new Error(`steer queue exceeds max pending messages (${DEFAULT_MAX_PENDING_STEERS})`);
197
+ }
198
+ if (this.pendingSteerBytes + addBytes > DEFAULT_MAX_PENDING_STEER_BYTES) {
199
+ throw new Error(`steer queue exceeds max pending bytes (${DEFAULT_MAX_PENDING_STEER_BYTES})`);
200
+ }
201
+ this.pendingSteers.push(...messages);
202
+ this.pendingSteerBytes += addBytes;
203
+ this.emit({ type: "queue_updated", sessionId: this.id, runId: this.activeRunId, size: this.pendingSteers.length });
204
+ if (options.softInterrupt) {
205
+ if (this.activeProviderTurnAbort)
206
+ this.activeProviderTurnAbort.abort(new SteerSoftInterrupt());
207
+ else
208
+ this.pendingSoftInterrupt = true;
209
+ }
210
+ }
211
+ async resumeDurable(state, runState, ownership, signal) {
212
+ return this.runInternal(state.input ?? [], { runState, ownership, signal }, state.runId, { options: runState, state, version: state.version });
129
213
  }
130
214
  async recordDurableDenial(runId, interruption, version, ownership) {
131
215
  this.activeLedger = this.agent.config.runLedger;
132
216
  this.activeOwnership = ownership ?? this.agent.config.ownership;
133
217
  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;
218
+ try {
219
+ this.emit({ type: "agent_denied", sessionId: this.id, runId, interruption, version });
220
+ await this.drainLedger();
221
+ }
222
+ finally {
223
+ this.activeLedger = undefined;
224
+ this.activeOwnership = undefined;
225
+ this.activeRedactor = undefined;
226
+ this.closeSubscribers();
227
+ }
139
228
  }
140
229
  async runInternal(input, options, runId, resumed) {
141
230
  if (this.agent.config.secure && (options.redactor !== undefined || options.ownership !== undefined || options.validate !== undefined || options.runState !== undefined)) {
@@ -165,6 +254,10 @@ class RuntimeAgentSession {
165
254
  const controller = new AbortController();
166
255
  const cleanupSignal = bridgeAbort(options.signal, controller);
167
256
  this.activeRun = controller;
257
+ this.activeRunId = runId;
258
+ this.pendingSteers = [];
259
+ this.pendingSteerBytes = 0;
260
+ this.pendingSoftInterrupt = false;
168
261
  this.activeRedactor = options.redactor ?? this.agent.config.redactor;
169
262
  this.activeLedger = options.runLedger ?? this.agent.config.runLedger;
170
263
  this.activeOwnership = options.ownership ?? this.agent.config.ownership;
@@ -327,7 +420,15 @@ class RuntimeAgentSession {
327
420
  assembledTurn = false;
328
421
  const policyResult = await this.applyProviderRequestPolicies(request, runId, options, metadata, controller.signal);
329
422
  const middlewareRequest = await this.agent.config.middleware?.run("provider_request", policyResult.request) ?? policyResult.request;
330
- return this.generateWithRetry(this.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, this.activeLoopTurn, recordProviderUsage);
423
+ try {
424
+ return await this.generateWithRetry(this.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, this.activeLoopTurn, recordProviderUsage);
425
+ }
426
+ catch (error) {
427
+ if (isSteerSoftInterrupt(error)) {
428
+ return { content: [], calls: [], started: false, usage: undefined };
429
+ }
430
+ throw error;
431
+ }
331
432
  },
332
433
  isToolCallExclusive: (call) => registry.get(call.name)?.exclusive === true,
333
434
  dispatchToolCall: (call) => dispatchToolCall({
@@ -367,6 +468,8 @@ class RuntimeAgentSession {
367
468
  validate,
368
469
  }),
369
470
  appendMessage: (message) => this.appendMessage(message, runId),
471
+ hasPendingSteers: () => this.pendingSteers.length > 0,
472
+ applyPendingSteers: () => this.applyPendingSteers(runId, metadata, controller.signal),
370
473
  emit: (event) => {
371
474
  if (event.type === "turn_started")
372
475
  this.activeLoopTurn = event.turn;
@@ -443,6 +546,11 @@ class RuntimeAgentSession {
443
546
  finally {
444
547
  if (this.activeRun === controller)
445
548
  this.activeRun = undefined;
549
+ this.activeRunId = undefined;
550
+ this.activeProviderTurnAbort = undefined;
551
+ this.pendingSoftInterrupt = false;
552
+ this.pendingSteers = [];
553
+ this.pendingSteerBytes = 0;
446
554
  try {
447
555
  await this.drainLedger();
448
556
  if (this.activeLedger) {
@@ -693,7 +801,7 @@ class RuntimeAgentSession {
693
801
  return await this.generateProviderTurn(request, runId, signal, secrets, turn, attempt, recordUsage);
694
802
  }
695
803
  catch (error) {
696
- if (error instanceof GuardrailError)
804
+ if (error instanceof GuardrailError || isSteerSoftInterrupt(error))
697
805
  throw error;
698
806
  const failure = error instanceof ProviderTurnFailure ? error : undefined;
699
807
  const info = failure ? redactSecrets(failure.info, secrets) : errorToErrorInfo(error, secrets);
@@ -745,9 +853,18 @@ class RuntimeAgentSession {
745
853
  usageRecorded = true;
746
854
  await recordUsage?.(usage, turn, attempt);
747
855
  };
856
+ const turnAbort = new AbortController();
857
+ const cleanupTurn = bridgeAbort(signal, turnAbort);
858
+ this.activeProviderTurnAbort = turnAbort;
859
+ if (this.pendingSoftInterrupt) {
860
+ this.pendingSoftInterrupt = false;
861
+ turnAbort.abort(new SteerSoftInterrupt());
862
+ }
863
+ const turnRequest = { ...request, signal: turnAbort.signal };
748
864
  try {
749
- for await (const event of this.activeProvider.generate(request)) {
750
- throwIfAborted(signal);
865
+ throwIfAborted(turnAbort.signal);
866
+ for await (const event of this.activeProvider.generate(turnRequest)) {
867
+ throwIfAborted(turnAbort.signal);
751
868
  this.activeLimits.charge("maxResponseBytes", jsonBytes(event));
752
869
  if (event.type === "error")
753
870
  throw new ProviderTurnFailure(event.error, started);
@@ -791,7 +908,7 @@ class RuntimeAgentSession {
791
908
  stage: "output",
792
909
  guardrails: this.activeGuardrails,
793
910
  value: { content, calls, messageId, started, usage },
794
- context: { sessionId: this.id, runId, metadata: this.activeMetadata ?? {}, signal },
911
+ context: { sessionId: this.id, runId, metadata: this.activeMetadata ?? {}, signal: turnAbort.signal },
795
912
  redactor: this.activeRedactor,
796
913
  emit: (event) => this.emit(event),
797
914
  }));
@@ -811,6 +928,19 @@ class RuntimeAgentSession {
811
928
  return { content, calls, messageId, started, usage };
812
929
  }
813
930
  catch (error) {
931
+ if (isSteerSoftInterrupt(error) || isSteerSoftInterrupt(turnAbort.signal.reason)) {
932
+ await recordTurnUsage();
933
+ const latencyMs = Math.round(performance.now() - startedAt);
934
+ this.emit({
935
+ type: "provider_turn_finished",
936
+ sessionId: this.id,
937
+ runId,
938
+ turn,
939
+ metadata: buildMetadata({ latencyMs }),
940
+ usage,
941
+ });
942
+ throw new SteerSoftInterrupt();
943
+ }
814
944
  const latencyMs = Math.round(performance.now() - startedAt);
815
945
  const info = error instanceof ProviderTurnFailure ? redactSecrets(error.info, secrets) : errorToErrorInfo(error, secrets);
816
946
  await recordTurnUsage();
@@ -827,6 +957,33 @@ class RuntimeAgentSession {
827
957
  throw error;
828
958
  throw new ProviderTurnFailure(info, started);
829
959
  }
960
+ finally {
961
+ cleanupTurn();
962
+ if (this.activeProviderTurnAbort === turnAbort)
963
+ this.activeProviderTurnAbort = undefined;
964
+ }
965
+ }
966
+ async applyPendingSteers(runId, metadata, signal) {
967
+ if (this.pendingSteers.length === 0)
968
+ return false;
969
+ const drained = this.pendingSteers.splice(0);
970
+ this.pendingSteerBytes = 0;
971
+ this.emit({ type: "queue_updated", sessionId: this.id, runId, size: 0 });
972
+ for (const message of drained) {
973
+ throwIfAborted(signal);
974
+ const inputGuardrails = await runGuardrails({
975
+ stage: "input",
976
+ guardrails: this.activeGuardrails,
977
+ value: [message],
978
+ context: { sessionId: this.id, runId, metadata, signal },
979
+ redactor: this.activeRedactor,
980
+ emit: (event) => this.emit(event),
981
+ });
982
+ assertGuardrailsAllowed(inputGuardrails);
983
+ this.history.push(message);
984
+ await this.appendMessage(message, runId);
985
+ }
986
+ return true;
830
987
  }
831
988
  async applyProviderRequestPolicies(request, runId, options, metadata, signal) {
832
989
  const policies = [...policyList(this.agent.config.providerRequestPolicies), ...policyList(options.providerRequestPolicies)];
@@ -987,6 +1144,27 @@ function inputToMessages(input) {
987
1144
  return [input];
988
1145
  return [...input];
989
1146
  }
1147
+ const steerTextEncoder = new TextEncoder();
1148
+ function messageTextBytes(message) {
1149
+ let total = 0;
1150
+ for (const block of message.content) {
1151
+ if (block.type === "text")
1152
+ total += steerTextEncoder.encode(block.text).byteLength;
1153
+ }
1154
+ return total;
1155
+ }
1156
+ const STEER_SOFT_INTERRUPT_CODE = "steer_soft_interrupt";
1157
+ class SteerSoftInterrupt extends Error {
1158
+ code = STEER_SOFT_INTERRUPT_CODE;
1159
+ constructor() {
1160
+ super("Provider turn soft-interrupted by steer");
1161
+ this.name = "SteerSoftInterrupt";
1162
+ }
1163
+ }
1164
+ function isSteerSoftInterrupt(error) {
1165
+ return error instanceof SteerSoftInterrupt
1166
+ || (typeof error === "object" && error !== null && error.code === STEER_SOFT_INTERRUPT_CODE);
1167
+ }
990
1168
  function finalAssistantMessage(history) {
991
1169
  for (let index = history.length - 1; index >= 0; index -= 1) {
992
1170
  const message = history[index];
@@ -0,0 +1,63 @@
1
+ import type { ContextBlock, InputAssemblyLayout, Message, ProviderRequest, Skill, ToolDefinition } from "./contracts.js";
2
+ /** Assembler-time input budget. At least one max required when present. */
3
+ export interface ContextBudget {
4
+ readonly maxInputTokens?: number;
5
+ readonly maxInputBytes?: number;
6
+ readonly reportOmissions?: boolean;
7
+ }
8
+ export type ContextBudgetOmissionKind = "skills" | "context" | "history" | "tool_results" | "summaries" | "attachments" | "tools";
9
+ export interface ContextBudgetOmission {
10
+ readonly kind: ContextBudgetOmissionKind;
11
+ readonly id?: string;
12
+ readonly tokenEstimate: number;
13
+ readonly byteLength: number;
14
+ }
15
+ export interface ContextBudgetReport {
16
+ readonly omitted: readonly ContextBudgetOmission[];
17
+ readonly keptTokens: number;
18
+ readonly keptBytes: number;
19
+ readonly maxInputTokens?: number;
20
+ readonly maxInputBytes?: number;
21
+ readonly truncated: boolean;
22
+ }
23
+ export interface ContextBudgetMessageGroups {
24
+ readonly instructions: readonly Message[];
25
+ readonly summaries: readonly Message[];
26
+ readonly history: readonly Message[];
27
+ readonly input: readonly Message[];
28
+ readonly attachments: readonly Message[];
29
+ readonly toolResults: readonly Message[];
30
+ }
31
+ export declare const CONTEXT_BUDGET_REPORT_METADATA_KEY: "contextBudgetReport";
32
+ export declare const HARD_MAX_CONTEXT_BUDGET_TOKENS = 2000000;
33
+ export declare const HARD_MAX_CONTEXT_BUDGET_BYTES: number;
34
+ export declare const DEFAULT_MAX_CONTEXT_BUDGET_OMISSIONS = 256;
35
+ export declare const HARD_MAX_CONTEXT_BUDGET_OMISSIONS = 1024;
36
+ export declare const CONTEXT_BUDGET_ERROR_CODE: "context_budget_exceeded";
37
+ export declare class ContextBudgetError extends Error {
38
+ readonly code: "context_budget_exceeded";
39
+ constructor(message?: string);
40
+ }
41
+ export declare function isContextBudgetError(error: unknown): error is ContextBudgetError;
42
+ /** UTF-16 code units / 4. Estimate only — not billing. */
43
+ export declare function estimateTextTokens(text: string): number;
44
+ export declare function estimateTextBytes(text: string): number;
45
+ export declare function estimateMessageTokens(message: Message): number;
46
+ export declare function estimateMessageBytes(message: Message): number;
47
+ export declare function estimateAssemblyTokens(messages: readonly Message[]): number;
48
+ export declare function resolveContextBudget(budget: ContextBudget): Required<Pick<ContextBudget, "reportOmissions">> & ContextBudget;
49
+ export declare function getContextBudgetReport(request: ProviderRequest): ContextBudgetReport | undefined;
50
+ export declare function applyContextBudget(options: {
51
+ readonly groups: ContextBudgetMessageGroups;
52
+ readonly context?: readonly ContextBlock[];
53
+ readonly skills?: readonly Skill[];
54
+ readonly tools?: readonly ToolDefinition[];
55
+ readonly budget: ContextBudget;
56
+ readonly layout?: InputAssemblyLayout;
57
+ }): {
58
+ readonly groups: ContextBudgetMessageGroups;
59
+ readonly context: readonly ContextBlock[];
60
+ readonly skills: readonly Skill[];
61
+ readonly tools: readonly ToolDefinition[] | undefined;
62
+ readonly report: ContextBudgetReport;
63
+ };