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