@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.
- package/CHANGELOG.md +27 -0
- package/dist/agent-loops.d.ts +1 -0
- package/dist/agent-loops.js +37 -2
- package/dist/agent-run-lifecycle.d.ts +5 -1
- package/dist/agent-run-lifecycle.js +17 -1
- package/dist/agents.d.ts +3 -1
- package/dist/agents.js +202 -24
- package/dist/context-budget.d.ts +63 -0
- package/dist/context-budget.js +235 -0
- package/dist/contracts.d.ts +109 -0
- package/dist/contracts.js +77 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +5 -4
- package/dist/input.d.ts +3 -0
- package/dist/input.js +71 -28
- package/dist/node/session-store-jsonl.js +4 -1
- package/dist/rpc.js +13 -2
- package/dist/session-stores.d.ts +7 -2
- package/dist/session-stores.js +174 -4
- package/dist/structured-output.d.ts +5 -1
- package/dist/structured-output.js +18 -0
- package/dist/testing/persistence-schema.d.ts +1 -1
- package/dist/testing/persistence-schema.js +8 -2
- package/dist/testing/session-store-conformance.d.ts +6 -0
- package/dist/testing/session-store-conformance.js +36 -1
- package/docs/a2a.md +1 -0
- package/docs/ag-ui.md +123 -0
- package/docs/agent-events.md +2 -1
- package/docs/agent-loops.md +8 -1
- package/docs/agent-session-runtime.md +10 -2
- package/docs/cli-rpc.md +2 -1
- package/docs/coding-agent-tools.md +70 -1
- package/docs/compaction-and-retry.md +2 -1
- package/docs/compaction-llm.md +20 -1
- package/docs/credential-storage.md +2 -1
- package/docs/credentials-and-redaction.md +8 -0
- package/docs/evaluations.md +1 -1
- package/docs/host-security.md +2 -0
- package/docs/index.md +20 -17
- package/docs/input-and-prompt-assembly.md +4 -1
- package/docs/migration.md +32 -0
- package/docs/node-jsonl-session-store.md +1 -1
- package/docs/performance.md +36 -0
- package/docs/postgres-persistence.md +3 -3
- package/docs/provider-packages.md +11 -1
- package/docs/providers/anthropic.md +93 -0
- package/docs/providers/google.md +88 -0
- package/docs/providers/openai.md +2 -2
- package/docs/public-contracts.md +6 -0
- package/docs/release-and-install.md +232 -65
- package/docs/review-coverage-2026-07-22-phase-6.md +209 -0
- package/docs/review-coverage-2026-07-22-phase-7.md +173 -0
- package/docs/runs-and-usage.md +1 -0
- package/docs/server.md +1 -0
- package/docs/session-store-conformance.md +2 -0
- package/docs/session-stores.md +40 -1
- package/docs/sqlite-persistence.md +3 -3
- package/docs/structured-output.md +7 -1
- package/docs/workflows.md +2 -0
- 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
|
package/dist/agent-loops.d.ts
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;
|
package/dist/agent-loops.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
750
|
-
|
|
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
|
+
};
|