@arnilo/prism 0.6.0 → 0.8.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.
Files changed (178) hide show
  1. package/CHANGELOG.md +79 -5
  2. package/README.md +12 -11
  3. package/dist/agent-approval.d.ts +4 -0
  4. package/dist/agent-approval.js +5 -1
  5. package/dist/agent-definitions.js +1 -0
  6. package/dist/agent-run-lifecycle.js +39 -4
  7. package/dist/agent-run-state.d.ts +18 -0
  8. package/dist/agent-run-state.js +39 -9
  9. package/dist/agent-session/helpers.js +6 -1
  10. package/dist/agent-session/session/assemble.js +159 -7
  11. package/dist/agent-session/session/persist.d.ts +16 -0
  12. package/dist/agent-session/session/persist.js +64 -4
  13. package/dist/agent-session/session/provider-round.d.ts +3 -3
  14. package/dist/agent-session/session/provider-round.js +12 -6
  15. package/dist/agent-session/session/tool-round.js +5 -1
  16. package/dist/agent-session/session/types.d.ts +22 -1
  17. package/dist/agent-session/session.d.ts +16 -0
  18. package/dist/agent-session/session.js +42 -3
  19. package/dist/artifacts.d.ts +39 -1
  20. package/dist/artifacts.js +73 -0
  21. package/dist/attention-compiler.d.ts +121 -0
  22. package/dist/attention-compiler.js +479 -0
  23. package/dist/checkpoints.js +7 -11
  24. package/dist/cli-init.js +20 -6
  25. package/dist/context-budget.d.ts +20 -1
  26. package/dist/context-budget.js +10 -1
  27. package/dist/contracts-core/agent.d.ts +7 -0
  28. package/dist/contracts-core/attention.d.ts +66 -0
  29. package/dist/contracts-core/attention.js +2 -0
  30. package/dist/contracts-core/compaction.d.ts +59 -0
  31. package/dist/contracts-core/compaction.js +77 -1
  32. package/dist/contracts-core/content.d.ts +5 -0
  33. package/dist/contracts-core/loop.d.ts +42 -0
  34. package/dist/contracts-core/provider.d.ts +4 -0
  35. package/dist/contracts-core/run-limits.d.ts +2 -0
  36. package/dist/contracts-core.d.ts +1 -0
  37. package/dist/contracts-core.js +1 -0
  38. package/dist/contracts-protocol.d.ts +44 -3
  39. package/dist/contracts-run-state.d.ts +32 -5
  40. package/dist/evidence-grounding.d.ts +29 -0
  41. package/dist/evidence-grounding.js +162 -0
  42. package/dist/host-composition.d.ts +91 -0
  43. package/dist/host-composition.js +279 -0
  44. package/dist/index.d.ts +13 -6
  45. package/dist/index.js +7 -4
  46. package/dist/input.d.ts +13 -1
  47. package/dist/input.js +40 -1
  48. package/dist/provider-events.d.ts +3 -1
  49. package/dist/provider-events.js +2 -2
  50. package/dist/providers/transport.d.ts +3 -1
  51. package/dist/providers/transport.js +36 -0
  52. package/dist/redaction.js +18 -2
  53. package/dist/run-bundle.d.ts +89 -0
  54. package/dist/run-bundle.js +149 -0
  55. package/dist/secure-agent.d.ts +2 -0
  56. package/dist/secure-agent.js +6 -1
  57. package/dist/testing/state-concurrency-conformance.js +5 -12
  58. package/dist/tool-result-fold.d.ts +12 -0
  59. package/dist/tool-result-fold.js +13 -6
  60. package/dist/tools.d.ts +10 -0
  61. package/dist/tools.js +41 -0
  62. package/docs/acp-agent.md +42 -11
  63. package/docs/acp.md +2 -1
  64. package/docs/ag-ui.md +10 -3
  65. package/docs/agent-definitions.md +9 -1
  66. package/docs/agent-events.md +4 -1
  67. package/docs/agent-loops.md +33 -0
  68. package/docs/agent-session-runtime.md +8 -7
  69. package/docs/attention-compiler.md +272 -0
  70. package/docs/cli-rpc.md +4 -2
  71. package/docs/coding-agent-tools.md +1 -1
  72. package/docs/coding-security.md +6 -3
  73. package/docs/coding-tools.md +0 -1
  74. package/docs/coding-workspaces.md +22 -0
  75. package/docs/compaction-and-retry.md +36 -4
  76. package/docs/compaction-observational-memory.md +63 -10
  77. package/docs/connected-apps.md +116 -0
  78. package/docs/context-and-skills.md +17 -2
  79. package/docs/conversations.md +1 -1
  80. package/docs/core.md +1 -1
  81. package/docs/dev-inspector.md +4 -0
  82. package/docs/device-adapters.md +1 -0
  83. package/docs/diagrams.md +6 -6
  84. package/docs/document-reader.md +18 -10
  85. package/docs/documents.md +40 -11
  86. package/docs/durable-runs.md +87 -0
  87. package/docs/enterprise-postgres-state.md +6 -2
  88. package/docs/evaluations.md +168 -4
  89. package/docs/execution-timeline.md +186 -0
  90. package/docs/guardrails.md +33 -0
  91. package/docs/history/0.7.0-primitive-review.md +254 -0
  92. package/docs/history/079-messaging-primitive-review.md +391 -0
  93. package/docs/history/080-messaging-followon-primitive-review.md +234 -0
  94. package/docs/history/081-connected-apps-primitive-review.md +74 -0
  95. package/docs/history/083-prism-work-primitive-review.md +84 -0
  96. package/docs/history/084-primitive-review.md +96 -0
  97. package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
  98. package/docs/history/README.md +5 -0
  99. package/docs/history/migration-0.0.md +2 -2
  100. package/docs/history/release-handoffs.md +75 -1
  101. package/docs/host-compositions.md +149 -0
  102. package/docs/host-security.md +2 -2
  103. package/docs/hosted-sandboxes.md +94 -0
  104. package/docs/index.md +82 -45
  105. package/docs/input-and-prompt-assembly.md +1 -0
  106. package/docs/knowledge-sync.md +84 -0
  107. package/docs/language-intelligence.md +1 -1
  108. package/docs/live-testing.md +8 -3
  109. package/docs/mcp-tools.md +3 -1
  110. package/docs/memory-fabric.md +416 -0
  111. package/docs/messaging-channel-operations.md +166 -0
  112. package/docs/messaging-channels.md +150 -0
  113. package/docs/migrate-to-0.5.md +1 -1
  114. package/docs/migrate-to-0.6.md +1 -0
  115. package/docs/migrate-to-0.7.md +345 -0
  116. package/docs/migrate-to-0.8.md +124 -0
  117. package/docs/migration.md +43 -1
  118. package/docs/model-registry.md +12 -2
  119. package/docs/model-routing.md +79 -4
  120. package/docs/multi-agent-patterns.md +20 -6
  121. package/docs/observability.md +52 -1
  122. package/docs/openapi-tools.md +1 -1
  123. package/docs/operations.md +14 -4
  124. package/docs/options-index.md +47 -3
  125. package/docs/peer-dependencies.md +12 -10
  126. package/docs/postgres-persistence.md +1 -1
  127. package/docs/process-sessions.md +3 -1
  128. package/docs/prompt-registry.md +1 -1
  129. package/docs/provider-caching.md +4 -2
  130. package/docs/provider-conformance.md +1 -1
  131. package/docs/provider-layer.md +2 -2
  132. package/docs/provider-packages.md +22 -22
  133. package/docs/providers/bedrock.md +71 -7
  134. package/docs/providers/neuralwatt.md +5 -1
  135. package/docs/providers/openai.md +1 -1
  136. package/docs/rag.md +24 -8
  137. package/docs/realtime-voice.md +87 -0
  138. package/docs/release-and-install.md +53 -45
  139. package/docs/run-bundle.md +92 -0
  140. package/docs/runs-and-usage.md +17 -2
  141. package/docs/server.md +7 -3
  142. package/docs/sheets.md +9 -9
  143. package/docs/signal-channel.md +112 -0
  144. package/docs/speech.md +7 -1
  145. package/docs/sqlite-persistence.md +1 -1
  146. package/docs/supervisors.md +33 -5
  147. package/docs/telegram-channel.md +157 -0
  148. package/docs/testing.md +2 -2
  149. package/docs/thinking-and-reasoning.md +3 -1
  150. package/docs/tools.md +6 -5
  151. package/docs/web-tools.md +2 -1
  152. package/docs/wiki.md +1 -1
  153. package/docs/work-artifacts-and-review.md +14 -4
  154. package/docs/work-connectors.md +12 -10
  155. package/docs/work-sandbox.md +115 -0
  156. package/docs/work-tools.md +50 -18
  157. package/docs/workflows.md +69 -1
  158. package/docs/working-and-semantic-memory.md +25 -14
  159. package/package.json +5 -3
  160. package/templates/README.md +2 -0
  161. package/templates/business-worker/README.md.tmpl +19 -0
  162. package/templates/business-worker/env.example.tmpl +1 -0
  163. package/templates/business-worker/gitignore.tmpl +11 -0
  164. package/templates/business-worker/manifest.json +12 -0
  165. package/templates/business-worker/package.json.tmpl +23 -0
  166. package/templates/business-worker/src/agent.ts.tmpl +92 -0
  167. package/templates/business-worker/src/index.ts.tmpl +13 -0
  168. package/templates/business-worker/src/tests/agent.test.ts.tmpl +77 -0
  169. package/templates/business-worker/tsconfig.json.tmpl +15 -0
  170. package/templates/personal-assistant/README.md.tmpl +18 -0
  171. package/templates/personal-assistant/env.example.tmpl +1 -0
  172. package/templates/personal-assistant/gitignore.tmpl +11 -0
  173. package/templates/personal-assistant/manifest.json +11 -0
  174. package/templates/personal-assistant/package.json.tmpl +23 -0
  175. package/templates/personal-assistant/src/agent.ts.tmpl +65 -0
  176. package/templates/personal-assistant/src/index.ts.tmpl +13 -0
  177. package/templates/personal-assistant/src/tests/agent.test.ts.tmpl +28 -0
  178. package/templates/personal-assistant/tsconfig.json.tmpl +15 -0
@@ -3,6 +3,7 @@ import { AgentRunSuspended } from "../../agent-approval.js";
3
3
  import { resolveLoop, resolveToolConcurrency } from "../../agent-loops.js";
4
4
  import { validateRunStateOptions } from "../../agent-run-state.js";
5
5
  import { activeTools } from "../../agent-tool-dispatch.js";
6
+ import { resolveRunAttentionCompiler } from "../../attention-compiler.js";
6
7
  import { AgentLoopStateError, AgentRunError, AgentRunStateError } from "../../contracts.js";
7
8
  import { assertGuardrailsAllowed, runGuardrails } from "../../guardrails.js";
8
9
  import { identityTelemetryAttributes, ownershipFromIdentity, resolveRunIdentity } from "../../identity.js";
@@ -16,13 +17,106 @@ import { assertStructuredOutputRequestSupported, resolveRunProviderOptions } fro
16
17
  import { composeSystemPrompt, mergeSystemPromptConfig } from "../../system-prompts.js";
17
18
  import { resolveToolResultFold } from "../../tool-result-fold.js";
18
19
  import { createSearchToolsTool, createToolSearchState, resolveToolsDisclosure } from "../../tool-search.js";
19
- import { createToolRegistry } from "../../tools.js";
20
+ import { createToolRegistry, selectRunTools } from "../../tools.js";
20
21
  import { bridgeAbort, createUsageAccumulator, inputToMessages, isDurableLoop, isSteerSoftInterrupt, mergeGuardrails, throwIfAborted, } from "../helpers.js";
21
- import { cleanupRun, persistDurable, persistSucceeded, suspendDurable } from "./persist.js";
22
+ import { checkpointDurableTurn, cleanupRun, persistDurable, persistSucceeded, suspendDurable } from "./persist.js";
22
23
  import { generateWithRetry, recordProviderUsage } from "./provider-round.js";
23
24
  import { bindChargeToolRound, bindDispatchToolCall, replayDurableNestedAndPending, runLoopUntilSettled, suspendGatedRound, } from "./tool-round.js";
24
25
  const PROMPT_VERSION_MAX_NAME_BYTES = 256;
25
26
  const PROMPT_VERSION_HASH_PATTERN = /^sha256:[0-9a-f]{64}$/;
27
+ /** Cap on the host stop detail that reaches the result, ledger, and timeline (plan 084 Task 2). */
28
+ const TURN_STOP_DETAIL_MAX_BYTES = 256;
29
+ /**
30
+ * `RunOptions.turnPolicy` stopped the run at a turn boundary (plan 084 Task 2). Internal control
31
+ * signal: it unwinds any loop shape and `executeRun` turns it into a clean terminal success with
32
+ * `stopReason: "host_policy"` — never a run error.
33
+ */
34
+ class AgentRunStopped extends Error {
35
+ constructor() {
36
+ super("Agent run stopped by host turn policy");
37
+ this.name = "AgentRunStopped";
38
+ }
39
+ }
40
+ /** Host turn-policy misuse: a throwing or malformed callback fails the run closed. */
41
+ class TurnPolicyError extends Error {
42
+ code = "ERR_PRISM_TURN_POLICY";
43
+ constructor(message, options) {
44
+ super(message, options);
45
+ this.name = "TurnPolicyError";
46
+ }
47
+ }
48
+ /** Validate `RunOptions.turnPolicy` once, before any provider turn (plan 084 Task 2). */
49
+ function assertTurnPolicy(policy, resolvedLimits) {
50
+ if (policy === undefined)
51
+ return;
52
+ if (typeof policy !== "object" || policy === null)
53
+ throw new TypeError("RunOptions.turnPolicy must be an object");
54
+ if (policy.stop !== undefined && typeof policy.stop !== "function") {
55
+ throw new TypeError("RunOptions.turnPolicy.stop must be a function");
56
+ }
57
+ const maxTurns = policy.maxTurns;
58
+ if (maxTurns === undefined)
59
+ return;
60
+ if (!Number.isSafeInteger(maxTurns) || maxTurns < 1) {
61
+ throw new TypeError("RunOptions.turnPolicy.maxTurns must be a positive safe integer");
62
+ }
63
+ // Same narrowing law as `limits`: a run overlay may tighten the agent's cap, never widen it.
64
+ const configured = resolvedLimits.maxTurns;
65
+ if (configured !== null && maxTurns > configured) {
66
+ throw new TypeError(`RunOptions.turnPolicy.maxTurns (${maxTurns}) cannot widen limits.maxTurns (${configured})`);
67
+ }
68
+ }
69
+ /** Redact and bound a host stop reason; anything unusable fails the run closed. */
70
+ function boundedStopDetail(session, reason) {
71
+ if (typeof reason !== "string" || reason.length === 0) {
72
+ throw new TurnPolicyError("RunOptions.turnPolicy.stop must return a non-empty reason string");
73
+ }
74
+ const redacted = session.redact(reason);
75
+ if (Buffer.byteLength(redacted, "utf8") > TURN_STOP_DETAIL_MAX_BYTES) {
76
+ throw new TurnPolicyError(`RunOptions.turnPolicy.stop reason must be at most ${TURN_STOP_DETAIL_MAX_BYTES} UTF-8 bytes`);
77
+ }
78
+ return redacted;
79
+ }
80
+ /**
81
+ * Evaluate the host turn policy at the current provider-turn boundary (plan 084 Task 2). Returns
82
+ * the stop to record, or `undefined` to run the turn. Omitted policy → nothing is read or called.
83
+ */
84
+ function evaluateTurnStop(ctx) {
85
+ const policy = ctx.options.turnPolicy;
86
+ if (!policy)
87
+ return undefined;
88
+ const turn = Math.max(1, ctx.session.activeLoopTurn);
89
+ const turns = turn - 1;
90
+ if (policy.maxTurns !== undefined && turns >= policy.maxTurns)
91
+ return { reason: "turn_limit", detail: "maxTurns" };
92
+ if (!policy.stop)
93
+ return undefined;
94
+ const context = {
95
+ sessionId: ctx.session.id,
96
+ runId: ctx.runId,
97
+ turn,
98
+ turns,
99
+ toolCalls: ctx.toolCalls,
100
+ ...(ctx.runUsage.value() ? { usage: ctx.runUsage.value() } : {}),
101
+ metadata: ctx.metadata,
102
+ };
103
+ let decision;
104
+ try {
105
+ decision = policy.stop(context);
106
+ }
107
+ catch (error) {
108
+ throw new TurnPolicyError("RunOptions.turnPolicy.stop threw", { cause: error });
109
+ }
110
+ if (decision === null || typeof decision !== "object" || typeof decision.then === "function") {
111
+ throw new TurnPolicyError("RunOptions.turnPolicy.stop must synchronously return a TurnStopDecision");
112
+ }
113
+ const action = decision.action;
114
+ if (action === "continue")
115
+ return undefined;
116
+ if (action !== "stop")
117
+ throw new TurnPolicyError('RunOptions.turnPolicy.stop decision action must be "continue" or "stop"');
118
+ return { reason: "host_policy", detail: boundedStopDetail(ctx.session, decision.reason) };
119
+ }
26
120
  function assertPromptVersionRef(ref) {
27
121
  if (ref === undefined)
28
122
  return undefined;
@@ -61,7 +155,12 @@ async function assembleRoundContext(params) {
61
155
  };
62
156
  await session.activeLedger?.appendRun(redactRunLedgerRecord(startRecord, session.activeRedactor));
63
157
  await session.rebuildHistory();
64
- const { registry: baseRegistry, tools: activeToolList } = activeTools(session.agent.config.tools);
158
+ const { tools: listed } = activeTools(session.agent.config.tools);
159
+ const selected = selectRunTools(listed, options.toolNames, resumed?.state?.toolNames);
160
+ session.activeToolNames = selected.grant;
161
+ // Run-local snapshot: concurrent runs and MCP refresh must not mutate this registry.
162
+ const activeToolList = selected.tools;
163
+ const baseRegistry = createToolRegistry(activeToolList);
65
164
  const toolsDisclosure = resolveToolsDisclosure(options.toolsDisclosure, session.agent.config.toolsDisclosure);
66
165
  const toolSearch = toolsDisclosure === "search" && activeToolList.length > 0
67
166
  ? {
@@ -115,6 +214,26 @@ async function assembleRoundContext(params) {
115
214
  const providerOptions = resolveRunProviderOptions(options, session.agent.config);
116
215
  assertStructuredOutputRequestSupported(options.model ?? session.agent.config.model, providerOptions);
117
216
  const validate = options.validate ?? session.agent.config.validator;
217
+ // Resolved once per run, before any provider turn: a bad setting or a widening run overlay
218
+ // fails here rather than on the turn that happens to cross the ratio (plan 074 C12).
219
+ const attentionCompiler = resolveRunAttentionCompiler(session.agent.config.attentionCompiler, options.attentionCompiler, options.model ?? session.agent.config.model);
220
+ // Telemetry seam (plan 074 T6): one `attention_compiled` per mutated turn, counts and the
221
+ // measured ratio inputs only. Under-ratio turns and compiler-off runs emit nothing.
222
+ const onAttentionReport = attentionCompiler
223
+ ? (report) => session.emit({
224
+ type: "attention_compiled",
225
+ sessionId: session.id,
226
+ runId,
227
+ used: report.used,
228
+ usedAfter: report.usedAfter,
229
+ inputCap: report.inputCap,
230
+ triggerRatio: report.triggerRatio,
231
+ droppedThinkingTurns: report.droppedThinkingTurns,
232
+ stubbedToolResults: report.stubbedToolResults,
233
+ stubbedBytes: report.stubbedBytes,
234
+ truncated: report.truncated,
235
+ })
236
+ : undefined;
118
237
  const instructionInjectors = options.instructionInjectors ?? session.agent.config.instructionInjectors ?? [];
119
238
  const inputLayout = options.inputLayout ?? session.agent.config.inputLayout;
120
239
  const loop = resolveLoop(options, session.agent.config);
@@ -148,6 +267,8 @@ async function assembleRoundContext(params) {
148
267
  assembledTurn: false,
149
268
  artifactFinished: false,
150
269
  artifactFailedInfo: undefined,
270
+ toolCalls: 0,
271
+ toolResults: [],
151
272
  runUsage,
152
273
  loopCtx: undefined,
153
274
  };
@@ -183,6 +304,11 @@ async function assembleRoundContext(params) {
183
304
  toolsSearch: session.agent.config.toolsSearch,
184
305
  activatedTools: session.activatedTools,
185
306
  toolResultFold: resolveToolResultFold(options.toolResultFold, session.agent.config.toolResultFold),
307
+ attentionCompiler,
308
+ // Session-owned: a stub made earlier stays applied even on a later under-ratio turn, so
309
+ // the prompt-cache prefix is not rewritten (C10). Undefined when the compiler is off.
310
+ attentionSticky: attentionCompiler ? session.attentionStickyFor() : undefined,
311
+ onAttentionReport,
186
312
  loadedSkills: session.loadedSkills,
187
313
  tools,
188
314
  resourceLoader: session.agent.config.resourceLoader,
@@ -205,10 +331,24 @@ async function assembleRoundContext(params) {
205
331
  if (!ctx.assembledTurn)
206
332
  limits.charge("maxTurns");
207
333
  ctx.assembledTurn = false;
334
+ // Host turn policy (plan 084 Task 2): evaluated at the same turn boundary as the
335
+ // crash-recovery checkpoint below, before any provider work. Throwing unwinds any loop
336
+ // shape; `executeRun` converts it into a clean terminal success with `stopReason`.
337
+ const stop = evaluateTurnStop(ctx);
338
+ if (stop) {
339
+ ctx.runStop = stop;
340
+ ctx.loopCtx.finishReason = stop.reason;
341
+ throw new AgentRunStopped();
342
+ }
343
+ // Crash-recovery boundary (plan 084 Task 1): after the previous turn's tool results are in
344
+ // the store and before this provider request. No-op unless `checkpointPolicy: "every-turn"`.
345
+ if (session.activeDurable?.options.checkpointPolicy === "every-turn") {
346
+ await checkpointDurableTurn(session, { runId, model, limits });
347
+ }
208
348
  const policyResult = await session.applyProviderRequestPolicies(request, runId, options, metadata, controller.signal);
209
349
  const middlewareRequest = (await session.agent.config.middleware?.run("provider_request", policyResult.request)) ?? policyResult.request;
210
350
  try {
211
- return await generateWithRetry(session, session.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, session.activeLoopTurn, (turnUsage, turn, attempt) => recordProviderUsage(ctx, turnUsage, turn, attempt));
351
+ return await generateWithRetry(session, session.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, session.activeLoopTurn, (turnUsage, turn, attempt) => recordProviderUsage(ctx, turnUsage, turn, attempt), ctx.toolResults);
212
352
  }
213
353
  catch (error) {
214
354
  if (isSteerSoftInterrupt(error)) {
@@ -257,6 +397,7 @@ export async function executeRun(session, input, options, runId, resumed) {
257
397
  }
258
398
  const requestedLimits = options.limits;
259
399
  const resolvedLimits = resolveRunLimits(session.agent.config.limits, requestedLimits);
400
+ assertTurnPolicy(options.turnPolicy, resolvedLimits);
260
401
  const durableOptions = options.runState ?? session.agent.config.runState;
261
402
  if (session.agent.config.runState && options.runState && session.agent.config.runState !== options.runState) {
262
403
  throw new AgentRunStateError("RunOptions cannot replace agent durable run-state configuration");
@@ -299,6 +440,9 @@ export async function executeRun(session, input, options, runId, resumed) {
299
440
  const startedAt = new Date().toISOString();
300
441
  let runError;
301
442
  let runStatus = "succeeded";
443
+ // Set only on the clean-success path; the finish ledger record carries them (plan 084 Task 2).
444
+ let stopReason;
445
+ let stopDetail;
302
446
  const runUsage = createUsageAccumulator();
303
447
  let usage;
304
448
  const metadata = {
@@ -343,8 +487,16 @@ export async function executeRun(session, input, options, runId, resumed) {
343
487
  }
344
488
  ctx.loop.restore?.(resumedLoopState.snapshot);
345
489
  }
346
- const loopUsage = await runLoopUntilSettled(ctx);
347
- if (ctx.loop.name === "generate-validate-revise" && !ctx.artifactFinished) {
490
+ const loopUsage = await runLoopUntilSettled(ctx).catch((error) => {
491
+ // Host turn-policy stop (plan 084 Task 2): the loop was unwound on purpose at a turn
492
+ // boundary. Not an error — the run settles cleanly and stays resumable.
493
+ if (error instanceof AgentRunStopped)
494
+ return undefined;
495
+ throw error;
496
+ });
497
+ stopReason = ctx.runStop?.reason ?? ctx.loopCtx.finishReason;
498
+ stopDetail = ctx.runStop?.detail;
499
+ if (!ctx.runStop && ctx.loop.name === "generate-validate-revise" && !ctx.artifactFinished) {
348
500
  throw Object.assign(new Error(ctx.artifactFailedInfo?.message ?? "artifact loop ended without a validated artifact"), {
349
501
  name: "ArtifactFailed",
350
502
  code: ctx.artifactFailedInfo?.code ?? "artifact_failed",
@@ -387,7 +539,7 @@ export async function executeRun(session, input, options, runId, resumed) {
387
539
  throw new AgentRunError(result, { cause: error });
388
540
  }
389
541
  finally {
390
- await cleanupRun({ session, controller, cleanupSignal, runId, model, startedAt, runStatus, runError });
542
+ await cleanupRun({ session, controller, cleanupSignal, runId, model, startedAt, runStatus, runError, stopReason, stopDetail });
391
543
  }
392
544
  }
393
545
  //# sourceMappingURL=assemble.js.map
@@ -15,6 +15,19 @@ export declare function suspendDurable(session: SessionHost, input: {
15
15
  /** Full replacement when provided; otherwise the recorded nested runs are preserved. */
16
16
  readonly nestedRuns?: readonly NestedRunRef[];
17
17
  }): Promise<AgentRunState>;
18
+ /**
19
+ * Turn-boundary crash-recovery checkpoint (plan 084 Task 1). Called before each provider
20
+ * request when `checkpointPolicy: "every-turn"`; a no-op otherwise, so default-policy runs keep
21
+ * the 0.8.x checkpoint shape and write count unchanged. Pending-decision markers are dropped:
22
+ * at a turn boundary every gated call has been resolved or the run already suspended, and a
23
+ * stale marker must never replay. The recorded `checkpointPolicy` makes the cadence survive
24
+ * into a resumed run, and loop-local state rides along exactly as it does at suspension.
25
+ */
26
+ export declare function checkpointDurableTurn(session: SessionHost, input: {
27
+ readonly runId: string;
28
+ readonly model: ModelConfig;
29
+ readonly limits: RunLimitTracker;
30
+ }): Promise<void>;
18
31
  export declare function persistSucceeded(ctx: RoundContext, loopUsage: Usage | undefined): Promise<AgentRunResult>;
19
32
  export declare function cleanupRun(input: {
20
33
  session: SessionHost;
@@ -25,4 +38,7 @@ export declare function cleanupRun(input: {
25
38
  startedAt: string;
26
39
  runStatus: AgentRunResult["status"];
27
40
  runError: ErrorInfo | undefined;
41
+ /** Clean stop taxonomy for the finish record; only written for a succeeded run (plan 084 Task 2). */
42
+ stopReason?: import("../../contracts.js").AgentFinishReason;
43
+ stopDetail?: string;
28
44
  }): Promise<void>;
@@ -9,11 +9,14 @@ export async function persistDurable(session, state) {
9
9
  const durable = session.activeDurable;
10
10
  if (!durable)
11
11
  throw new AgentRunStateError("Durable run state is not configured");
12
+ const withGrant = session.activeToolNames !== undefined ? { ...state, toolNames: session.activeToolNames } : state;
13
+ const attentionSticky = session.serializedAttentionSticky();
12
14
  const persisted = durable.options.persistSessionState
13
15
  ? {
14
- ...state,
16
+ ...withGrant,
15
17
  sessionState: {
16
18
  loadedSkillNames: session.loadedSkills.list(),
19
+ ...(attentionSticky ? { attentionSticky } : {}),
17
20
  ...(session.activatedTools.list().length ? { activatedToolNames: session.activatedTools.list() } : {}),
18
21
  ...(durable.options.includeSkillBodies
19
22
  ? {
@@ -24,7 +27,7 @@ export async function persistDurable(session, state) {
24
27
  : {}),
25
28
  },
26
29
  }
27
- : state;
30
+ : withGrant;
28
31
  const saved = await saveAgentRunState({
29
32
  checkpoints: durable.options.checkpoints,
30
33
  state: persisted,
@@ -74,8 +77,50 @@ export async function suspendDurable(session, input) {
74
77
  counters: input.limits.snapshot(),
75
78
  });
76
79
  }
80
+ /**
81
+ * Turn-boundary crash-recovery checkpoint (plan 084 Task 1). Called before each provider
82
+ * request when `checkpointPolicy: "every-turn"`; a no-op otherwise, so default-policy runs keep
83
+ * the 0.8.x checkpoint shape and write count unchanged. Pending-decision markers are dropped:
84
+ * at a turn boundary every gated call has been resolved or the run already suspended, and a
85
+ * stale marker must never replay. The recorded `checkpointPolicy` makes the cadence survive
86
+ * into a resumed run, and loop-local state rides along exactly as it does at suspension.
87
+ */
88
+ export async function checkpointDurableTurn(session, input) {
89
+ const durable = session.activeDurable;
90
+ if (durable?.options.checkpointPolicy !== "every-turn")
91
+ return;
92
+ const loop = session.activeLoop;
93
+ const loopState = loop?.snapshot ? boundedLoopSnapshot(loop.name, loop.revision ?? "1", loop.snapshot()) : undefined;
94
+ const state = durable.state ??
95
+ initialAgentRunState({
96
+ agent: session.agent,
97
+ options: durable.options,
98
+ runId: input.runId,
99
+ sessionId: session.id,
100
+ leafId: session.currentLeafId,
101
+ model: input.model,
102
+ counters: input.limits.snapshot(),
103
+ deadlineAt: input.limits.deadlineAt,
104
+ status: "running",
105
+ interruptBeforeTool: durable.options.interruptBeforeTool,
106
+ });
107
+ await persistDurable(session, {
108
+ ...state,
109
+ leafId: session.currentLeafId,
110
+ status: "running",
111
+ interruption: undefined,
112
+ // The input messages are already in the session store by the time a turn boundary is
113
+ // reached; keeping them would re-append them on a later resume.
114
+ input: undefined,
115
+ pending: undefined,
116
+ pendingCalls: undefined,
117
+ ...(loopState ? { loopState } : {}),
118
+ counters: input.limits.snapshot(),
119
+ });
120
+ }
77
121
  export async function persistSucceeded(ctx, loopUsage) {
78
122
  const { session, runId, runUsage } = ctx;
123
+ const stop = ctx.runStop;
79
124
  const usage = runUsage.value() ?? loopUsage;
80
125
  if (usage && session.activeLedger) {
81
126
  const usageRecord = {
@@ -94,12 +139,15 @@ export async function persistSucceeded(ctx, loopUsage) {
94
139
  ? await persistDurable(session, {
95
140
  ...session.activeDurable.state,
96
141
  status: "succeeded",
142
+ // Plan 084 Task 2: a host-policy stop is terminal for the run but leaves the frontier
143
+ // intact — the loop state is kept and the state is marked continuable.
144
+ ...(stop ? { stopReason: "host_policy", leafId: session.currentLeafId } : {}),
97
145
  pending: undefined,
98
146
  pendingCalls: undefined,
99
147
  nestedRuns: undefined,
100
148
  stickyDecisions: undefined,
101
149
  interruption: undefined,
102
- loopState: undefined,
150
+ ...(stop ? {} : { loopState: undefined }),
103
151
  })
104
152
  : undefined;
105
153
  session.emit({
@@ -108,8 +156,17 @@ export async function persistSucceeded(ctx, loopUsage) {
108
156
  runId,
109
157
  usage,
110
158
  ...(ctx.loopCtx.finishReason ? { finishReason: ctx.loopCtx.finishReason } : {}),
159
+ ...(stop?.detail ? { stopDetail: stop.detail } : {}),
160
+ });
161
+ const stopReason = ctx.runStop?.reason ?? ctx.loopCtx.finishReason;
162
+ return session.buildRunResult({
163
+ runId,
164
+ status: "succeeded",
165
+ usage,
166
+ runState,
167
+ ...(stopReason ? { stopReason } : {}),
168
+ ...(stop?.detail ? { stopDetail: stop.detail } : {}),
111
169
  });
112
- return session.buildRunResult({ runId, status: "succeeded", usage, runState });
113
170
  }
114
171
  export async function cleanupRun(input) {
115
172
  const { session, controller, cleanupSignal, runId, model, startedAt, runStatus, runError } = input;
@@ -135,6 +192,8 @@ export async function cleanupRun(input) {
135
192
  status: runStatus,
136
193
  startedAt,
137
194
  finishedAt: new Date().toISOString(),
195
+ ...(input.runStatus === "succeeded" && input.stopReason ? { stopReason: input.stopReason } : {}),
196
+ ...(input.runStatus === "succeeded" && input.stopDetail ? { stopDetail: input.stopDetail } : {}),
138
197
  abortReason: controller.signal.aborted ? String(controller.signal.reason) : undefined,
139
198
  error: runError,
140
199
  ...(session.activePromptVersion ? { promptVersion: session.activePromptVersion } : {}),
@@ -155,6 +214,7 @@ export async function cleanupRun(input) {
155
214
  session.activeMetadata = undefined;
156
215
  session.activePromptVersion = undefined;
157
216
  session.activeLimits?.dispose();
217
+ session.activeToolNames = undefined;
158
218
  session.activeLimits = undefined;
159
219
  session.activeLimitOutputBuffer = false;
160
220
  session.activeRedactor = undefined;
@@ -1,6 +1,6 @@
1
1
  /** Provider-round phase of runInternal (plan 059). */
2
- import type { ProviderRequest, ProviderTurnResult, RunOptions, Usage } from "../../contracts.js";
2
+ import type { ProviderRequest, ProviderTurnResult, RunOptions, ToolResult, Usage } from "../../contracts.js";
3
3
  import type { RoundContext, SessionHost } from "./types.js";
4
4
  export declare function recordProviderUsage(ctx: RoundContext, turnUsage: Usage | undefined, turn: number, attempt: number): Promise<void>;
5
- export declare function generateWithRetry(session: SessionHost, request: ProviderRequest, runId: string, options: RunOptions, signal: AbortSignal, requestSecrets?: readonly (string | undefined)[], turn?: number, recordUsage?: (usage: Usage | undefined, turn: number, attempt: number) => Promise<void>): Promise<ProviderTurnResult>;
6
- export declare function generateProviderTurn(session: SessionHost, request: ProviderRequest, runId: string, signal: AbortSignal, secrets?: readonly (string | undefined)[], turn?: number, attempt?: number, recordUsage?: (usage: Usage | undefined, turn: number, attempt: number) => Promise<void>): Promise<ProviderTurnResult>;
5
+ export declare function generateWithRetry(session: SessionHost, request: ProviderRequest, runId: string, options: RunOptions, signal: AbortSignal, requestSecrets?: readonly (string | undefined)[], turn?: number, recordUsage?: (usage: Usage | undefined, turn: number, attempt: number) => Promise<void>, toolResults?: readonly ToolResult[]): Promise<ProviderTurnResult>;
6
+ export declare function generateProviderTurn(session: SessionHost, request: ProviderRequest, runId: string, signal: AbortSignal, secrets?: readonly (string | undefined)[], turn?: number, attempt?: number, recordUsage?: (usage: Usage | undefined, turn: number, attempt: number) => Promise<void>, toolResults?: readonly ToolResult[]): Promise<ProviderTurnResult>;
@@ -1,7 +1,7 @@
1
1
  /** Provider-round phase of runInternal (plan 059). */
2
2
  import { assertGuardrailsAllowed, GuardrailError, runGuardrails } from "../../guardrails.js";
3
3
  import { createProviderTurnMetadata, readProviderHttpStatus } from "../../observability.js";
4
- import { providerToolCallDeltaContent } from "../../provider-events.js";
4
+ import { providerError, providerToolCallDeltaContent } from "../../provider-events.js";
5
5
  import { errorToErrorInfo, redactRunLedgerRecord, redactSecrets } from "../../redaction.js";
6
6
  import { createDefaultRetryPolicy, waitForRetry } from "../../retry.js";
7
7
  import { bridgeAbort, errorFromInfo, isSteerSoftInterrupt, jsonBytes, mergeRetry, ProviderTurnFailure, providerContent, randomId, reconstructMissingToolCalls, SteerSoftInterrupt, throwIfAborted, } from "../helpers.js";
@@ -70,13 +70,13 @@ export async function recordProviderUsage(ctx, turnUsage, turn, attempt) {
70
70
  };
71
71
  await session.activeLedger.appendUsage(redactRunLedgerRecord(usageRecord, session.activeRedactor));
72
72
  }
73
- export async function generateWithRetry(session, request, runId, options, signal, requestSecrets = [], turn = 1, recordUsage) {
73
+ export async function generateWithRetry(session, request, runId, options, signal, requestSecrets = [], turn = 1, recordUsage, toolResults = []) {
74
74
  const retry = mergeRetry(session.agent.config.retry, options.retry);
75
75
  const secrets = [...requestSecrets, ...(retry?.secrets ?? [])];
76
76
  const policy = retry?.policy ?? (retry ? createDefaultRetryPolicy(retry) : undefined);
77
77
  for (let attempt = 1;; attempt += 1) {
78
78
  try {
79
- return await generateProviderTurn(session, request, runId, signal, secrets, turn, attempt, recordUsage);
79
+ return await generateProviderTurn(session, request, runId, signal, secrets, turn, attempt, recordUsage, toolResults);
80
80
  }
81
81
  catch (error) {
82
82
  if (error instanceof GuardrailError || isSteerSoftInterrupt(error))
@@ -100,7 +100,7 @@ export async function generateWithRetry(session, request, runId, options, signal
100
100
  }
101
101
  }
102
102
  }
103
- export async function generateProviderTurn(session, request, runId, signal, secrets = [], turn = 1, attempt = 1, recordUsage) {
103
+ export async function generateProviderTurn(session, request, runId, signal, secrets = [], turn = 1, attempt = 1, recordUsage, toolResults = []) {
104
104
  session.activeLimits.charge("maxProviderAttempts");
105
105
  session.activeLimits.charge("maxRequestBytes", jsonBytes(request));
106
106
  const startedAt = performance.now();
@@ -189,7 +189,13 @@ export async function generateProviderTurn(session, request, runId, signal, secr
189
189
  stage: "output",
190
190
  guardrails: session.activeGuardrails,
191
191
  value: { content, calls, messageId, started, usage },
192
- context: { sessionId: session.id, runId, metadata: session.activeMetadata ?? {}, signal: turnAbort.signal },
192
+ context: {
193
+ sessionId: session.id,
194
+ runId,
195
+ metadata: session.activeMetadata ?? {},
196
+ signal: turnAbort.signal,
197
+ toolResults,
198
+ },
193
199
  redactor: session.activeRedactor,
194
200
  emit: (event) => session.emit(event),
195
201
  }));
@@ -223,7 +229,7 @@ export async function generateProviderTurn(session, request, runId, signal, secr
223
229
  throw new SteerSoftInterrupt();
224
230
  }
225
231
  const latencyMs = Math.round(performance.now() - startedAt);
226
- const info = error instanceof ProviderTurnFailure ? redactSecrets(error.info, secrets) : errorToErrorInfo(error, secrets);
232
+ const info = error instanceof ProviderTurnFailure ? redactSecrets(error.info, secrets) : providerError(error, secrets).error;
227
233
  await recordTurnUsage();
228
234
  session.emit({
229
235
  type: "provider_turn_finished",
@@ -251,8 +251,9 @@ export function bindDispatchToolCall(ctx) {
251
251
  if (ctx.session.activeGatedRound?.has(call.id)) {
252
252
  return { toolCallId: call.id, name: call.name, metadata: { approvalPending: true } };
253
253
  }
254
+ ctx.toolCalls += 1;
254
255
  try {
255
- return await dispatchToolCall({
256
+ const result = await dispatchToolCall({
256
257
  call,
257
258
  registry: ctx.registry,
258
259
  context: {
@@ -278,6 +279,7 @@ export function bindDispatchToolCall(ctx) {
278
279
  ownership: ctx.session.activeOwnership,
279
280
  identity: ctx.session.activeIdentity,
280
281
  guardrails: ctx.session.activeGuardrails,
282
+ ...(ctx.tools.length > 0 ? { filter: { allow: ctx.tools.map((tool) => tool.name) } } : {}),
281
283
  limitTracker: ctx.limits,
282
284
  beforeExecute: async (mediatedCall) => {
283
285
  const durable = ctx.session.activeDurable;
@@ -328,6 +330,8 @@ export function bindDispatchToolCall(ctx) {
328
330
  },
329
331
  validate: ctx.validate,
330
332
  });
333
+ ctx.toolResults.push(result);
334
+ return result;
331
335
  }
332
336
  catch (error) {
333
337
  if (error instanceof AgentDelegationSuspendedError && !error.toolCall)
@@ -1,7 +1,8 @@
1
1
  /** Shared host/round types for runInternal phase split (plan 059). Internal only. */
2
2
  import type { ActiveDurableRun } from "../../agent-approval.js";
3
3
  import type { PendingToolCall } from "../../agent-run-state.js";
4
- import type { Agent, AgentEvent, AgentLoopStrategy, AgentRunResult, AIProvider, ErrorInfo, Guardrails, LoopContext, Message, ModelConfig, OwnershipScope, PendingDecision, PromptVersionRef, ProviderRequest, RunLedger, RunOptions, SessionEntry, SessionStore, Skill, ToolDefinition, ToolEffectStore, ToolRegistry, Usage } from "../../contracts.js";
4
+ import type { AttentionStickyFrontier, PersistedAttentionStickyFrontier } from "../../attention-compiler.js";
5
+ import type { Agent, AgentEvent, AgentFinishReason, AgentLoopStrategy, AgentRunResult, AIProvider, ErrorInfo, Guardrails, LoopContext, Message, ModelConfig, OwnershipScope, PendingDecision, PromptVersionRef, ProviderRequest, RunLedger, RunOptions, SessionEntry, SessionStore, Skill, ToolDefinition, ToolEffectStore, ToolRegistry, ToolResult, Usage } from "../../contracts.js";
5
6
  import type { AgentIdentity } from "../../identity.js";
6
7
  import type { AgentInput } from "../../input.js";
7
8
  import type { SecretRedactor } from "../../redaction.js";
@@ -47,6 +48,15 @@ export type SessionHost = {
47
48
  readonly activatedTools: ActiveToolSet;
48
49
  restoredSkillBodies: readonly LoadedSkillBodiesEntry[];
49
50
  activeRunSkills: readonly Skill[];
51
+ /** Names-only grant for this run; undefined means the full registered set. */
52
+ activeToolNames?: readonly string[];
53
+ /** Sticky mutation frontier for this session (plan 074 C10); session-owned so it survives
54
+ * across turns, runs, and provider rounds. Lazily created on first use. */
55
+ attentionStickyFor(): AttentionStickyFrontier;
56
+ /** Plan 074 P3: bounded frontier snapshot for durable checkpoints (undefined before any mutation). */
57
+ serializedAttentionSticky(): PersistedAttentionStickyFrontier | undefined;
58
+ /** Plan 074 P3: restore a frontier that was validated when the checkpoint was loaded. */
59
+ restoreAttentionSticky(persisted: PersistedAttentionStickyFrontier): void;
50
60
  invalidateSnapshot(): void;
51
61
  resolveRunProvider(options: RunOptions): void;
52
62
  emit(event: AgentEvent): void;
@@ -77,6 +87,11 @@ export type SessionHost = {
77
87
  snapshot(): Promise<SessionContextSnapshot>;
78
88
  };
79
89
  export declare function asSessionHost(session: unknown): SessionHost;
90
+ /** Why a run's loop ended, plus the host's stop detail when `RunOptions.turnPolicy` stopped it. */
91
+ export type RunStopInfo = {
92
+ readonly reason: AgentFinishReason;
93
+ readonly detail?: string;
94
+ };
80
95
  export type RoundContext = {
81
96
  session: SessionHost;
82
97
  input: AgentInput;
@@ -107,6 +122,12 @@ export type RoundContext = {
107
122
  message: string;
108
123
  code?: string | number;
109
124
  } | undefined;
125
+ /** Host tool calls dispatched in this run; the turn-boundary context's `toolCalls` (plan 084 Task 2). */
126
+ toolCalls: number;
127
+ /** Completed host tool results from this run, for output evidence guardrails (plan 084 Task 5). */
128
+ toolResults: ToolResult[];
129
+ /** Set when a `RunOptions.turnPolicy` stop ended the loop (plan 084 Task 2). */
130
+ runStop?: RunStopInfo;
110
131
  runUsage: {
111
132
  add(usage: Usage): void;
112
133
  value(): Usage | undefined;
@@ -1,6 +1,7 @@
1
1
  /** session (0.2.5 plan 025 Task 1 split). Moved verbatim from agent-session.ts; public surface unchanged behind the barrel. */
2
2
  import { ActiveDurableRun } from "../agent-approval.js";
3
3
  import type { PendingToolCall, StoredAgentRunState } from "../agent-run-state.js";
4
+ import { type AttentionStickyFrontier, type PersistedAttentionStickyFrontier } from "../attention-compiler.js";
4
5
  import type { Agent, AgentEvent, AgentRunResult, AgentRunState, AgentRunStateOptions, AgentSession, AgentSessionConfig, AIProvider, CompactionOptions, CompactionResult, ErrorInfo, Message, OwnershipScope, PendingDecision, PromptVersionRef, ProviderRequest, RunDecision, RunOptions, SessionEntry, Skill, SteerOptions, SubscribeOptions, ToolDefinition, ToolEffectStore, Usage } from "../contracts.js";
5
6
  import type { AgentIdentity } from "../identity.js";
6
7
  import type { AgentInput } from "../input.js";
@@ -48,6 +49,19 @@ export declare class RuntimeAgentSession implements AgentSession {
48
49
  restoredSkillBodies: readonly LoadedSkillBodiesEntry[];
49
50
  /** Skills of the current run (for the bodies snapshot); replaced at each run start. */
50
51
  activeRunSkills: readonly import("../contracts.js").Skill[];
52
+ /** Per-run tool allow-list (Task 21); undefined means the full registered set. */
53
+ activeToolNames?: readonly string[];
54
+ /** Sticky frontier for this session (plan 074 C10); created on first use, so a session whose
55
+ * agents never enable the compiler allocates nothing. Mutations stay applied once made, so a
56
+ * later under-ratio turn re-applies them instead of rewriting the prompt-cache prefix. */
57
+ private attentionSticky?;
58
+ attentionStickyFor(): AttentionStickyFrontier;
59
+ /** Plan 074 P3: bounded snapshot for a durable checkpoint; `undefined` when the session never
60
+ * mutated anything, so a compiler-off (or never-over-ratio) session persists nothing extra. */
61
+ serializedAttentionSticky(): PersistedAttentionStickyFrontier | undefined;
62
+ /** Plan 074 P3: restore a frontier validated at checkpoint load, so a resumed run keeps its
63
+ * stubs instead of re-deciding its first turn from the ratio. */
64
+ restoreAttentionSticky(persisted: PersistedAttentionStickyFrontier): void;
51
65
  /** Plan 015 Task 4: re-add persisted loaded-skill names (names only; bodies re-resolve on demand). */
52
66
  restoreLoadedSkills(names: readonly string[]): void;
53
67
  /** Plan 041: re-add persisted activated-tool names (names only; inert for absent tools). */
@@ -81,6 +95,8 @@ export declare class RuntimeAgentSession implements AgentSession {
81
95
  readonly limit?: import("../contracts.js").RunLimitBreach;
82
96
  readonly error?: ErrorInfo;
83
97
  readonly abortReason?: string;
98
+ readonly stopReason?: import("../contracts.js").AgentFinishReason;
99
+ readonly stopDetail?: string;
84
100
  readonly runState?: AgentRunState;
85
101
  readonly interruption?: import("../contracts.js").AgentRunInterruption;
86
102
  }): AgentRunResult;