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