@arnilo/prism 0.4.0 → 0.5.0
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 +33 -1
- package/README.md +23 -20
- package/dist/agent-run-state.d.ts +1 -2
- package/dist/agent-run-state.js +0 -3
- package/dist/agent-session/session/assemble.d.ts +6 -0
- package/dist/agent-session/session/assemble.js +391 -0
- package/dist/agent-session/session/persist.d.ts +28 -0
- package/dist/agent-session/session/persist.js +166 -0
- package/dist/agent-session/session/provider-round.d.ts +6 -0
- package/dist/agent-session/session/provider-round.js +231 -0
- package/dist/agent-session/session/tool-round.d.ts +31 -0
- package/dist/agent-session/session/tool-round.js +473 -0
- package/dist/agent-session/session/types.d.ts +115 -0
- package/dist/agent-session/session/types.js +5 -0
- package/dist/agent-session/session.d.ts +49 -43
- package/dist/agent-session/session.js +11 -1177
- package/dist/capture.d.ts +63 -0
- package/dist/capture.js +67 -0
- package/dist/cli-init.d.ts +18 -2
- package/dist/cli-init.js +2 -7
- package/dist/cli-runner.d.ts +2 -2
- package/dist/cli-runner.js +45 -9
- package/dist/content.d.ts +3 -3
- package/dist/content.js +3 -1
- package/dist/contracts-core/agent.d.ts +2 -0
- package/dist/contracts-core/batch.d.ts +97 -0
- package/dist/contracts-core/batch.js +65 -0
- package/dist/contracts-core/content.d.ts +72 -1
- package/dist/contracts-core/embeddings.d.ts +30 -0
- package/dist/contracts-core/embeddings.js +17 -0
- package/dist/contracts-core/images.d.ts +60 -0
- package/dist/contracts-core/images.js +17 -0
- package/dist/contracts-core/moderation.d.ts +46 -0
- package/dist/contracts-core/moderation.js +34 -0
- package/dist/contracts-core/speech.d.ts +39 -0
- package/dist/contracts-core/speech.js +17 -0
- package/dist/contracts-core/transcription.d.ts +48 -0
- package/dist/contracts-core/transcription.js +17 -0
- package/dist/contracts-core/video.d.ts +61 -0
- package/dist/contracts-core/video.js +17 -0
- package/dist/contracts-core.d.ts +7 -0
- package/dist/contracts-core.js +7 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -3
- package/dist/node/agent-definitions.d.ts +1 -8
- package/dist/node/agent-definitions.js +0 -34
- package/dist/node/settings.d.ts +0 -1
- package/dist/node/settings.js +0 -5
- package/dist/pinned-fetch.js +29 -3
- package/dist/provider-events.js +3 -4
- package/dist/providers/media.d.ts +1 -2
- package/dist/providers/media.js +1 -4
- package/dist/rpc.d.ts +1 -1
- package/dist/rpc.js +4 -4
- package/dist/testing/provider-conformance.d.ts +114 -5
- package/dist/testing/provider-conformance.js +342 -0
- package/dist/testing/tool-effect-store-conformance.d.ts +0 -1
- package/dist/testing/tool-effect-store-conformance.js +0 -3
- package/dist/thinking.d.ts +48 -9
- package/dist/thinking.js +134 -8
- package/docs/0.1.0-readiness.md +3 -3
- package/docs/a2a.md +2 -2
- package/docs/acp.md +3 -3
- package/docs/ag-ui-adoption.md +1 -1
- package/docs/ag-ui.md +1 -2
- package/docs/agent-definitions.md +1 -1
- package/docs/agent-events.md +5 -5
- package/docs/agent-identity.md +13 -2
- package/docs/audit-export.md +3 -3
- package/docs/batch-jobs.md +120 -0
- package/docs/cli-rpc.md +20 -9
- package/docs/coding-agent-tools.md +19 -19
- package/docs/coding-review-and-diagnostics.md +2 -2
- package/docs/coding-security.md +4 -4
- package/docs/coding-workspaces.md +2 -2
- package/docs/computer-use-linux.md +13 -2
- package/docs/context-and-skills.md +1 -1
- package/docs/conversations.md +4 -4
- package/docs/credential-storage.md +11 -7
- package/docs/credentials-and-redaction.md +1 -1
- package/docs/data-classification.md +1 -1
- package/docs/database-persistence.md +4 -4
- package/docs/dev-inspector.md +6 -6
- package/docs/device-adapters.md +2 -2
- package/docs/diagrams.md +1 -1
- package/docs/document-reader.md +6 -6
- package/docs/documents.md +5 -4
- package/docs/embeddings.md +112 -0
- package/docs/enterprise-postgres-state.md +7 -7
- package/docs/evaluations.md +8 -8
- package/docs/extensions.md +3 -3
- package/docs/forge-integration.md +3 -3
- package/docs/graft.md +2 -2
- package/docs/guardrails.md +1 -1
- package/docs/host-security.md +15 -15
- package/docs/image-generation.md +129 -0
- package/docs/impeccable.md +5 -3
- package/docs/index.md +60 -33
- package/docs/indexed-code-search.md +2 -2
- package/docs/language-intelligence.md +4 -4
- package/docs/live-testing.md +126 -0
- package/docs/mcp-tools.md +43 -12
- package/docs/middleware-hooks.md +1 -1
- package/docs/migrate-to-0.4.md +3 -3
- package/docs/migrate-to-0.5.md +122 -0
- package/docs/migration.md +29 -1
- package/docs/model-registry.md +38 -0
- package/docs/model-routing.md +5 -5
- package/docs/moderation.md +117 -0
- package/docs/multi-agent-patterns.md +4 -4
- package/docs/multimodal-content.md +26 -2
- package/docs/obscura.md +2 -2
- package/docs/observability.md +32 -7
- package/docs/openapi-tools.md +13 -3
- package/docs/operations.md +11 -0
- package/docs/performance.md +7 -7
- package/docs/persistence-credentials-multimodality-primitives.md +6 -6
- package/docs/policy-and-audit.md +17 -7
- package/docs/ponytail.md +1 -1
- package/docs/postgres-persistence.md +5 -5
- package/docs/process-sessions.md +2 -2
- package/docs/prompt-registry.md +7 -7
- package/docs/provider-caching.md +4 -0
- package/docs/provider-conformance.md +23 -1
- package/docs/provider-packages.md +39 -3
- package/docs/provider-primitives.md +1 -1
- package/docs/provider-request-policies.md +1 -1
- package/docs/providers/ai-sdk.md +15 -3
- package/docs/providers/alibaba.md +5 -1
- package/docs/providers/anthropic.md +4 -0
- package/docs/providers/azure.md +17 -1
- package/docs/providers/bedrock.md +15 -0
- package/docs/providers/clinepass.md +4 -0
- package/docs/providers/commandcode.md +253 -0
- package/docs/providers/deepseek.md +4 -0
- package/docs/providers/google.md +4 -0
- package/docs/providers/hyper.md +284 -0
- package/docs/providers/kimi.md +4 -0
- package/docs/providers/neuralwatt.md +4 -0
- package/docs/providers/ollama.md +15 -0
- package/docs/providers/openai-compatible.md +4 -0
- package/docs/providers/openai.md +4 -0
- package/docs/providers/opencode-go.md +4 -0
- package/docs/providers/openrouter.md +5 -1
- package/docs/providers/vertex.md +16 -0
- package/docs/providers/xai.md +4 -0
- package/docs/providers/zai.md +4 -0
- package/docs/rag.md +26 -4
- package/docs/release-and-install.md +103 -46
- package/docs/resource-loading.md +1 -1
- package/docs/runs-and-usage.md +14 -2
- package/docs/server.md +5 -5
- package/docs/settings-auth-trust-security.md +7 -5
- package/docs/sheets.md +2 -2
- package/docs/speech.md +126 -0
- package/docs/sqlite-persistence.md +4 -4
- package/docs/supervisors.md +3 -3
- package/docs/thinking-and-reasoning.md +93 -60
- package/docs/tool-conformance.md +1 -1
- package/docs/tool-execution-primitives.md +8 -8
- package/docs/tools.md +4 -4
- package/docs/web-tools.md +1 -1
- package/docs/wiki.md +1 -1
- package/docs/work-artifacts-and-review.md +17 -6
- package/docs/work-connectors.md +4 -4
- package/docs/work-tools.md +5 -5
- package/docs/workflow-orchestration-primitives.md +11 -11
- package/docs/workflows.md +5 -5
- package/package.json +11 -8
- package/templates/init/providers.json +24 -8
- package/docs/antigravity-agent.md +0 -207
|
@@ -1,53 +1,20 @@
|
|
|
1
1
|
/** session (0.2.5 plan 025 Task 1 split). Moved verbatim from agent-session.ts; public surface unchanged behind the barrel. */
|
|
2
|
-
import {
|
|
3
|
-
import { resolveLoop, resolveToolConcurrency } from "../agent-loops.js";
|
|
4
|
-
import { boundedLoopSnapshot, initialAgentRunState, publicState, saveAgentRunState, validateRunStateOptions } from "../agent-run-state.js";
|
|
5
|
-
import { activeTools, policyList, toolElicitationRequest } from "../agent-tool-dispatch.js";
|
|
2
|
+
import { policyList } from "../agent-tool-dispatch.js";
|
|
6
3
|
import { createDefaultCompactionStrategy, isCompactionEntryData } from "../compaction.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { identityTelemetryAttributes, ownershipFromIdentity, resolveRunIdentity } from "../identity.js";
|
|
10
|
-
import { assembleProviderInput } from "../input.js";
|
|
11
|
-
import { createProviderTurnMetadata, readProviderHttpStatus } from "../observability.js";
|
|
12
|
-
import { providerToolCallDeltaContent } from "../provider-events.js";
|
|
4
|
+
import { DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS } from "../contracts.js";
|
|
5
|
+
import { GuardrailError, runGuardrails } from "../guardrails.js";
|
|
13
6
|
import { createProviderRequestPolicyChain, normalizeProviderRequestPolicyResult } from "../provider-request-policy.js";
|
|
14
|
-
import {
|
|
15
|
-
import { createDefaultRetryPolicy, waitForRetry } from "../retry.js";
|
|
16
|
-
import { isFlushableRunLedger } from "../run-ledger.js";
|
|
17
|
-
import { RunLimitError, RunLimitTracker, resolveRunLimits } from "../run-limits.js";
|
|
7
|
+
import { redactAgentEvent, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry } from "../redaction.js";
|
|
18
8
|
import { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, rebuildSessionContext } from "../session-stores.js";
|
|
19
|
-
import { createLoadedSkillSet
|
|
20
|
-
import {
|
|
9
|
+
import { createLoadedSkillSet } from "../skill-disclosure.js";
|
|
10
|
+
import { validateLoadedSkillBodies } from "../skill-load.js";
|
|
21
11
|
import { resolveActiveSkills } from "../skills.js";
|
|
22
|
-
import {
|
|
23
|
-
import { composeSystemPrompt, mergeSystemPromptConfig } from "../system-prompts.js";
|
|
24
|
-
import { canonicalToolEffectJson, toolEffectArgumentsHash } from "../tool-effects.js";
|
|
25
|
-
import { resolveToolResultFold } from "../tool-result-fold.js";
|
|
26
|
-
import { createActiveToolSet, createSearchToolsTool, createToolSearchState, resolveToolsDisclosure } from "../tool-search.js";
|
|
27
|
-
import { createToolRegistry, dispatchToolCall, resolveToolEffectDeclaration } from "../tools.js";
|
|
12
|
+
import { createActiveToolSet } from "../tool-search.js";
|
|
28
13
|
import { createAgentSession } from "./create-agent.js";
|
|
29
14
|
import { EventSubscriber } from "./event-subscriber.js";
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/** Plan 042: validate the opaque provenance ref fail-closed before any session mutation. */
|
|
34
|
-
function assertPromptVersionRef(ref) {
|
|
35
|
-
if (ref === undefined)
|
|
36
|
-
return undefined;
|
|
37
|
-
if (typeof ref !== "object" || ref === null)
|
|
38
|
-
throw new TypeError("RunOptions.promptVersion must be a PromptVersionRef object");
|
|
39
|
-
const { name, version, hash } = ref;
|
|
40
|
-
if (typeof name !== "string" || name.length === 0 || Buffer.byteLength(name, "utf8") > PROMPT_VERSION_MAX_NAME_BYTES) {
|
|
41
|
-
throw new TypeError(`RunOptions.promptVersion.name must be 1-${PROMPT_VERSION_MAX_NAME_BYTES} UTF-8 bytes`);
|
|
42
|
-
}
|
|
43
|
-
if (!Number.isInteger(version) || version < 1 || version > 0x7fffffff) {
|
|
44
|
-
throw new TypeError("RunOptions.promptVersion.version must be an integer in [1, 2147483647]");
|
|
45
|
-
}
|
|
46
|
-
if (typeof hash !== "string" || !PROMPT_VERSION_HASH_PATTERN.test(hash)) {
|
|
47
|
-
throw new TypeError('RunOptions.promptVersion.hash must be "sha256:" plus 64 lowercase hex characters');
|
|
48
|
-
}
|
|
49
|
-
return ref;
|
|
50
|
-
}
|
|
15
|
+
import { finalAssistantMessage, inputToMessages, mergeCompaction, messageTextBytes, randomId, SteerSoftInterrupt, throwIfAborted, throwIfAbortedSignal, withoutTrailingInput, } from "./helpers.js";
|
|
16
|
+
import { executeRun } from "./session/assemble.js";
|
|
17
|
+
import { asSessionHost } from "./session/types.js";
|
|
51
18
|
export class RuntimeAgentSession {
|
|
52
19
|
id;
|
|
53
20
|
agent;
|
|
@@ -193,813 +160,7 @@ export class RuntimeAgentSession {
|
|
|
193
160
|
}
|
|
194
161
|
}
|
|
195
162
|
async runInternal(input, options, runId, resumed) {
|
|
196
|
-
|
|
197
|
-
// untyped/legacy input fail-closed before any session mutation, provider call, or tool
|
|
198
|
-
// execution; silently honoring it would widen or mis-apply the intended tool-round cap.
|
|
199
|
-
const legacyMaxToolRounds = options.maxToolRounds;
|
|
200
|
-
if (legacyMaxToolRounds !== undefined) {
|
|
201
|
-
throw new TypeError("RunOptions.maxToolRounds was removed in 0.1.5; use RunOptions.limits.maxToolRounds instead");
|
|
202
|
-
}
|
|
203
|
-
const promptVersion = assertPromptVersionRef(options.promptVersion);
|
|
204
|
-
if (this.agent.config.secure &&
|
|
205
|
-
(options.redactor !== undefined ||
|
|
206
|
-
options.ownership !== undefined ||
|
|
207
|
-
options.validate !== undefined ||
|
|
208
|
-
options.effectStore !== undefined ||
|
|
209
|
-
options.runState !== undefined)) {
|
|
210
|
-
throw new AgentRunStateError("Secure agent defaults cannot be replaced per run");
|
|
211
|
-
}
|
|
212
|
-
const requestedLimits = options.limits;
|
|
213
|
-
const resolvedLimits = resolveRunLimits(this.agent.config.limits, requestedLimits);
|
|
214
|
-
const durableOptions = options.runState ?? this.agent.config.runState;
|
|
215
|
-
if (this.agent.config.runState && options.runState && this.agent.config.runState !== options.runState) {
|
|
216
|
-
throw new AgentRunStateError("RunOptions cannot replace agent durable run-state configuration");
|
|
217
|
-
}
|
|
218
|
-
if (durableOptions) {
|
|
219
|
-
validateRunStateOptions(durableOptions);
|
|
220
|
-
if (options.model || options.guardrails || options.loop || options.effectStore)
|
|
221
|
-
throw new AgentRunStateError("Durable runs require model, guardrails, loop, and effect store on AgentConfig for fingerprinting");
|
|
222
|
-
const configuredLoop = this.agent.config.loop;
|
|
223
|
-
if (configuredLoop && !isDurableLoop(configuredLoop)) {
|
|
224
|
-
throw new AgentLoopStateError("ERR_PRISM_LOOP_NOT_DURABLE", "Custom AgentLoopStrategy on a durable run requires snapshot and restore hooks");
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (this.activeRun) {
|
|
228
|
-
const error = new Error("Agent session already has an active run");
|
|
229
|
-
this.emit({ type: "error", sessionId: this.id, runId, error: errorToErrorInfo(error) });
|
|
230
|
-
throw error;
|
|
231
|
-
}
|
|
232
|
-
const controller = new AbortController();
|
|
233
|
-
const cleanupSignal = bridgeAbort(options.signal, controller);
|
|
234
|
-
this.activeRun = controller;
|
|
235
|
-
this.activeRunId = runId;
|
|
236
|
-
this.pendingSteers = [];
|
|
237
|
-
this.pendingSteerBytes = 0;
|
|
238
|
-
this.pendingSoftInterrupt = false;
|
|
239
|
-
this.activeRedactor = options.redactor ?? this.agent.config.redactor;
|
|
240
|
-
this.activeLedger = options.runLedger ?? this.agent.config.runLedger;
|
|
241
|
-
this.activeEffectStore = options.effectStore ?? this.agent.config.effectStore;
|
|
242
|
-
this.activeOwnership = options.ownership ?? this.agent.config.ownership;
|
|
243
|
-
this.activeIdentity = resolveRunIdentity(options.identity, this.agent.config.identity, this.activeOwnership);
|
|
244
|
-
if (this.activeIdentity && !this.activeOwnership)
|
|
245
|
-
this.activeOwnership = ownershipFromIdentity(this.activeIdentity);
|
|
246
|
-
this.activeIdempotencyKey = options.idempotencyKey ?? this.agent.config.idempotencyKey;
|
|
247
|
-
this.activeGuardrails = mergeGuardrails(this.agent.config.guardrails, options.guardrails);
|
|
248
|
-
this.activeDurable = resumed ?? (durableOptions ? { options: durableOptions, version: 0 } : undefined);
|
|
249
|
-
this.activeGatedRound = undefined;
|
|
250
|
-
if (resumed)
|
|
251
|
-
this.invalidateSnapshot();
|
|
252
|
-
const model = options.model ?? this.agent.config.model;
|
|
253
|
-
const startedAt = new Date().toISOString();
|
|
254
|
-
let runError;
|
|
255
|
-
let runStatus = "succeeded";
|
|
256
|
-
const runUsage = createUsageAccumulator();
|
|
257
|
-
let usage;
|
|
258
|
-
const metadata = {
|
|
259
|
-
...this.agent.config.metadata,
|
|
260
|
-
...this.metadata,
|
|
261
|
-
...options.metadata,
|
|
262
|
-
...(this.activeIdentity ? identityTelemetryAttributes(this.activeIdentity) : {}),
|
|
263
|
-
};
|
|
264
|
-
this.activeMetadata = metadata;
|
|
265
|
-
this.activePromptVersion = promptVersion;
|
|
266
|
-
const limits = new RunLimitTracker(resolvedLimits, {
|
|
267
|
-
onExceeded: (breach) => {
|
|
268
|
-
this.emit({ type: "run_limit_exceeded", sessionId: this.id, runId, breach });
|
|
269
|
-
controller.abort(new RunLimitError(breach));
|
|
270
|
-
},
|
|
271
|
-
snapshot: resumed?.state?.counters,
|
|
272
|
-
deadlineAt: resumed?.state?.deadlineAt,
|
|
273
|
-
});
|
|
274
|
-
this.activeLimits = limits;
|
|
275
|
-
this.activeLimitOutputBuffer = [this.agent.config.limits, requestedLimits].some((value) => value?.maxOutputTokens !== undefined || value?.maxTotalTokens !== undefined || value?.maxCost !== undefined);
|
|
276
|
-
try {
|
|
277
|
-
this.resolveRunProvider(options);
|
|
278
|
-
throwIfAborted(controller.signal);
|
|
279
|
-
this.emit({ type: "agent_started", sessionId: this.id, runId });
|
|
280
|
-
if (resumed)
|
|
281
|
-
this.emit({ type: "agent_resumed", sessionId: this.id, runId, version: resumed.version });
|
|
282
|
-
const startRecord = {
|
|
283
|
-
id: runId,
|
|
284
|
-
sessionId: this.id,
|
|
285
|
-
branchId: this.currentLeafId,
|
|
286
|
-
model,
|
|
287
|
-
provider: model.provider,
|
|
288
|
-
idempotencyKey: this.activeIdempotencyKey,
|
|
289
|
-
status: "running",
|
|
290
|
-
startedAt,
|
|
291
|
-
...(promptVersion ? { promptVersion } : {}),
|
|
292
|
-
...this.activeOwnership,
|
|
293
|
-
};
|
|
294
|
-
await this.activeLedger?.appendRun(redactRunLedgerRecord(startRecord, this.activeRedactor));
|
|
295
|
-
await this.rebuildHistory();
|
|
296
|
-
const { registry: baseRegistry, tools: activeToolList } = activeTools(this.agent.config.tools);
|
|
297
|
-
const toolsDisclosure = resolveToolsDisclosure(options.toolsDisclosure, this.agent.config.toolsDisclosure);
|
|
298
|
-
// Search mode: run-local registry copy plus the generated `search_tools` tool; the host
|
|
299
|
-
// registry is never mutated. Activation names are intersected at disclosure only.
|
|
300
|
-
const toolSearch = toolsDisclosure === "search" && activeToolList.length > 0
|
|
301
|
-
? {
|
|
302
|
-
state: createToolSearchState({
|
|
303
|
-
tools: activeToolList,
|
|
304
|
-
activated: this.activatedTools,
|
|
305
|
-
search: this.agent.config.toolsSearch,
|
|
306
|
-
}),
|
|
307
|
-
}
|
|
308
|
-
: undefined;
|
|
309
|
-
const searchTool = toolSearch ? createSearchToolsTool(toolSearch.state) : undefined;
|
|
310
|
-
const registry = searchTool ? createToolRegistry([...activeToolList, searchTool]) : baseRegistry;
|
|
311
|
-
const tools = searchTool ? [...activeToolList, searchTool] : activeToolList;
|
|
312
|
-
const activeSkills = this.resolveRunSkills(options, tools);
|
|
313
|
-
this.activeRunSkills = activeSkills; // for the durable bodies snapshot (plan 018 Task 6)
|
|
314
|
-
if (options.model && JSON.stringify(options.model) !== JSON.stringify(this.agent.config.model)) {
|
|
315
|
-
await this.appendEntry(createSessionEntry({
|
|
316
|
-
sessionId: this.id,
|
|
317
|
-
parentId: this.currentLeafId,
|
|
318
|
-
runId,
|
|
319
|
-
kind: "model_change",
|
|
320
|
-
previousModel: this.agent.config.model,
|
|
321
|
-
model: options.model,
|
|
322
|
-
}));
|
|
323
|
-
}
|
|
324
|
-
const inputMessages = inputToMessages(input).map((message) => this.redact(message));
|
|
325
|
-
const inputGuardrails = await runGuardrails({
|
|
326
|
-
stage: "input",
|
|
327
|
-
guardrails: this.activeGuardrails,
|
|
328
|
-
value: inputMessages,
|
|
329
|
-
context: { sessionId: this.id, runId, metadata, signal: controller.signal },
|
|
330
|
-
redactor: this.activeRedactor,
|
|
331
|
-
emit: (event) => this.emit(event),
|
|
332
|
-
});
|
|
333
|
-
// Input-guardrail decision table:
|
|
334
|
-
// - interrupt + durable + fresh run → suspend for approval.
|
|
335
|
-
// - interrupt + durable + resumed run → proceed: resuming IS the operator approval.
|
|
336
|
-
// - interrupt without durable, or block/tripwire → fail via assertGuardrailsAllowed.
|
|
337
|
-
const approvedByResume = resumed !== undefined && inputGuardrails.terminal?.action === "interrupt" && this.activeDurable !== undefined;
|
|
338
|
-
if (inputGuardrails.terminal?.action === "interrupt" && this.activeDurable && !approvedByResume) {
|
|
339
|
-
const interruption = { kind: "input_guardrail", reason: inputGuardrails.terminal.reason ?? "Input requires approval" };
|
|
340
|
-
throw new AgentRunSuspended(await this.suspendDurable({ runId, model, limits, interruption, messages: inputMessages }), interruption);
|
|
341
|
-
}
|
|
342
|
-
if (inputGuardrails.terminal && !approvedByResume)
|
|
343
|
-
assertGuardrailsAllowed(inputGuardrails);
|
|
344
|
-
for (const message of inputMessages)
|
|
345
|
-
await this.appendMessage(message, runId);
|
|
346
|
-
await this.autoCompact(runId, options, controller.signal, inputMessages);
|
|
347
|
-
const maxToolRounds = resolvedLimits.maxToolRounds;
|
|
348
|
-
const systemInstructions = composeSystemPrompt(mergeSystemPromptConfig(this.agent.config.systemPrompt, options.systemPrompt), {
|
|
349
|
-
base: this.agent.config.instructions,
|
|
350
|
-
});
|
|
351
|
-
const contextProviders = [
|
|
352
|
-
...(this.agent.config.context ?? []),
|
|
353
|
-
// ponytail: skill context after host context; no per-skill token budget yet.
|
|
354
|
-
...activeSkills.flatMap((skill) => skill.context ?? []),
|
|
355
|
-
];
|
|
356
|
-
const providerOptions = resolveRunProviderOptions(options, this.agent.config);
|
|
357
|
-
assertStructuredOutputRequestSupported(options.model ?? this.agent.config.model, providerOptions);
|
|
358
|
-
const validate = options.validate ?? this.agent.config.validator;
|
|
359
|
-
// ponytail: RunOptions.instructionInjectors overrides AgentConfig.instructionInjectors (mirrors validate/loop).
|
|
360
|
-
const instructionInjectors = options.instructionInjectors ?? this.agent.config.instructionInjectors ?? [];
|
|
361
|
-
const inputLayout = options.inputLayout ?? this.agent.config.inputLayout;
|
|
362
|
-
const loop = resolveLoop(options, this.agent.config);
|
|
363
|
-
this.activeLoop = loop;
|
|
364
|
-
const toolConcurrency = resolveToolConcurrency(options, this.agent.config);
|
|
365
|
-
this.activeLoopTurn = 1;
|
|
366
|
-
const recordProviderUsage = async (turnUsage, turn, attempt) => {
|
|
367
|
-
limits.recordUsage(turnUsage);
|
|
368
|
-
if (!turnUsage)
|
|
369
|
-
return;
|
|
370
|
-
runUsage.add(turnUsage);
|
|
371
|
-
if (!this.activeLedger)
|
|
372
|
-
return;
|
|
373
|
-
const usageRecord = {
|
|
374
|
-
id: randomId("usage"),
|
|
375
|
-
sessionId: this.id,
|
|
376
|
-
runId,
|
|
377
|
-
scope: "provider_turn",
|
|
378
|
-
turn,
|
|
379
|
-
attempt,
|
|
380
|
-
usage: turnUsage,
|
|
381
|
-
recordedAt: new Date().toISOString(),
|
|
382
|
-
...this.activeOwnership,
|
|
383
|
-
};
|
|
384
|
-
await this.activeLedger.appendUsage(redactRunLedgerRecord(usageRecord, this.activeRedactor));
|
|
385
|
-
};
|
|
386
|
-
// Suspends the run when a round recorded gated calls. Fires at the next provider turn
|
|
387
|
-
// (generate) or after the loop ends, so ungated round siblings dispatch first.
|
|
388
|
-
const suspendGatedRound = async () => {
|
|
389
|
-
const gated = this.activeGatedRound;
|
|
390
|
-
if (!gated?.size)
|
|
391
|
-
return;
|
|
392
|
-
const entries = [...gated.values()];
|
|
393
|
-
const decisions = entries.map((gatedCall) => gatedCall.decision);
|
|
394
|
-
const single = decisions.length === 1 ? decisions[0] : undefined;
|
|
395
|
-
const interruption = {
|
|
396
|
-
kind: single?.kind === "elicitation" ? "elicitation" : "tool_approval",
|
|
397
|
-
reason: single ? single.reason : `${decisions.length} tool side effects require approval`,
|
|
398
|
-
...(single?.toolCallId ? { toolCallId: single.toolCallId } : {}),
|
|
399
|
-
...(single?.scope.toolName ? { toolName: single.scope.toolName } : {}),
|
|
400
|
-
pendingDecisions: decisions,
|
|
401
|
-
};
|
|
402
|
-
throw new AgentRunSuspended(await this.suspendDurable({ runId, model, limits, interruption, pendingCalls: entries.map((gatedCall) => gatedCall.entry) }), interruption);
|
|
403
|
-
};
|
|
404
|
-
// Suspends on a nested run's pending decisions, merging any still-ready round entries
|
|
405
|
-
// (with their decisions attached) so a nested signal mid-replay never drops own work.
|
|
406
|
-
const suspendNested = async (nested) => {
|
|
407
|
-
const state = this.activeDurable?.state;
|
|
408
|
-
const kept = (state?.pendingCalls ?? [])
|
|
409
|
-
.filter((entry) => entry.status === "ready")
|
|
410
|
-
.map((entry) => {
|
|
411
|
-
const decision = entry.decision ?? resumed?.decisions?.get(entry.approvalId);
|
|
412
|
-
return decision ? { ...entry, decision } : entry;
|
|
413
|
-
});
|
|
414
|
-
const gated = [...(this.activeGatedRound?.values() ?? [])];
|
|
415
|
-
const pendingCalls = [
|
|
416
|
-
...kept,
|
|
417
|
-
...gated.map((gatedCall) => gatedCall.entry),
|
|
418
|
-
{ call: nested.toolCall, status: "ready", approvalId: `nr_${nested.entry.runId}` },
|
|
419
|
-
];
|
|
420
|
-
const keptIds = new Set(kept.map((entry) => entry.approvalId));
|
|
421
|
-
const decisions = [
|
|
422
|
-
...(state?.interruption?.pendingDecisions ?? []).filter((pending) => keptIds.has(pending.approvalId)),
|
|
423
|
-
...gated.map((gatedCall) => gatedCall.decision),
|
|
424
|
-
...nested.pending,
|
|
425
|
-
];
|
|
426
|
-
if (decisions.length > HARD_MAX_PENDING_DECISIONS) {
|
|
427
|
-
throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Pending decisions exceed ${HARD_MAX_PENDING_DECISIONS} per run`);
|
|
428
|
-
}
|
|
429
|
-
const single = decisions.length === 1 ? decisions[0] : undefined;
|
|
430
|
-
const interruption = {
|
|
431
|
-
kind: single?.kind ?? "tool_approval",
|
|
432
|
-
reason: single ? single.reason : `${decisions.length} approval request(s) need a decision`,
|
|
433
|
-
...(single?.toolCallId ? { toolCallId: single.toolCallId } : {}),
|
|
434
|
-
...(single?.scope.toolName ? { toolName: single.scope.toolName } : {}),
|
|
435
|
-
pendingDecisions: decisions,
|
|
436
|
-
};
|
|
437
|
-
throw new AgentRunSuspended(await this.suspendDurable({
|
|
438
|
-
runId,
|
|
439
|
-
model,
|
|
440
|
-
limits,
|
|
441
|
-
interruption,
|
|
442
|
-
pendingCalls,
|
|
443
|
-
nestedRuns: [...(state?.nestedRuns ?? []), nested.entry],
|
|
444
|
-
}), interruption);
|
|
445
|
-
};
|
|
446
|
-
// Converts a nested-run suspension into either root-visible pending decisions (hashed,
|
|
447
|
-
// attributed approval ids) or — when a root sticky covers every surfaced decision and a
|
|
448
|
-
// hook is available — an immediate child resume loop ending in a synthesized tool result.
|
|
449
|
-
const applyNestedRun = async (input) => {
|
|
450
|
-
let current = input.pending;
|
|
451
|
-
// ponytail: sticky auto-apply only when the whole surfaced set is covered; mixed sets
|
|
452
|
-
// surface to the host. Hook round-trips capped at 4 per suspension event.
|
|
453
|
-
for (let depth = 0;; depth += 1) {
|
|
454
|
-
const attributed = current.map((decision) => {
|
|
455
|
-
const id = nestedApprovalId(input.ref.runId, decision.approvalId);
|
|
456
|
-
return {
|
|
457
|
-
id,
|
|
458
|
-
childApprovalId: decision.approvalId,
|
|
459
|
-
decision: { ...decision, approvalId: id, attribution: decision.attribution ?? { path: input.path } },
|
|
460
|
-
};
|
|
461
|
-
});
|
|
462
|
-
if (attributed.some(({ decision }) => (decision.attribution?.path.length ?? 0) > MAX_ATTRIBUTION_DEPTH)) {
|
|
463
|
-
throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Attribution path exceeds ${MAX_ATTRIBUTION_DEPTH} entries`);
|
|
464
|
-
}
|
|
465
|
-
const allSticky = attributed.length > 0 && attributed.every(({ decision }) => this.matchNestedSticky(decision) !== undefined);
|
|
466
|
-
if (!input.hook || !allSticky || depth >= 4) {
|
|
467
|
-
return {
|
|
468
|
-
entry: {
|
|
469
|
-
runId: input.ref.runId,
|
|
470
|
-
...(input.ref.sessionId ? { sessionId: input.ref.sessionId } : {}),
|
|
471
|
-
toolCallId: input.toolCall.id,
|
|
472
|
-
path: attributed[0]?.decision.attribution?.path ?? input.path,
|
|
473
|
-
approvals: attributed.map(({ id, childApprovalId }) => ({ id, childApprovalId })),
|
|
474
|
-
},
|
|
475
|
-
pending: attributed.map(({ decision }) => decision),
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
const outcome = await input.hook({ ref: input.ref, toolCallId: input.toolCall.id, path: input.path }, attributed.map(({ childApprovalId, decision }) => {
|
|
479
|
-
const sticky = this.matchNestedSticky(decision);
|
|
480
|
-
return {
|
|
481
|
-
approvalId: childApprovalId,
|
|
482
|
-
outcome: sticky.outcome,
|
|
483
|
-
...(sticky.reason !== undefined ? { reason: sticky.reason } : {}),
|
|
484
|
-
};
|
|
485
|
-
}));
|
|
486
|
-
if (outcome.status === "suspended") {
|
|
487
|
-
current = outcome.pendingDecisions;
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
return { toolResult: nestedOutcomeToolResult(outcome, input.toolCall.id, input.toolCall.name) };
|
|
491
|
-
}
|
|
492
|
-
};
|
|
493
|
-
// ponytail: LoopContext binds existing private helpers; loop orchestrates only.
|
|
494
|
-
let assembledTurn = false;
|
|
495
|
-
let artifactFinished = false;
|
|
496
|
-
let artifactFailedInfo;
|
|
497
|
-
const ctx = {
|
|
498
|
-
sessionId: this.id,
|
|
499
|
-
runId,
|
|
500
|
-
metadata,
|
|
501
|
-
signal: controller.signal,
|
|
502
|
-
history: this.history,
|
|
503
|
-
input,
|
|
504
|
-
inputMessages,
|
|
505
|
-
maxToolRounds,
|
|
506
|
-
toolConcurrency,
|
|
507
|
-
restoredLoopState: resumed?.state?.loopState?.snapshot,
|
|
508
|
-
assemble: async (nextInput, toolResults, turn) => {
|
|
509
|
-
limits.charge("maxTurns");
|
|
510
|
-
const request = await assembleProviderInput({
|
|
511
|
-
model: options.model ?? this.agent.config.model,
|
|
512
|
-
input: nextInput,
|
|
513
|
-
history: this.history,
|
|
514
|
-
summaries: (await this.snapshot()).summaries,
|
|
515
|
-
toolResults: toolResults ?? [],
|
|
516
|
-
turn,
|
|
517
|
-
instructionInjectors,
|
|
518
|
-
inputLayout,
|
|
519
|
-
systemInstructions,
|
|
520
|
-
inputBuilder: this.agent.config.inputBuilder,
|
|
521
|
-
promptBuilder: this.agent.config.promptBuilder,
|
|
522
|
-
contextProviders,
|
|
523
|
-
skills: this.restoredSkillBodies.length ? applyRestoredSkillBodies(activeSkills, this.restoredSkillBodies) : activeSkills,
|
|
524
|
-
skillsDisclosure: resolveSkillsDisclosure(options.skillsDisclosure, this.agent.config.skillsDisclosure),
|
|
525
|
-
toolsDisclosure,
|
|
526
|
-
toolsSearch: this.agent.config.toolsSearch,
|
|
527
|
-
activatedTools: this.activatedTools,
|
|
528
|
-
toolResultFold: resolveToolResultFold(options.toolResultFold, this.agent.config.toolResultFold),
|
|
529
|
-
loadedSkills: this.loadedSkills,
|
|
530
|
-
tools,
|
|
531
|
-
resourceLoader: this.agent.config.resourceLoader,
|
|
532
|
-
permission: this.agent.config.permission,
|
|
533
|
-
trust: this.agent.config.trust,
|
|
534
|
-
providerOptions,
|
|
535
|
-
redactor: this.activeRedactor,
|
|
536
|
-
middleware: this.agent.config.middleware,
|
|
537
|
-
sessionId: this.id,
|
|
538
|
-
runId,
|
|
539
|
-
metadata,
|
|
540
|
-
signal: controller.signal,
|
|
541
|
-
});
|
|
542
|
-
assembledTurn = true;
|
|
543
|
-
return request;
|
|
544
|
-
},
|
|
545
|
-
chargeToolRound: (calls) => {
|
|
546
|
-
if (calls.length > 0)
|
|
547
|
-
limits.charge("maxToolRounds");
|
|
548
|
-
const durable = this.activeDurable;
|
|
549
|
-
if (!durable?.options.interruptBeforeTool || calls.length === 0)
|
|
550
|
-
return;
|
|
551
|
-
// Round-level gate: record one pending decision per uncovered gated call. Ungated
|
|
552
|
-
// and sticky-allowed calls still dispatch; the suspension fires at the next provider
|
|
553
|
-
// turn (or run end) via suspendGatedRound. Per-call beforeExecute is the backstop
|
|
554
|
-
// for loops that dispatch without charging a round.
|
|
555
|
-
for (const call of calls) {
|
|
556
|
-
if (this.matchStickyDecision(call, registry))
|
|
557
|
-
continue;
|
|
558
|
-
const approvalId = randomId("approval");
|
|
559
|
-
this.activeGatedRound ??= new Map();
|
|
560
|
-
this.activeGatedRound.set(call.id, {
|
|
561
|
-
entry: { call, status: "ready", approvalId },
|
|
562
|
-
decision: this.buildPendingDecision(call, approvalId, registry, runId, metadata, controller.signal),
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
if (this.activeGatedRound && this.activeGatedRound.size > DEFAULT_MAX_PENDING_DECISIONS) {
|
|
566
|
-
throw new AgentDecisionError("ERR_PRISM_DECISION_LIMIT", `Pending decisions exceed ${DEFAULT_MAX_PENDING_DECISIONS} per run`);
|
|
567
|
-
}
|
|
568
|
-
},
|
|
569
|
-
generate: async (request) => {
|
|
570
|
-
await suspendGatedRound();
|
|
571
|
-
if (!assembledTurn)
|
|
572
|
-
limits.charge("maxTurns");
|
|
573
|
-
assembledTurn = false;
|
|
574
|
-
const policyResult = await this.applyProviderRequestPolicies(request, runId, options, metadata, controller.signal);
|
|
575
|
-
const middlewareRequest = (await this.agent.config.middleware?.run("provider_request", policyResult.request)) ?? policyResult.request;
|
|
576
|
-
try {
|
|
577
|
-
return await this.generateWithRetry(this.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, this.activeLoopTurn, recordProviderUsage);
|
|
578
|
-
}
|
|
579
|
-
catch (error) {
|
|
580
|
-
if (isSteerSoftInterrupt(error)) {
|
|
581
|
-
return { content: [], calls: [], started: false, usage: undefined };
|
|
582
|
-
}
|
|
583
|
-
throw error;
|
|
584
|
-
}
|
|
585
|
-
},
|
|
586
|
-
isToolCallExclusive: (call) => registry.get(call.name)?.exclusive === true,
|
|
587
|
-
dispatchToolCall: async (call) => {
|
|
588
|
-
const sticky = this.matchStickyDecision(call, registry);
|
|
589
|
-
if (sticky?.outcome === "reject_for_run") {
|
|
590
|
-
return {
|
|
591
|
-
toolCallId: call.id,
|
|
592
|
-
name: call.name,
|
|
593
|
-
error: { code: "approval_rejected", message: sticky.reason ?? "Rejected for this run" },
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
if (this.activeGatedRound?.has(call.id)) {
|
|
597
|
-
// Gated this round: never dispatched. The marker is skipped by
|
|
598
|
-
// dispatchToolCallsInOrder so the transcript stays free of phantom results.
|
|
599
|
-
return { toolCallId: call.id, name: call.name, metadata: { approvalPending: true } };
|
|
600
|
-
}
|
|
601
|
-
try {
|
|
602
|
-
return await dispatchToolCall({
|
|
603
|
-
call,
|
|
604
|
-
registry,
|
|
605
|
-
context: {
|
|
606
|
-
sessionId: this.id,
|
|
607
|
-
runId,
|
|
608
|
-
toolCallId: call.id,
|
|
609
|
-
signal: controller.signal,
|
|
610
|
-
metadata: {
|
|
611
|
-
...metadata,
|
|
612
|
-
loadedSkills: this.loadedSkills,
|
|
613
|
-
activeTools: tools,
|
|
614
|
-
activeSkillNames: activeSkills.map((skill) => skill.name),
|
|
615
|
-
},
|
|
616
|
-
identity: this.activeIdentity,
|
|
617
|
-
},
|
|
618
|
-
middleware: this.agent.config.middleware,
|
|
619
|
-
emit: (event) => this.emit(event),
|
|
620
|
-
permission: this.agent.config.permission,
|
|
621
|
-
trust: this.agent.config.trust,
|
|
622
|
-
redactor: this.activeRedactor,
|
|
623
|
-
ledger: this.activeLedger,
|
|
624
|
-
effectStore: this.activeEffectStore,
|
|
625
|
-
ownership: this.activeOwnership,
|
|
626
|
-
identity: this.activeIdentity,
|
|
627
|
-
guardrails: this.activeGuardrails,
|
|
628
|
-
limitTracker: limits,
|
|
629
|
-
beforeExecute: async (mediatedCall) => {
|
|
630
|
-
const durable = this.activeDurable;
|
|
631
|
-
if (!durable)
|
|
632
|
-
return;
|
|
633
|
-
const pendingCalls = durable.state?.pendingCalls;
|
|
634
|
-
const matched = pendingCalls?.find((entry) => entry.call.id === mediatedCall.id && entry.status === "ready");
|
|
635
|
-
if (matched) {
|
|
636
|
-
await this.persistDurable({
|
|
637
|
-
...durable.state,
|
|
638
|
-
status: "running",
|
|
639
|
-
pendingCalls: pendingCalls.map((entry) => (entry === matched ? { ...entry, status: "dispatched" } : entry)),
|
|
640
|
-
interruption: undefined,
|
|
641
|
-
});
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
const pending = durable.state?.pending;
|
|
645
|
-
if (pending?.call.id === mediatedCall.id && pending.status === "ready") {
|
|
646
|
-
await this.persistDurable({
|
|
647
|
-
...durable.state,
|
|
648
|
-
status: "running",
|
|
649
|
-
pending: { ...pending, status: "dispatched" },
|
|
650
|
-
interruption: undefined,
|
|
651
|
-
});
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
if (!durable.options.interruptBeforeTool)
|
|
655
|
-
return;
|
|
656
|
-
if (this.matchStickyDecision(mediatedCall, registry)?.outcome === "allow_for_run")
|
|
657
|
-
return;
|
|
658
|
-
// Backstop for loops that dispatch without awaiting chargeToolRound: suspend on
|
|
659
|
-
// the first uncovered gated call with a single pending decision.
|
|
660
|
-
const approvalId = randomId("approval");
|
|
661
|
-
const decision = this.buildPendingDecision(mediatedCall, approvalId, registry, runId, metadata, controller.signal);
|
|
662
|
-
const interruption = {
|
|
663
|
-
kind: "tool_approval",
|
|
664
|
-
reason: decision.reason,
|
|
665
|
-
toolCallId: mediatedCall.id,
|
|
666
|
-
toolName: mediatedCall.name,
|
|
667
|
-
pendingDecisions: [decision],
|
|
668
|
-
};
|
|
669
|
-
throw new AgentRunSuspended(await this.suspendDurable({
|
|
670
|
-
runId,
|
|
671
|
-
model,
|
|
672
|
-
limits,
|
|
673
|
-
interruption,
|
|
674
|
-
pending: { call: mediatedCall, status: "ready" },
|
|
675
|
-
pendingCalls: [{ call: mediatedCall, status: "ready", approvalId }],
|
|
676
|
-
}), interruption);
|
|
677
|
-
},
|
|
678
|
-
// ponytail: RunOptions wins; array-compose deferred (roadmap: compose-later).
|
|
679
|
-
validate,
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
catch (error) {
|
|
683
|
-
// Link the suspension signal to the hosting call so the root suspension can
|
|
684
|
-
// synthesize this call's tool_result when the nested run later terminates.
|
|
685
|
-
if (error instanceof AgentDelegationSuspendedError && !error.toolCall)
|
|
686
|
-
error.toolCall = call;
|
|
687
|
-
throw error;
|
|
688
|
-
}
|
|
689
|
-
},
|
|
690
|
-
appendMessage: (message) => this.appendMessage(message, runId),
|
|
691
|
-
hasPendingSteers: () => this.pendingSteers.length > 0,
|
|
692
|
-
applyPendingSteers: () => this.applyPendingSteers(runId, metadata, controller.signal),
|
|
693
|
-
emit: (event) => {
|
|
694
|
-
if (event.type === "turn_started")
|
|
695
|
-
this.activeLoopTurn = event.turn;
|
|
696
|
-
if (event.type === "artifact_finished")
|
|
697
|
-
artifactFinished = true;
|
|
698
|
-
if (event.type === "artifact_failed") {
|
|
699
|
-
const first = event.result.errors?.[0];
|
|
700
|
-
const reason = event.result.metadata?.reason;
|
|
701
|
-
artifactFailedInfo = {
|
|
702
|
-
message: first?.message ?? "artifact failed",
|
|
703
|
-
code: typeof reason === "string" || typeof reason === "number" ? reason : "artifact_failed",
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
this.emit(event);
|
|
707
|
-
},
|
|
708
|
-
};
|
|
709
|
-
const replayToolResult = async (result) => {
|
|
710
|
-
await ctx.appendMessage({
|
|
711
|
-
role: "tool",
|
|
712
|
-
content: [
|
|
713
|
-
{ type: "tool_result", toolCallId: result.toolCallId, name: result.name, result: result.value, error: result.error },
|
|
714
|
-
...(result.content ?? []),
|
|
715
|
-
],
|
|
716
|
-
metadata: result.metadata,
|
|
717
|
-
});
|
|
718
|
-
};
|
|
719
|
-
// Handles a nested-run suspension signal: sticky auto-apply appends a synthesized
|
|
720
|
-
// tool_result and returns; anything else re-suspends the root (throws AgentRunSuspended).
|
|
721
|
-
const handleNestedSignal = async (error) => {
|
|
722
|
-
const durableOptions = this.activeDurable?.options;
|
|
723
|
-
if (!durableOptions || !error.toolCall) {
|
|
724
|
-
throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Nested-run suspension requires durable run state and a hosting tool call");
|
|
725
|
-
}
|
|
726
|
-
if (error.pendingDecisions.length === 0) {
|
|
727
|
-
throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Nested-run suspension carried no pending decisions");
|
|
728
|
-
}
|
|
729
|
-
const applied = await applyNestedRun({
|
|
730
|
-
ref: error.ref,
|
|
731
|
-
toolCall: error.toolCall,
|
|
732
|
-
path: error.path ?? [],
|
|
733
|
-
pending: error.pendingDecisions,
|
|
734
|
-
hook: durableOptions.resumeNestedRun,
|
|
735
|
-
});
|
|
736
|
-
if ("toolResult" in applied) {
|
|
737
|
-
await replayToolResult(applied.toolResult);
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
await suspendNested({ entry: applied.entry, toolCall: error.toolCall, pending: applied.pending });
|
|
741
|
-
};
|
|
742
|
-
// Route decided nested-run approvals back to their children before replaying own calls.
|
|
743
|
-
// Undecided or re-suspended children re-suspend the root with the surfaced remainder.
|
|
744
|
-
let resumePendingCalls = resumed?.state?.pendingCalls;
|
|
745
|
-
if (resumed?.state?.nestedRuns?.length) {
|
|
746
|
-
const nestedRuns = resumed.state.nestedRuns;
|
|
747
|
-
const hook = this.activeDurable?.options.resumeNestedRun;
|
|
748
|
-
const oldPending = resumed.state.interruption?.pendingDecisions ?? [];
|
|
749
|
-
const nestedApprovalIds = new Set(nestedRuns.flatMap((entry) => entry.approvals.map((approval) => approval.id)));
|
|
750
|
-
const remainingNested = [];
|
|
751
|
-
const surfacedPending = [];
|
|
752
|
-
const resolvedToolCallIds = new Set();
|
|
753
|
-
for (const entry of nestedRuns) {
|
|
754
|
-
const grouped = [];
|
|
755
|
-
for (const approval of entry.approvals) {
|
|
756
|
-
const decision = entry.decisions?.[approval.id] ?? resumed.decisions?.get(approval.id);
|
|
757
|
-
if (decision)
|
|
758
|
-
grouped.push({ ...decision, approvalId: approval.childApprovalId });
|
|
759
|
-
}
|
|
760
|
-
if (grouped.length === 0) {
|
|
761
|
-
remainingNested.push(entry);
|
|
762
|
-
surfacedPending.push(...oldPending.filter((pending) => entry.approvals.some((approval) => approval.id === pending.approvalId)));
|
|
763
|
-
continue;
|
|
764
|
-
}
|
|
765
|
-
if (!hook) {
|
|
766
|
-
throw new AgentDecisionError("ERR_PRISM_DECISION_INVALID", "Nested-run decisions require a resumeNestedRun hook");
|
|
767
|
-
}
|
|
768
|
-
const toolCall = resumePendingCalls?.find((pending) => pending.call.id === entry.toolCallId)?.call;
|
|
769
|
-
if (!toolCall)
|
|
770
|
-
throw new AgentRunStateError("Nested run link is missing its tool call");
|
|
771
|
-
const ref = { runId: entry.runId, ...(entry.sessionId ? { sessionId: entry.sessionId } : {}) };
|
|
772
|
-
const outcome = await hook({ ref, toolCallId: entry.toolCallId, path: entry.path }, grouped);
|
|
773
|
-
if (outcome.status !== "suspended") {
|
|
774
|
-
await replayToolResult(nestedOutcomeToolResult(outcome, entry.toolCallId, toolCall.name));
|
|
775
|
-
resolvedToolCallIds.add(entry.toolCallId);
|
|
776
|
-
continue;
|
|
777
|
-
}
|
|
778
|
-
const applied = await applyNestedRun({ ref, toolCall, path: entry.path, pending: outcome.pendingDecisions, hook });
|
|
779
|
-
if ("toolResult" in applied) {
|
|
780
|
-
await replayToolResult(applied.toolResult);
|
|
781
|
-
resolvedToolCallIds.add(entry.toolCallId);
|
|
782
|
-
}
|
|
783
|
-
else {
|
|
784
|
-
remainingNested.push(applied.entry);
|
|
785
|
-
surfacedPending.push(...applied.pending);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
resumePendingCalls = resumePendingCalls
|
|
789
|
-
?.filter((entry) => !resolvedToolCallIds.has(entry.call.id))
|
|
790
|
-
.map((entry) => {
|
|
791
|
-
const decision = resumed.decisions?.get(entry.approvalId);
|
|
792
|
-
return decision && !entry.decision ? { ...entry, decision } : entry;
|
|
793
|
-
});
|
|
794
|
-
if (this.activeDurable?.state) {
|
|
795
|
-
this.activeDurable.state = {
|
|
796
|
-
...this.activeDurable.state,
|
|
797
|
-
pendingCalls: resumePendingCalls?.length ? resumePendingCalls : undefined,
|
|
798
|
-
nestedRuns: remainingNested.length ? remainingNested : undefined,
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
const remainingOwn = oldPending.filter((pending) => !nestedApprovalIds.has(pending.approvalId) &&
|
|
802
|
-
!resumed.decisions?.has(pending.approvalId) &&
|
|
803
|
-
!resumePendingCalls?.some((entry) => entry.approvalId === pending.approvalId && entry.decision !== undefined));
|
|
804
|
-
if (remainingOwn.length > 0 || surfacedPending.length > 0) {
|
|
805
|
-
const pendingDecisions = [...remainingOwn, ...surfacedPending];
|
|
806
|
-
const single = pendingDecisions.length === 1 ? pendingDecisions[0] : undefined;
|
|
807
|
-
const interruption = {
|
|
808
|
-
kind: single?.kind ?? "tool_approval",
|
|
809
|
-
reason: `${pendingDecisions.length} approval request(s) remain`,
|
|
810
|
-
...(single?.toolCallId ? { toolCallId: single.toolCallId } : {}),
|
|
811
|
-
...(single?.scope.toolName ? { toolName: single.scope.toolName } : {}),
|
|
812
|
-
pendingDecisions,
|
|
813
|
-
};
|
|
814
|
-
throw new AgentRunSuspended(await this.suspendDurable({
|
|
815
|
-
runId,
|
|
816
|
-
model,
|
|
817
|
-
limits,
|
|
818
|
-
interruption,
|
|
819
|
-
pendingCalls: resumePendingCalls,
|
|
820
|
-
nestedRuns: remainingNested,
|
|
821
|
-
}), interruption);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
if (resumePendingCalls?.length) {
|
|
825
|
-
for (const entry of resumePendingCalls) {
|
|
826
|
-
if (entry.status !== "ready")
|
|
827
|
-
continue;
|
|
828
|
-
const decision = entry.decision ?? resumed?.decisions?.get(entry.approvalId);
|
|
829
|
-
if (decision && (decision.outcome === "reject_once" || decision.outcome === "reject_for_run")) {
|
|
830
|
-
await replayToolResult({
|
|
831
|
-
toolCallId: entry.call.id,
|
|
832
|
-
name: entry.call.name,
|
|
833
|
-
error: { code: "approval_rejected", message: decision.reason ?? "Approval rejected" },
|
|
834
|
-
});
|
|
835
|
-
continue;
|
|
836
|
-
}
|
|
837
|
-
if (decision?.elicitation !== undefined) {
|
|
838
|
-
// Elicitation acceptance resolves the suspended call with the validated payload.
|
|
839
|
-
await replayToolResult({ toolCallId: entry.call.id, name: entry.call.name, value: decision.elicitation });
|
|
840
|
-
continue;
|
|
841
|
-
}
|
|
842
|
-
const call = decision?.modifiedArguments ? { ...entry.call, arguments: decision.modifiedArguments } : entry.call;
|
|
843
|
-
try {
|
|
844
|
-
await replayToolResult(await ctx.dispatchToolCall(call));
|
|
845
|
-
}
|
|
846
|
-
catch (error) {
|
|
847
|
-
if (!(error instanceof AgentDelegationSuspendedError))
|
|
848
|
-
throw error;
|
|
849
|
-
await handleNestedSignal(error);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
else if (resumed?.state?.pending?.status === "ready") {
|
|
854
|
-
await replayToolResult(await ctx.dispatchToolCall(resumed.state.pending.call));
|
|
855
|
-
}
|
|
856
|
-
const resumedLoopState = resumed?.state?.loopState;
|
|
857
|
-
if (resumedLoopState) {
|
|
858
|
-
if (loop.name !== resumedLoopState.name || (loop.revision ?? "1") !== resumedLoopState.revision) {
|
|
859
|
-
throw new AgentLoopStateError("ERR_PRISM_LOOP_REVISION", `Loop ${resumedLoopState.name} revision ${resumedLoopState.revision} does not match the resumed durable run`);
|
|
860
|
-
}
|
|
861
|
-
loop.restore?.(resumedLoopState.snapshot);
|
|
862
|
-
}
|
|
863
|
-
let loopUsage;
|
|
864
|
-
while (true) {
|
|
865
|
-
try {
|
|
866
|
-
loopUsage = await loop.run(ctx);
|
|
867
|
-
await suspendGatedRound();
|
|
868
|
-
break;
|
|
869
|
-
}
|
|
870
|
-
catch (error) {
|
|
871
|
-
if (!(error instanceof AgentDelegationSuspendedError))
|
|
872
|
-
throw error;
|
|
873
|
-
await handleNestedSignal(error);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
if (loop.name === "generate-validate-revise" && !artifactFinished) {
|
|
877
|
-
throw Object.assign(new Error(artifactFailedInfo?.message ?? "artifact loop ended without a validated artifact"), {
|
|
878
|
-
name: "ArtifactFailed",
|
|
879
|
-
code: artifactFailedInfo?.code ?? "artifact_failed",
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
usage = runUsage.value() ?? loopUsage;
|
|
883
|
-
if (usage && this.activeLedger) {
|
|
884
|
-
const usageRecord = {
|
|
885
|
-
id: randomId("usage"),
|
|
886
|
-
sessionId: this.id,
|
|
887
|
-
runId,
|
|
888
|
-
scope: "run_total",
|
|
889
|
-
usage,
|
|
890
|
-
recordedAt: new Date().toISOString(),
|
|
891
|
-
...this.activeOwnership,
|
|
892
|
-
};
|
|
893
|
-
await this.activeLedger.appendUsage(redactRunLedgerRecord(usageRecord, this.activeRedactor));
|
|
894
|
-
}
|
|
895
|
-
await this.drainLedger();
|
|
896
|
-
const runState = this.activeDurable?.state
|
|
897
|
-
? await this.persistDurable({
|
|
898
|
-
...this.activeDurable.state,
|
|
899
|
-
status: "succeeded",
|
|
900
|
-
pending: undefined,
|
|
901
|
-
pendingCalls: undefined,
|
|
902
|
-
nestedRuns: undefined,
|
|
903
|
-
stickyDecisions: undefined,
|
|
904
|
-
interruption: undefined,
|
|
905
|
-
loopState: undefined,
|
|
906
|
-
})
|
|
907
|
-
: undefined;
|
|
908
|
-
this.emit({
|
|
909
|
-
type: "agent_finished",
|
|
910
|
-
sessionId: this.id,
|
|
911
|
-
runId,
|
|
912
|
-
usage,
|
|
913
|
-
// F4: loop strategies record why a ceiling ended the run cleanly (e.g. turn_limit).
|
|
914
|
-
...(ctx.finishReason ? { finishReason: ctx.finishReason } : {}),
|
|
915
|
-
});
|
|
916
|
-
return this.buildRunResult({ runId, status: "succeeded", usage, runState });
|
|
917
|
-
}
|
|
918
|
-
catch (error) {
|
|
919
|
-
if (error instanceof AgentRunSuspended) {
|
|
920
|
-
runStatus = "suspended";
|
|
921
|
-
const version = error.state.version;
|
|
922
|
-
this.emit({ type: "agent_suspended", sessionId: this.id, runId, interruption: error.interruption, version });
|
|
923
|
-
return this.buildRunResult({ runId, status: "suspended", runState: error.state, interruption: error.interruption });
|
|
924
|
-
}
|
|
925
|
-
runError = errorToErrorInfo(error);
|
|
926
|
-
this.emit({ type: "error", sessionId: this.id, runId, error: runError });
|
|
927
|
-
const breach = error instanceof RunLimitError ? error.breach : limits.breach;
|
|
928
|
-
runStatus = breach ? "failed" : controller.signal.aborted ? "aborted" : "failed";
|
|
929
|
-
const runState = this.activeDurable?.state
|
|
930
|
-
? await this.persistDurable({
|
|
931
|
-
...this.activeDurable.state,
|
|
932
|
-
status: runStatus,
|
|
933
|
-
interruption: undefined,
|
|
934
|
-
loopState: undefined,
|
|
935
|
-
pendingCalls: undefined,
|
|
936
|
-
nestedRuns: undefined,
|
|
937
|
-
stickyDecisions: undefined,
|
|
938
|
-
})
|
|
939
|
-
: undefined;
|
|
940
|
-
const result = this.buildRunResult({
|
|
941
|
-
runId,
|
|
942
|
-
status: runStatus,
|
|
943
|
-
usage: runUsage.value() ?? usage,
|
|
944
|
-
limit: breach,
|
|
945
|
-
error: runError,
|
|
946
|
-
abortReason: !breach && controller.signal.aborted ? String(controller.signal.reason) : undefined,
|
|
947
|
-
runState,
|
|
948
|
-
});
|
|
949
|
-
throw new AgentRunError(result, { cause: error });
|
|
950
|
-
}
|
|
951
|
-
finally {
|
|
952
|
-
if (this.activeRun === controller)
|
|
953
|
-
this.activeRun = undefined;
|
|
954
|
-
this.activeRunId = undefined;
|
|
955
|
-
this.activeLoop = undefined;
|
|
956
|
-
this.activeGatedRound = undefined;
|
|
957
|
-
this.activeProviderTurnAbort = undefined;
|
|
958
|
-
this.pendingSoftInterrupt = false;
|
|
959
|
-
this.pendingSteers = [];
|
|
960
|
-
this.pendingSteerBytes = 0;
|
|
961
|
-
try {
|
|
962
|
-
await this.drainLedger();
|
|
963
|
-
if (this.activeLedger) {
|
|
964
|
-
const status = runStatus;
|
|
965
|
-
const finishRecord = {
|
|
966
|
-
id: runId,
|
|
967
|
-
sessionId: this.id,
|
|
968
|
-
branchId: this.currentLeafId,
|
|
969
|
-
model,
|
|
970
|
-
provider: model.provider,
|
|
971
|
-
idempotencyKey: this.activeIdempotencyKey,
|
|
972
|
-
status,
|
|
973
|
-
startedAt,
|
|
974
|
-
finishedAt: new Date().toISOString(),
|
|
975
|
-
abortReason: controller.signal.aborted ? String(controller.signal.reason) : undefined,
|
|
976
|
-
error: runError,
|
|
977
|
-
...(this.activePromptVersion ? { promptVersion: this.activePromptVersion } : {}),
|
|
978
|
-
...this.activeOwnership,
|
|
979
|
-
};
|
|
980
|
-
await this.activeLedger.appendRun(redactRunLedgerRecord(finishRecord, this.activeRedactor));
|
|
981
|
-
if (isFlushableRunLedger(this.activeLedger) && this.activeLedger.durability === "flush_on_terminal")
|
|
982
|
-
await this.activeLedger.flush();
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
finally {
|
|
986
|
-
this.activeLedger = undefined;
|
|
987
|
-
this.activeEffectStore = undefined;
|
|
988
|
-
this.activeOwnership = undefined;
|
|
989
|
-
this.activeIdentity = undefined;
|
|
990
|
-
this.activeIdempotencyKey = undefined;
|
|
991
|
-
this.activeGuardrails = undefined;
|
|
992
|
-
this.activeMetadata = undefined;
|
|
993
|
-
this.activePromptVersion = undefined;
|
|
994
|
-
this.activeLimits?.dispose();
|
|
995
|
-
this.activeLimits = undefined;
|
|
996
|
-
this.activeLimitOutputBuffer = false;
|
|
997
|
-
this.activeRedactor = undefined;
|
|
998
|
-
this.activeProvider = undefined;
|
|
999
|
-
cleanupSignal();
|
|
1000
|
-
this.closeSubscribers();
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
163
|
+
return executeRun(asSessionHost(this), input, options, runId, resumed);
|
|
1003
164
|
}
|
|
1004
165
|
prompt(input, options) {
|
|
1005
166
|
return this.run(input, options);
|
|
@@ -1049,159 +210,6 @@ export class RuntimeAgentSession {
|
|
|
1049
210
|
interruption: input.interruption,
|
|
1050
211
|
};
|
|
1051
212
|
}
|
|
1052
|
-
async suspendDurable(input) {
|
|
1053
|
-
const durable = this.activeDurable;
|
|
1054
|
-
if (!durable)
|
|
1055
|
-
throw new AgentRunStateError("Durable interruption is not configured");
|
|
1056
|
-
// Capture loop-local state before persisting the suspension. Undefined before the loop
|
|
1057
|
-
// starts (input-guardrail suspensions) and for snapshot-less built-ins.
|
|
1058
|
-
const loop = this.activeLoop;
|
|
1059
|
-
const loopState = loop?.snapshot ? boundedLoopSnapshot(loop.name, loop.revision ?? "1", loop.snapshot()) : undefined;
|
|
1060
|
-
const state = durable.state ??
|
|
1061
|
-
initialAgentRunState({
|
|
1062
|
-
agent: this.agent,
|
|
1063
|
-
options: durable.options,
|
|
1064
|
-
runId: input.runId,
|
|
1065
|
-
sessionId: this.id,
|
|
1066
|
-
leafId: this.currentLeafId,
|
|
1067
|
-
model: input.model,
|
|
1068
|
-
counters: input.limits.snapshot(),
|
|
1069
|
-
deadlineAt: input.limits.deadlineAt,
|
|
1070
|
-
status: "suspended",
|
|
1071
|
-
interruption: input.interruption,
|
|
1072
|
-
messages: input.messages,
|
|
1073
|
-
pending: input.pending,
|
|
1074
|
-
pendingCalls: input.pendingCalls,
|
|
1075
|
-
interruptBeforeTool: durable.options.interruptBeforeTool,
|
|
1076
|
-
});
|
|
1077
|
-
return this.persistDurable({
|
|
1078
|
-
...state,
|
|
1079
|
-
leafId: this.currentLeafId,
|
|
1080
|
-
status: "suspended",
|
|
1081
|
-
interruption: input.interruption,
|
|
1082
|
-
...(input.messages ? { input: input.messages } : {}),
|
|
1083
|
-
...(input.pending ? { pending: input.pending } : {}),
|
|
1084
|
-
...(input.pendingCalls ? { pendingCalls: input.pendingCalls } : {}),
|
|
1085
|
-
nestedRuns: input.nestedRuns ?? state.nestedRuns,
|
|
1086
|
-
...(loopState ? { loopState } : {}),
|
|
1087
|
-
counters: input.limits.snapshot(),
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
/** First attributed sticky whose scope and delegation path exactly match a nested decision. */
|
|
1091
|
-
matchNestedSticky(decision) {
|
|
1092
|
-
const stickies = this.activeDurable?.state?.stickyDecisions;
|
|
1093
|
-
return stickies?.find((sticky) => sticky.attribution !== undefined &&
|
|
1094
|
-
pathsEqual(sticky.attribution.path, decision.attribution?.path) &&
|
|
1095
|
-
decisionScopesEqual(sticky.scope, decision.scope));
|
|
1096
|
-
}
|
|
1097
|
-
/** First sticky decision whose scope exactly matches this call, if any. */
|
|
1098
|
-
matchStickyDecision(call, registry) {
|
|
1099
|
-
const stickies = this.activeDurable?.state?.stickyDecisions;
|
|
1100
|
-
if (!stickies?.length)
|
|
1101
|
-
return undefined;
|
|
1102
|
-
const identityRef = decisionIdentityRef(this.activeIdentity);
|
|
1103
|
-
let argumentsHash;
|
|
1104
|
-
let effectKind;
|
|
1105
|
-
let effectResolved = false;
|
|
1106
|
-
return stickies.find((sticky) => {
|
|
1107
|
-
if (sticky.attribution !== undefined)
|
|
1108
|
-
return false; // nested-run stickies match decisions, not calls
|
|
1109
|
-
const scope = sticky.scope;
|
|
1110
|
-
if (scope.toolName !== undefined && scope.toolName !== call.name)
|
|
1111
|
-
return false;
|
|
1112
|
-
if (scope.identity !== undefined && scope.identity !== identityRef)
|
|
1113
|
-
return false;
|
|
1114
|
-
if (scope.argumentsHash !== undefined) {
|
|
1115
|
-
argumentsHash ??= toolEffectArgumentsHash(call.arguments);
|
|
1116
|
-
if (scope.argumentsHash !== argumentsHash)
|
|
1117
|
-
return false;
|
|
1118
|
-
}
|
|
1119
|
-
if (scope.effectKind !== undefined) {
|
|
1120
|
-
if (!effectResolved) {
|
|
1121
|
-
effectResolved = true;
|
|
1122
|
-
const tool = registry.get(call.name);
|
|
1123
|
-
effectKind = tool?.effect
|
|
1124
|
-
? resolveToolEffectDeclaration(tool, call.arguments, { sessionId: this.id, runId: "", toolCallId: call.id })?.kind
|
|
1125
|
-
: undefined;
|
|
1126
|
-
}
|
|
1127
|
-
if (scope.effectKind !== effectKind)
|
|
1128
|
-
return false;
|
|
1129
|
-
}
|
|
1130
|
-
if (scope.actionConstraints) {
|
|
1131
|
-
for (const [key, value] of Object.entries(scope.actionConstraints)) {
|
|
1132
|
-
const actual = call.arguments[key];
|
|
1133
|
-
if (canonicalToolEffectJson(actual === undefined ? null : actual) !== canonicalToolEffectJson(value))
|
|
1134
|
-
return false;
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
return true;
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
/** Redacted pending-decision descriptor for one gated call; never carries raw arguments. */
|
|
1141
|
-
buildPendingDecision(call, approvalId, registry, runId, metadata, signal) {
|
|
1142
|
-
const tool = registry.get(call.name);
|
|
1143
|
-
const declaration = tool?.effect
|
|
1144
|
-
? resolveToolEffectDeclaration(tool, call.arguments, {
|
|
1145
|
-
sessionId: this.id,
|
|
1146
|
-
runId,
|
|
1147
|
-
toolCallId: call.id,
|
|
1148
|
-
signal,
|
|
1149
|
-
metadata,
|
|
1150
|
-
})
|
|
1151
|
-
: undefined;
|
|
1152
|
-
const identityRef = decisionIdentityRef(this.activeIdentity);
|
|
1153
|
-
const elicitation = toolElicitationRequest(tool, call.arguments, {
|
|
1154
|
-
sessionId: this.id,
|
|
1155
|
-
runId,
|
|
1156
|
-
toolCallId: call.id,
|
|
1157
|
-
signal,
|
|
1158
|
-
metadata,
|
|
1159
|
-
});
|
|
1160
|
-
return {
|
|
1161
|
-
approvalId,
|
|
1162
|
-
kind: elicitation ? "elicitation" : "tool_approval",
|
|
1163
|
-
toolCallId: call.id,
|
|
1164
|
-
scope: {
|
|
1165
|
-
toolName: call.name,
|
|
1166
|
-
argumentsHash: toolEffectArgumentsHash(call.arguments),
|
|
1167
|
-
...(declaration && declaration.kind !== "none" ? { effectKind: declaration.kind } : {}),
|
|
1168
|
-
...(identityRef ? { identity: identityRef } : {}),
|
|
1169
|
-
},
|
|
1170
|
-
reason: elicitation?.reason ?? "Tool side effect requires approval",
|
|
1171
|
-
...(elicitation ? { elicitationSchema: elicitation.schema } : {}),
|
|
1172
|
-
};
|
|
1173
|
-
}
|
|
1174
|
-
async persistDurable(state) {
|
|
1175
|
-
const durable = this.activeDurable;
|
|
1176
|
-
if (!durable)
|
|
1177
|
-
throw new AgentRunStateError("Durable run state is not configured");
|
|
1178
|
-
const persisted = durable.options.persistSessionState
|
|
1179
|
-
? {
|
|
1180
|
-
...state,
|
|
1181
|
-
sessionState: {
|
|
1182
|
-
loadedSkillNames: this.loadedSkills.list(),
|
|
1183
|
-
...(this.activatedTools.list().length ? { activatedToolNames: this.activatedTools.list() } : {}),
|
|
1184
|
-
...(durable.options.includeSkillBodies
|
|
1185
|
-
? {
|
|
1186
|
-
loadedSkillBodies: snapshotLoadedSkillBodies(this.activeRunSkills, this.loadedSkills, this.restoredSkillBodies.length ? new Map(this.restoredSkillBodies.map((e) => [e.name, e.instructions])) : undefined),
|
|
1187
|
-
}
|
|
1188
|
-
: {}),
|
|
1189
|
-
},
|
|
1190
|
-
}
|
|
1191
|
-
: state;
|
|
1192
|
-
const saved = await saveAgentRunState({
|
|
1193
|
-
checkpoints: durable.options.checkpoints,
|
|
1194
|
-
state: persisted,
|
|
1195
|
-
expectedVersion: durable.version,
|
|
1196
|
-
ownership: this.activeOwnership,
|
|
1197
|
-
fencingToken: durable.options.fencingToken,
|
|
1198
|
-
redactor: this.activeRedactor,
|
|
1199
|
-
maxStateBytes: durable.options.maxStateBytes,
|
|
1200
|
-
});
|
|
1201
|
-
durable.state = saved.state;
|
|
1202
|
-
durable.version = saved.record.version;
|
|
1203
|
-
return publicState(saved.state);
|
|
1204
|
-
}
|
|
1205
213
|
async compact(options = {}) {
|
|
1206
214
|
if (this.activeRun)
|
|
1207
215
|
throw new Error("Agent session already has an active run");
|
|
@@ -1324,180 +332,6 @@ export class RuntimeAgentSession {
|
|
|
1324
332
|
if (failure)
|
|
1325
333
|
throw failure;
|
|
1326
334
|
}
|
|
1327
|
-
async generateWithRetry(request, runId, options, signal, requestSecrets = [], turn = 1, recordUsage) {
|
|
1328
|
-
const retry = mergeRetry(this.agent.config.retry, options.retry);
|
|
1329
|
-
const secrets = [...requestSecrets, ...(retry?.secrets ?? [])];
|
|
1330
|
-
const policy = retry?.policy ?? (retry ? createDefaultRetryPolicy(retry) : undefined);
|
|
1331
|
-
for (let attempt = 1;; attempt += 1) {
|
|
1332
|
-
try {
|
|
1333
|
-
return await this.generateProviderTurn(request, runId, signal, secrets, turn, attempt, recordUsage);
|
|
1334
|
-
}
|
|
1335
|
-
catch (error) {
|
|
1336
|
-
if (error instanceof GuardrailError || isSteerSoftInterrupt(error))
|
|
1337
|
-
throw error;
|
|
1338
|
-
const failure = error instanceof ProviderTurnFailure ? error : undefined;
|
|
1339
|
-
const info = failure ? redactSecrets(failure.info, secrets) : errorToErrorInfo(error, secrets);
|
|
1340
|
-
if (!policy || failure?.observable)
|
|
1341
|
-
throw errorFromInfo(info);
|
|
1342
|
-
const context = { sessionId: this.id, runId, attempt, error: info, metadata: retry?.metadata, signal };
|
|
1343
|
-
let decision = await policy.decide(context);
|
|
1344
|
-
const payload = (await this.agent.config.middleware?.run("retry", { context, decision })) ?? {
|
|
1345
|
-
context,
|
|
1346
|
-
decision,
|
|
1347
|
-
};
|
|
1348
|
-
decision = payload.decision;
|
|
1349
|
-
if (!decision.retry)
|
|
1350
|
-
throw errorFromInfo(info);
|
|
1351
|
-
const delayMs = decision.delayMs ?? 0;
|
|
1352
|
-
this.emit({ type: "retry_scheduled", sessionId: this.id, runId, attempt, delayMs, error: info });
|
|
1353
|
-
await waitForRetry(decision, signal);
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
async generateProviderTurn(request, runId, signal, secrets = [], turn = 1, attempt = 1, recordUsage) {
|
|
1358
|
-
this.activeLimits.charge("maxProviderAttempts");
|
|
1359
|
-
this.activeLimits.charge("maxRequestBytes", jsonBytes(request));
|
|
1360
|
-
const startedAt = performance.now();
|
|
1361
|
-
const providerId = this.activeProvider?.id ?? request.model.provider;
|
|
1362
|
-
const buildMetadata = (extra = {}) => createProviderTurnMetadata(request, providerId, { attempt, ...extra });
|
|
1363
|
-
this.emit({
|
|
1364
|
-
type: "provider_turn_started",
|
|
1365
|
-
sessionId: this.id,
|
|
1366
|
-
runId,
|
|
1367
|
-
turn,
|
|
1368
|
-
metadata: buildMetadata(),
|
|
1369
|
-
});
|
|
1370
|
-
const content = [];
|
|
1371
|
-
const calls = [];
|
|
1372
|
-
const toolDeltas = [];
|
|
1373
|
-
let messageId;
|
|
1374
|
-
let started = false;
|
|
1375
|
-
let usage;
|
|
1376
|
-
let usageRecorded = false;
|
|
1377
|
-
const bufferedOutput = [];
|
|
1378
|
-
const bufferOutput = Boolean(this.activeGuardrails?.output?.length || this.activeLimitOutputBuffer);
|
|
1379
|
-
const emitOutput = (event) => {
|
|
1380
|
-
if (bufferOutput)
|
|
1381
|
-
bufferedOutput.push(event);
|
|
1382
|
-
else
|
|
1383
|
-
this.emit(event);
|
|
1384
|
-
};
|
|
1385
|
-
const recordTurnUsage = async () => {
|
|
1386
|
-
if (usageRecorded)
|
|
1387
|
-
return;
|
|
1388
|
-
usageRecorded = true;
|
|
1389
|
-
await recordUsage?.(usage, turn, attempt);
|
|
1390
|
-
};
|
|
1391
|
-
const turnAbort = new AbortController();
|
|
1392
|
-
const cleanupTurn = bridgeAbort(signal, turnAbort);
|
|
1393
|
-
this.activeProviderTurnAbort = turnAbort;
|
|
1394
|
-
if (this.pendingSoftInterrupt) {
|
|
1395
|
-
this.pendingSoftInterrupt = false;
|
|
1396
|
-
turnAbort.abort(new SteerSoftInterrupt());
|
|
1397
|
-
}
|
|
1398
|
-
const turnRequest = { ...request, signal: turnAbort.signal };
|
|
1399
|
-
try {
|
|
1400
|
-
throwIfAborted(turnAbort.signal);
|
|
1401
|
-
for await (const event of this.activeProvider.generate(turnRequest)) {
|
|
1402
|
-
throwIfAborted(turnAbort.signal);
|
|
1403
|
-
this.activeLimits.charge("maxResponseBytes", jsonBytes(event));
|
|
1404
|
-
if (event.type === "error")
|
|
1405
|
-
throw new ProviderTurnFailure(event.error, started);
|
|
1406
|
-
if (event.type === "usage")
|
|
1407
|
-
usage = event.usage;
|
|
1408
|
-
if (event.type === "done") {
|
|
1409
|
-
usage = event.usage ?? usage;
|
|
1410
|
-
break;
|
|
1411
|
-
}
|
|
1412
|
-
if (event.type === "message_start") {
|
|
1413
|
-
started = true;
|
|
1414
|
-
messageId = event.messageId;
|
|
1415
|
-
emitOutput({ type: "message_started", sessionId: this.id, runId, message: { id: messageId, role: "assistant", content: [] } });
|
|
1416
|
-
continue;
|
|
1417
|
-
}
|
|
1418
|
-
if (event.type === "content_delta" || event.type === "tool_call" || event.type === "tool_call_delta") {
|
|
1419
|
-
if (!started) {
|
|
1420
|
-
started = true;
|
|
1421
|
-
emitOutput({ type: "message_started", sessionId: this.id, runId, message: { role: "assistant", content: [] } });
|
|
1422
|
-
}
|
|
1423
|
-
if (event.type === "tool_call_delta") {
|
|
1424
|
-
toolDeltas.push(event);
|
|
1425
|
-
emitOutput({ type: "message_delta", sessionId: this.id, runId, content: providerToolCallDeltaContent(event) });
|
|
1426
|
-
continue;
|
|
1427
|
-
}
|
|
1428
|
-
const block = providerContent(event);
|
|
1429
|
-
content.push(block);
|
|
1430
|
-
if (block.type === "tool_call")
|
|
1431
|
-
calls.push(block);
|
|
1432
|
-
emitOutput({ type: "message_delta", sessionId: this.id, runId, content: block });
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
for (const call of reconstructMissingToolCalls(toolDeltas, calls)) {
|
|
1436
|
-
content.push(call);
|
|
1437
|
-
calls.push(call);
|
|
1438
|
-
emitOutput({ type: "message_delta", sessionId: this.id, runId, content: call });
|
|
1439
|
-
}
|
|
1440
|
-
await recordTurnUsage();
|
|
1441
|
-
if (this.activeGuardrails?.output?.length) {
|
|
1442
|
-
assertGuardrailsAllowed(await runGuardrails({
|
|
1443
|
-
stage: "output",
|
|
1444
|
-
guardrails: this.activeGuardrails,
|
|
1445
|
-
value: { content, calls, messageId, started, usage },
|
|
1446
|
-
context: { sessionId: this.id, runId, metadata: this.activeMetadata ?? {}, signal: turnAbort.signal },
|
|
1447
|
-
redactor: this.activeRedactor,
|
|
1448
|
-
emit: (event) => this.emit(event),
|
|
1449
|
-
}));
|
|
1450
|
-
}
|
|
1451
|
-
if (bufferOutput)
|
|
1452
|
-
for (const event of bufferedOutput)
|
|
1453
|
-
this.emit(event);
|
|
1454
|
-
const latencyMs = Math.round(performance.now() - startedAt);
|
|
1455
|
-
this.emit({
|
|
1456
|
-
type: "provider_turn_finished",
|
|
1457
|
-
sessionId: this.id,
|
|
1458
|
-
runId,
|
|
1459
|
-
turn,
|
|
1460
|
-
metadata: buildMetadata({ latencyMs }),
|
|
1461
|
-
usage,
|
|
1462
|
-
});
|
|
1463
|
-
return { content, calls, messageId, started, usage };
|
|
1464
|
-
}
|
|
1465
|
-
catch (error) {
|
|
1466
|
-
if (isSteerSoftInterrupt(error) || isSteerSoftInterrupt(turnAbort.signal.reason)) {
|
|
1467
|
-
await recordTurnUsage();
|
|
1468
|
-
const latencyMs = Math.round(performance.now() - startedAt);
|
|
1469
|
-
this.emit({
|
|
1470
|
-
type: "provider_turn_finished",
|
|
1471
|
-
sessionId: this.id,
|
|
1472
|
-
runId,
|
|
1473
|
-
turn,
|
|
1474
|
-
metadata: buildMetadata({ latencyMs }),
|
|
1475
|
-
usage,
|
|
1476
|
-
});
|
|
1477
|
-
throw new SteerSoftInterrupt();
|
|
1478
|
-
}
|
|
1479
|
-
const latencyMs = Math.round(performance.now() - startedAt);
|
|
1480
|
-
const info = error instanceof ProviderTurnFailure ? redactSecrets(error.info, secrets) : errorToErrorInfo(error, secrets);
|
|
1481
|
-
await recordTurnUsage();
|
|
1482
|
-
this.emit({
|
|
1483
|
-
type: "provider_turn_finished",
|
|
1484
|
-
sessionId: this.id,
|
|
1485
|
-
runId,
|
|
1486
|
-
turn,
|
|
1487
|
-
metadata: buildMetadata({ latencyMs, httpStatus: readProviderHttpStatus(info) }),
|
|
1488
|
-
usage,
|
|
1489
|
-
error: info,
|
|
1490
|
-
});
|
|
1491
|
-
if (error instanceof GuardrailError || error instanceof ProviderTurnFailure)
|
|
1492
|
-
throw error;
|
|
1493
|
-
throw new ProviderTurnFailure(info, started);
|
|
1494
|
-
}
|
|
1495
|
-
finally {
|
|
1496
|
-
cleanupTurn();
|
|
1497
|
-
if (this.activeProviderTurnAbort === turnAbort)
|
|
1498
|
-
this.activeProviderTurnAbort = undefined;
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
335
|
async applyPendingSteers(runId, metadata, signal) {
|
|
1502
336
|
if (this.pendingSteers.length === 0)
|
|
1503
337
|
return false;
|