@arnilo/prism 0.8.0 → 0.9.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 (121) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +11 -11
  3. package/dist/agent-approval.d.ts +11 -2
  4. package/dist/agent-event-source.d.ts +9 -1
  5. package/dist/agent-event-source.js +10 -3
  6. package/dist/agent-loops.js +7 -4
  7. package/dist/agent-run-lifecycle.d.ts +15 -1
  8. package/dist/agent-run-lifecycle.js +63 -6
  9. package/dist/agent-run-state.d.ts +22 -2
  10. package/dist/agent-run-state.js +57 -5
  11. package/dist/agent-session/helpers.js +14 -0
  12. package/dist/agent-session/session/assemble.js +126 -24
  13. package/dist/agent-session/session/persist.d.ts +11 -0
  14. package/dist/agent-session/session/persist.js +37 -11
  15. package/dist/agent-session/session/provider-round.d.ts +14 -4
  16. package/dist/agent-session/session/provider-round.js +185 -19
  17. package/dist/agent-session/session/tool-round.js +20 -1
  18. package/dist/agent-session/session/types.d.ts +25 -2
  19. package/dist/agent-session/session.d.ts +38 -4
  20. package/dist/agent-session/session.js +76 -5
  21. package/dist/attention-compiler.d.ts +51 -2
  22. package/dist/attention-compiler.js +282 -21
  23. package/dist/cache-helpers.d.ts +4 -2
  24. package/dist/cache-helpers.js +8 -6
  25. package/dist/checkpoint-restore.d.ts +45 -0
  26. package/dist/checkpoint-restore.js +54 -0
  27. package/dist/context-budget.d.ts +2 -1
  28. package/dist/context-budget.js +24 -2
  29. package/dist/contracts-core/agent.d.ts +30 -0
  30. package/dist/contracts-core/attention.d.ts +95 -0
  31. package/dist/contracts-core/content.d.ts +10 -0
  32. package/dist/contracts-core/guardrail-packs.d.ts +41 -0
  33. package/dist/contracts-core/guardrail-packs.js +2 -0
  34. package/dist/contracts-core/provider.d.ts +25 -0
  35. package/dist/contracts-core/run-limits.d.ts +19 -0
  36. package/dist/contracts-core/session.d.ts +23 -5
  37. package/dist/contracts-core/session.js +21 -2
  38. package/dist/contracts-core/usage.d.ts +40 -0
  39. package/dist/contracts-core/usage.js +8 -0
  40. package/dist/contracts-core.d.ts +2 -0
  41. package/dist/contracts-core.js +2 -0
  42. package/dist/contracts-protocol.d.ts +76 -2
  43. package/dist/contracts-run-state.d.ts +56 -1
  44. package/dist/guardrail-packs/coding-standard.d.ts +3 -0
  45. package/dist/guardrail-packs/coding-standard.js +63 -0
  46. package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
  47. package/dist/guardrail-packs/destructive-commands.js +46 -0
  48. package/dist/guardrail-packs/errors.d.ts +7 -0
  49. package/dist/guardrail-packs/errors.js +9 -0
  50. package/dist/guardrail-packs/index.d.ts +4 -0
  51. package/dist/guardrail-packs/index.js +15 -0
  52. package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
  53. package/dist/guardrail-packs/secrets-hygiene.js +23 -0
  54. package/dist/guardrail-packs/types.d.ts +16 -0
  55. package/dist/guardrail-packs/types.js +2 -0
  56. package/dist/guardrail-packs/validation-respect.d.ts +3 -0
  57. package/dist/guardrail-packs/validation-respect.js +53 -0
  58. package/dist/guardrails.d.ts +20 -1
  59. package/dist/guardrails.js +268 -0
  60. package/dist/index.d.ts +14 -9
  61. package/dist/index.js +9 -6
  62. package/dist/input.d.ts +8 -1
  63. package/dist/input.js +68 -6
  64. package/dist/middleware.d.ts +37 -2
  65. package/dist/middleware.js +41 -0
  66. package/dist/node/session-store-jsonl.js +18 -3
  67. package/dist/observability.js +6 -0
  68. package/dist/provider-events.d.ts +8 -2
  69. package/dist/provider-events.js +60 -2
  70. package/dist/providers/openai-compatible.js +6 -3
  71. package/dist/run-bundle.js +2 -1
  72. package/dist/run-limits.d.ts +11 -1
  73. package/dist/run-limits.js +46 -0
  74. package/dist/session-stores.d.ts +12 -1
  75. package/dist/session-stores.js +21 -4
  76. package/dist/testing/agent-event-source-conformance.js +41 -2
  77. package/dist/testing/prefix-stability-conformance.d.ts +30 -0
  78. package/dist/testing/prefix-stability-conformance.js +104 -0
  79. package/dist/testing/session-store-conformance.d.ts +3 -2
  80. package/dist/testing/session-store-conformance.js +48 -0
  81. package/dist/tools.d.ts +5 -0
  82. package/dist/tools.js +11 -3
  83. package/dist/usage-estimation.d.ts +29 -0
  84. package/dist/usage-estimation.js +79 -0
  85. package/docs/agent-events.md +68 -1
  86. package/docs/agent-session-runtime.md +1 -0
  87. package/docs/attention-compiler.md +89 -8
  88. package/docs/coding-agent-tools.md +1 -1
  89. package/docs/compaction-and-retry.md +1 -1
  90. package/docs/compaction-observational-memory.md +33 -6
  91. package/docs/durable-runs.md +42 -0
  92. package/docs/embeddings.md +5 -0
  93. package/docs/evaluations.md +5 -0
  94. package/docs/execution-timeline.md +78 -1
  95. package/docs/guardrails.md +38 -2
  96. package/docs/index.md +32 -13
  97. package/docs/input-and-prompt-assembly.md +3 -3
  98. package/docs/knowledge-sync.md +4 -0
  99. package/docs/middleware-hooks.md +38 -2
  100. package/docs/migrate-to-0.9.md +210 -0
  101. package/docs/migration.md +13 -0
  102. package/docs/multi-agent-patterns.md +25 -2
  103. package/docs/node-jsonl-session-store.md +7 -1
  104. package/docs/observability.md +7 -3
  105. package/docs/options-index.md +2 -1
  106. package/docs/policy-and-audit.md +13 -1
  107. package/docs/prefix-stability-conformance.md +93 -0
  108. package/docs/provider-caching.md +4 -4
  109. package/docs/provider-conformance.md +16 -0
  110. package/docs/provider-packages.md +20 -20
  111. package/docs/public-contracts.md +2 -2
  112. package/docs/rag.md +101 -3
  113. package/docs/release-and-install.md +39 -37
  114. package/docs/runs-and-usage.md +43 -6
  115. package/docs/scoped-agent-memory.md +262 -0
  116. package/docs/session-store-conformance.md +1 -2
  117. package/docs/session-stores.md +17 -17
  118. package/docs/supervisors.md +32 -12
  119. package/docs/tools.md +17 -0
  120. package/docs/workflows.md +5 -0
  121. package/package.json +5 -1
@@ -9,7 +9,7 @@ import { assertGuardrailsAllowed, runGuardrails } from "../../guardrails.js";
9
9
  import { identityTelemetryAttributes, ownershipFromIdentity, resolveRunIdentity } from "../../identity.js";
10
10
  import { assembleProviderInput } from "../../input.js";
11
11
  import { errorToErrorInfo, redactRunLedgerRecord } from "../../redaction.js";
12
- import { RunLimitError, RunLimitTracker, resolveRunLimits } from "../../run-limits.js";
12
+ import { describeBudgetExhaustion, RunLimitError, RunLimitTracker, resolveRunLimits } from "../../run-limits.js";
13
13
  import { createSessionEntry } from "../../session-stores.js";
14
14
  import { resolveSkillsDisclosure } from "../../skill-disclosure.js";
15
15
  import { applyRestoredSkillBodies } from "../../skill-load.js";
@@ -17,15 +17,26 @@ import { assertStructuredOutputRequestSupported, resolveRunProviderOptions } fro
17
17
  import { composeSystemPrompt, mergeSystemPromptConfig } from "../../system-prompts.js";
18
18
  import { resolveToolResultFold } from "../../tool-result-fold.js";
19
19
  import { createSearchToolsTool, createToolSearchState, resolveToolsDisclosure } from "../../tool-search.js";
20
- import { createToolRegistry, selectRunTools } from "../../tools.js";
20
+ import { clampTurnToolNames, createToolRegistry, selectRunTools } from "../../tools.js";
21
21
  import { bridgeAbort, createUsageAccumulator, inputToMessages, isDurableLoop, isSteerSoftInterrupt, mergeGuardrails, throwIfAborted, } from "../helpers.js";
22
- import { checkpointDurableTurn, cleanupRun, persistDurable, persistSucceeded, suspendDurable } from "./persist.js";
23
- import { generateWithRetry, recordProviderUsage } from "./provider-round.js";
22
+ import { checkpointDurableFold, checkpointDurableTurn, cleanupRun, persistDurable, persistSucceeded, suspendDurable } from "./persist.js";
23
+ import { generateWithRetry, recordProviderUsage, resolveDeterministicTurn } from "./provider-round.js";
24
24
  import { bindChargeToolRound, bindDispatchToolCall, replayDurableNestedAndPending, runLoopUntilSettled, suspendGatedRound, } from "./tool-round.js";
25
25
  const PROMPT_VERSION_MAX_NAME_BYTES = 256;
26
26
  const PROMPT_VERSION_HASH_PATTERN = /^sha256:[0-9a-f]{64}$/;
27
27
  /** Cap on the host stop detail that reaches the result, ledger, and timeline (plan 084 Task 2). */
28
28
  const TURN_STOP_DETAIL_MAX_BYTES = 256;
29
+ function lastAssistantText(history) {
30
+ for (let i = history.length - 1; i >= 0; i -= 1) {
31
+ const message = history[i];
32
+ if (message?.role !== "assistant")
33
+ continue;
34
+ const text = message.content.map((block) => (block.type === "text" ? block.text : "")).join("");
35
+ if (text)
36
+ return text;
37
+ }
38
+ return undefined;
39
+ }
29
40
  /**
30
41
  * `RunOptions.turnPolicy` stopped the run at a turn boundary (plan 084 Task 2). Internal control
31
42
  * signal: it unwinds any loop shape and `executeRun` turns it into a clean terminal success with
@@ -140,7 +151,13 @@ async function assembleRoundContext(params) {
140
151
  throwIfAborted(controller.signal);
141
152
  session.emit({ type: "agent_started", sessionId: session.id, runId });
142
153
  if (resumed)
143
- session.emit({ type: "agent_resumed", sessionId: session.id, runId, version: resumed.version });
154
+ session.emit({
155
+ type: "agent_resumed",
156
+ sessionId: session.id,
157
+ runId,
158
+ version: resumed.version,
159
+ ...(resumed.restore ? { restore: resumed.restore } : {}),
160
+ });
144
161
  const startRecord = {
145
162
  id: runId,
146
163
  sessionId: session.id,
@@ -176,6 +193,7 @@ async function assembleRoundContext(params) {
176
193
  const tools = searchTool ? [...activeToolList, searchTool] : activeToolList;
177
194
  const activeSkills = session.resolveRunSkills(options, tools);
178
195
  session.activeRunSkills = activeSkills;
196
+ session.tailSegments.clear();
179
197
  if (options.model && JSON.stringify(options.model) !== JSON.stringify(session.agent.config.model)) {
180
198
  await session.appendEntry(createSessionEntry({
181
199
  sessionId: session.id,
@@ -215,24 +233,41 @@ async function assembleRoundContext(params) {
215
233
  assertStructuredOutputRequestSupported(options.model ?? session.agent.config.model, providerOptions);
216
234
  const validate = options.validate ?? session.agent.config.validator;
217
235
  // 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);
236
+ // fails here rather than on the turn that happens to cross the ratio (plan 074 C12). The
237
+ // resolved run input budget rides the handle so `run_input_ratio` folds against the same cap
238
+ // the run limit enforces (plan 086 T2); `null` (disabled) leaves that axis on the input cap.
239
+ const attentionCompiler = resolveRunAttentionCompiler(session.agent.config.attentionCompiler, options.attentionCompiler, options.model ?? session.agent.config.model, limits.limits.maxInputTokens);
240
+ // Plan 086 T3: durable folding writes the fold ledger to the run checkpoint, so it needs a
241
+ // durable run (the session's durable state is set before this call). Fail at run start, before
242
+ // any provider turn, rather than folding into memory only. The fold state rides that
243
+ // checkpoint independently of `persistSessionState`.
244
+ if (attentionCompiler?.durable && !session.activeDurable) {
245
+ throw new AgentRunStateError("attentionCompiler.durable requires a durable run: set AgentConfig or RunOptions runState with a checkpoint store");
246
+ }
247
+ session.attentionDurable = attentionCompiler?.durable === true;
220
248
  // Telemetry seam (plan 074 T6): one `attention_compiled` per mutated turn, counts and the
221
249
  // measured ratio inputs only. Under-ratio turns and compiler-off runs emit nothing.
250
+ // Plan 086 T3: a turn that folded new bodies is the fold-boundary durability signal, so it is
251
+ // remembered here (the callback is synchronous) and checkpointed by the assembler below.
252
+ let foldCheckpointPending = false;
222
253
  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
- })
254
+ ? (report) => {
255
+ if (report.newFoldedBodies > 0)
256
+ foldCheckpointPending = true;
257
+ session.emit({
258
+ type: "attention_compiled",
259
+ sessionId: session.id,
260
+ runId,
261
+ used: report.used,
262
+ usedAfter: report.usedAfter,
263
+ inputCap: report.inputCap,
264
+ triggerRatio: report.triggerRatio,
265
+ droppedThinkingTurns: report.droppedThinkingTurns,
266
+ stubbedToolResults: report.stubbedToolResults,
267
+ stubbedBytes: report.stubbedBytes,
268
+ truncated: report.truncated,
269
+ });
270
+ }
236
271
  : undefined;
237
272
  const instructionInjectors = options.instructionInjectors ?? session.agent.config.instructionInjectors ?? [];
238
273
  const inputLayout = options.inputLayout ?? session.agent.config.inputLayout;
@@ -272,6 +307,8 @@ async function assembleRoundContext(params) {
272
307
  runUsage,
273
308
  loopCtx: undefined,
274
309
  };
310
+ const toolNarrowing = options.toolNarrowing ?? session.agent.config.toolNarrowing;
311
+ let narrowedForTurn;
275
312
  const loopCtx = {
276
313
  sessionId: session.id,
277
314
  runId,
@@ -285,6 +322,38 @@ async function assembleRoundContext(params) {
285
322
  restoredLoopState: resumed?.state?.loopState?.snapshot,
286
323
  assemble: async (nextInput, toolResults, turn) => {
287
324
  limits.charge("maxTurns");
325
+ const turnIndex = turn ?? 1;
326
+ let turnTools = tools;
327
+ if (toolNarrowing) {
328
+ if (typeof toolNarrowing !== "function")
329
+ throw new TypeError("toolNarrowing must be a function");
330
+ if (narrowedForTurn?.turn === turnIndex) {
331
+ turnTools = narrowedForTurn.tools;
332
+ }
333
+ else {
334
+ const assistant = lastAssistantText(session.history);
335
+ const requested = await toolNarrowing({
336
+ turn: turnIndex,
337
+ toolIds: tools.map((tool) => tool.name),
338
+ ...(assistant !== undefined ? { lastAssistantText: assistant } : {}),
339
+ });
340
+ if (!Array.isArray(requested))
341
+ throw new TypeError("toolNarrowing must return a string array");
342
+ const clamped = clampTurnToolNames(tools, requested);
343
+ if (clamped.dropped.length > 0) {
344
+ session.emit({
345
+ type: "tool_narrowing_clamped",
346
+ sessionId: session.id,
347
+ runId,
348
+ turn: turnIndex,
349
+ dropped: clamped.dropped,
350
+ });
351
+ }
352
+ turnTools = clamped.tools;
353
+ narrowedForTurn = { turn: turnIndex, tools: turnTools };
354
+ }
355
+ ctx.turnAllow = turnTools.map((tool) => tool.name);
356
+ }
288
357
  const request = await assembleProviderInput({
289
358
  model: options.model ?? session.agent.config.model,
290
359
  input: nextInput,
@@ -308,9 +377,16 @@ async function assembleRoundContext(params) {
308
377
  // Session-owned: a stub made earlier stays applied even on a later under-ratio turn, so
309
378
  // the prompt-cache prefix is not rewritten (C10). Undefined when the compiler is off.
310
379
  attentionSticky: attentionCompiler ? session.attentionStickyFor() : undefined,
380
+ // Folded bodies (plan 086 T3): a row summarized once is re-applied, never re-summarized,
381
+ // so sticky rows stay byte-identical and a resumed run reuses the persisted bodies.
382
+ attentionFold: attentionCompiler ? session.attentionFoldFor() : undefined,
383
+ // Charge-so-far for the `run_input_ratio` axis: the counter only holds completed turns,
384
+ // so the axis projects this turn's estimate onto it.
385
+ runInputTokens: limits.snapshot().inputTokens,
311
386
  onAttentionReport,
312
387
  loadedSkills: session.loadedSkills,
313
- tools,
388
+ tailSegments: session.tailSegments,
389
+ tools: turnTools,
314
390
  resourceLoader: session.agent.config.resourceLoader,
315
391
  permission: session.agent.config.permission,
316
392
  trust: session.agent.config.trust,
@@ -323,6 +399,13 @@ async function assembleRoundContext(params) {
323
399
  signal: controller.signal,
324
400
  });
325
401
  ctx.assembledTurn = true;
402
+ if (foldCheckpointPending) {
403
+ foldCheckpointPending = false;
404
+ // Fold-boundary durability (plan 086 T3): one write per turn that added folded bodies,
405
+ // after the request is assembled and before the provider sees it, so a crash during this
406
+ // turn resumes with the same ledger. No-op unless the compiler is durable.
407
+ await checkpointDurableFold(session, { runId, model, limits });
408
+ }
326
409
  return request;
327
410
  },
328
411
  chargeToolRound: bindChargeToolRound(ctx),
@@ -345,10 +428,15 @@ async function assembleRoundContext(params) {
345
428
  if (session.activeDurable?.options.checkpointPolicy === "every-turn") {
346
429
  await checkpointDurableTurn(session, { runId, model, limits });
347
430
  }
431
+ // Deterministic no-model turn (plan 096): host middleware answers at the provider boundary,
432
+ // before any provider-round work. No answer → provider path unchanged.
433
+ const deterministic = await resolveDeterministicTurn(session, request, runId, session.activeLoopTurn, controller.signal, ctx.toolResults);
434
+ if (deterministic)
435
+ return deterministic;
348
436
  const policyResult = await session.applyProviderRequestPolicies(request, runId, options, metadata, controller.signal);
349
437
  const middlewareRequest = (await session.agent.config.middleware?.run("provider_request", policyResult.request)) ?? policyResult.request;
350
438
  try {
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);
439
+ return await generateWithRetry(session, session.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, session.activeLoopTurn, (turnUsage, turn, attempt) => recordProviderUsage(ctx, turnUsage, turn, attempt, middlewareRequest), ctx.toolResults);
352
440
  }
353
441
  catch (error) {
354
442
  if (isSteerSoftInterrupt(error)) {
@@ -431,8 +519,11 @@ export async function executeRun(session, input, options, runId, resumed) {
431
519
  if (session.activeIdentity && !session.activeOwnership)
432
520
  session.activeOwnership = ownershipFromIdentity(session.activeIdentity);
433
521
  session.activeIdempotencyKey = options.idempotencyKey ?? session.agent.config.idempotencyKey;
434
- session.activeGuardrails = mergeGuardrails(session.agent.config.guardrails, options.guardrails);
522
+ session.activeGuardrails = mergeGuardrails(mergeGuardrails(session.agent.config.guardrails, session.packGuardrails), options.guardrails);
435
523
  session.activeDurable = resumed ?? (durableOptions ? { options: durableOptions, version: 0 } : undefined);
524
+ // Plan 086 T3: reset here, so a suspension before the compiler is resolved (input guardrail)
525
+ // cannot inherit the previous run's durable-folding flag. `assembleRoundContext` sets it true.
526
+ session.attentionDurable = false;
436
527
  session.activeGatedRound = undefined;
437
528
  if (resumed)
438
529
  session.invalidateSnapshot();
@@ -462,6 +553,7 @@ export async function executeRun(session, input, options, runId, resumed) {
462
553
  deadlineAt: resumed?.state?.deadlineAt,
463
554
  });
464
555
  session.activeLimits = limits;
556
+ session.activeRecentToolCalls = [];
465
557
  const hasFiniteTokenCap = (value) => typeof value === "number" && Number.isFinite(value);
466
558
  session.activeLimitOutputBuffer = [session.agent.config.limits, requestedLimits].some((value) => hasFiniteTokenCap(value?.maxOutputTokens) || hasFiniteTokenCap(value?.maxTotalTokens) || value?.maxCost !== undefined);
467
559
  try {
@@ -513,8 +605,18 @@ export async function executeRun(session, input, options, runId, resumed) {
513
605
  return session.buildRunResult({ runId, status: "suspended", runState: error.state, interruption: error.interruption });
514
606
  }
515
607
  runError = errorToErrorInfo(error);
516
- session.emit({ type: "error", sessionId: session.id, runId, error: runError });
517
608
  const breach = error instanceof RunLimitError ? error.breach : limits.breach;
609
+ // Terminal attribution before the terminal `error`/finish records, so a subscriber that stops
610
+ // at the first terminal event still sees why the run died (plan 087 T2).
611
+ if (breach) {
612
+ session.emit({
613
+ type: "budget_exhausted",
614
+ sessionId: session.id,
615
+ runId,
616
+ ...describeBudgetExhaustion(limits, breach, session.activeRecentToolCalls ?? []),
617
+ });
618
+ }
619
+ session.emit({ type: "error", sessionId: session.id, runId, error: runError });
518
620
  runStatus = breach ? "failed" : controller.signal.aborted ? "aborted" : "failed";
519
621
  const runState = session.activeDurable?.state
520
622
  ? await persistDurable(session, {
@@ -28,6 +28,17 @@ export declare function checkpointDurableTurn(session: SessionHost, input: {
28
28
  readonly model: ModelConfig;
29
29
  readonly limits: RunLimitTracker;
30
30
  }): Promise<void>;
31
+ /**
32
+ * Fold-boundary checkpoint (plan 086 T3). Called once per turn that added folded bodies — never
33
+ * per turn — when the resolved compiler is durable, so a crash after a fold resumes with the
34
+ * ledger and frontier already on disk. Independent of `checkpointPolicy`: the fold is the
35
+ * durability point that matters for a long single run, not the turn boundary.
36
+ */
37
+ export declare function checkpointDurableFold(session: SessionHost, input: {
38
+ readonly runId: string;
39
+ readonly model: ModelConfig;
40
+ readonly limits: RunLimitTracker;
41
+ }): Promise<void>;
31
42
  export declare function persistSucceeded(ctx: RoundContext, loopUsage: Usage | undefined): Promise<AgentRunResult>;
32
43
  export declare function cleanupRun(input: {
33
44
  session: SessionHost;
@@ -1,5 +1,5 @@
1
1
  /** Finalize/persist phase of runInternal (plan 059). */
2
- import { boundedLoopSnapshot, initialAgentRunState, publicState, saveAgentRunState } from "../../agent-run-state.js";
2
+ import { boundedLoopSnapshot, initialAgentRunState, publicState, resolveCheckpointMetadata, saveAgentRunState, } from "../../agent-run-state.js";
3
3
  import { AgentRunStateError } from "../../contracts.js";
4
4
  import { redactRunLedgerRecord } from "../../redaction.js";
5
5
  import { isFlushableRunLedger } from "../../run-ledger.js";
@@ -10,13 +10,16 @@ export async function persistDurable(session, state) {
10
10
  if (!durable)
11
11
  throw new AgentRunStateError("Durable run state is not configured");
12
12
  const withGrant = session.activeToolNames !== undefined ? { ...state, toolNames: session.activeToolNames } : state;
13
- const attentionSticky = session.serializedAttentionSticky();
14
- const persisted = durable.options.persistSessionState
15
- ? {
16
- ...withGrant,
17
- sessionState: {
13
+ const persistSessionState = durable.options.persistSessionState === true;
14
+ // Plan 086 T3: durable folding owns its two keys. A run that opted into `durable` writes the
15
+ // fold ledger and its frontier even when the broader session-state bag stays off; a run that
16
+ // did not keeps exactly today's bytes, where the frontier rides `persistSessionState`.
17
+ const attentionSticky = persistSessionState || session.attentionDurable ? session.serializedAttentionSticky() : undefined;
18
+ const attentionFold = session.attentionDurable ? session.serializedAttentionFold() : undefined;
19
+ const sessionState = {
20
+ ...(persistSessionState
21
+ ? {
18
22
  loadedSkillNames: session.loadedSkills.list(),
19
- ...(attentionSticky ? { attentionSticky } : {}),
20
23
  ...(session.activatedTools.list().length ? { activatedToolNames: session.activatedTools.list() } : {}),
21
24
  ...(durable.options.includeSkillBodies
22
25
  ? {
@@ -25,9 +28,13 @@ export async function persistDurable(session, state) {
25
28
  : undefined),
26
29
  }
27
30
  : {}),
28
- },
29
- }
30
- : withGrant;
31
+ }
32
+ : {}),
33
+ ...(attentionSticky ? { attentionSticky } : {}),
34
+ ...(attentionFold ? { attentionFold } : {}),
35
+ };
36
+ const persisted = Object.keys(sessionState).length > 0 ? { ...withGrant, sessionState } : withGrant;
37
+ const metadata = resolveCheckpointMetadata(durable.options.checkpointMetadata) ?? durable.checkpointMetadata;
31
38
  const saved = await saveAgentRunState({
32
39
  checkpoints: durable.options.checkpoints,
33
40
  state: persisted,
@@ -36,6 +43,7 @@ export async function persistDurable(session, state) {
36
43
  fencingToken: durable.options.fencingToken,
37
44
  redactor: session.activeRedactor,
38
45
  maxStateBytes: durable.options.maxStateBytes,
46
+ ...(metadata ? { metadata } : {}),
39
47
  });
40
48
  durable.state = saved.state;
41
49
  durable.version = saved.record.version;
@@ -86,8 +94,25 @@ export async function suspendDurable(session, input) {
86
94
  * into a resumed run, and loop-local state rides along exactly as it does at suspension.
87
95
  */
88
96
  export async function checkpointDurableTurn(session, input) {
97
+ if (session.activeDurable?.options.checkpointPolicy !== "every-turn")
98
+ return;
99
+ await writeRunningCheckpoint(session, input);
100
+ }
101
+ /**
102
+ * Fold-boundary checkpoint (plan 086 T3). Called once per turn that added folded bodies — never
103
+ * per turn — when the resolved compiler is durable, so a crash after a fold resumes with the
104
+ * ledger and frontier already on disk. Independent of `checkpointPolicy`: the fold is the
105
+ * durability point that matters for a long single run, not the turn boundary.
106
+ */
107
+ export async function checkpointDurableFold(session, input) {
108
+ if (!session.attentionDurable)
109
+ return;
110
+ await writeRunningCheckpoint(session, input);
111
+ }
112
+ /** Shared running-checkpoint write for the turn-boundary and fold-boundary triggers. */
113
+ async function writeRunningCheckpoint(session, input) {
89
114
  const durable = session.activeDurable;
90
- if (durable?.options.checkpointPolicy !== "every-turn")
115
+ if (!durable)
91
116
  return;
92
117
  const loop = session.activeLoop;
93
118
  const loopState = loop?.snapshot ? boundedLoopSnapshot(loop.name, loop.revision ?? "1", loop.snapshot()) : undefined;
@@ -216,6 +241,7 @@ export async function cleanupRun(input) {
216
241
  session.activeLimits?.dispose();
217
242
  session.activeToolNames = undefined;
218
243
  session.activeLimits = undefined;
244
+ session.activeRecentToolCalls = undefined;
219
245
  session.activeLimitOutputBuffer = false;
220
246
  session.activeRedactor = undefined;
221
247
  session.activeProvider = undefined;
@@ -1,6 +1,16 @@
1
1
  /** Provider-round phase of runInternal (plan 059). */
2
- import type { ProviderRequest, ProviderTurnResult, RunOptions, ToolResult, Usage } from "../../contracts.js";
2
+ import type { ModelConfig, ProviderRequest, ProviderTurnResult, RunOptions, ToolResult, Usage } from "../../contracts.js";
3
3
  import type { RoundContext, SessionHost } from "./types.js";
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>, 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>;
4
+ /** Resolve the per-request input cap for turn-budget metadata (plan 087 T1). A model without a
5
+ * derivable cap (or a bad attention setting on an unrelated run) omits the field instead of
6
+ * failing an emitting turn; the attention compiler, when enabled, is the cap authority. */
7
+ export declare function resolveTurnInputCap(session: SessionHost, model: ModelConfig): number | undefined;
8
+ export declare function recordProviderUsage(ctx: RoundContext, turnUsage: Usage | undefined, turn: number, attempt: number, request?: ProviderRequest): Promise<Usage | undefined>;
9
+ /**
10
+ * Plan 096: host middleware may answer the turn deterministically at the `beforeProviderTurn` seam —
11
+ * no provider request, no usage, mandatory provenance. `undefined` sends the turn to the provider
12
+ * unchanged; a malformed answer fails the run closed instead of falling through to the provider.
13
+ */
14
+ export declare function resolveDeterministicTurn(session: SessionHost, request: ProviderRequest, runId: string, turn: number, signal: AbortSignal, toolResults?: readonly ToolResult[]): Promise<ProviderTurnResult | undefined>;
15
+ 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<Usage | undefined>, toolResults?: readonly ToolResult[]): Promise<ProviderTurnResult>;
16
+ 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<Usage | undefined>, toolResults?: readonly ToolResult[]): Promise<ProviderTurnResult>;