@arnilo/prism 0.7.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 (182) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +12 -11
  3. package/dist/agent-approval.d.ts +15 -2
  4. package/dist/agent-approval.js +5 -1
  5. package/dist/agent-event-source.d.ts +9 -1
  6. package/dist/agent-event-source.js +10 -3
  7. package/dist/agent-loops.js +7 -4
  8. package/dist/agent-run-lifecycle.d.ts +15 -1
  9. package/dist/agent-run-lifecycle.js +91 -10
  10. package/dist/agent-run-state.d.ts +34 -2
  11. package/dist/agent-run-state.js +68 -6
  12. package/dist/agent-session/helpers.js +20 -1
  13. package/dist/agent-session/session/assemble.js +250 -27
  14. package/dist/agent-session/session/persist.d.ts +27 -0
  15. package/dist/agent-session/session/persist.js +94 -12
  16. package/dist/agent-session/session/provider-round.d.ts +14 -4
  17. package/dist/agent-session/session/provider-round.js +197 -25
  18. package/dist/agent-session/session/tool-round.js +24 -2
  19. package/dist/agent-session/session/types.d.ts +36 -2
  20. package/dist/agent-session/session.d.ts +40 -4
  21. package/dist/agent-session/session.js +78 -5
  22. package/dist/attention-compiler.d.ts +51 -2
  23. package/dist/attention-compiler.js +282 -21
  24. package/dist/cache-helpers.d.ts +4 -2
  25. package/dist/cache-helpers.js +8 -6
  26. package/dist/checkpoint-restore.d.ts +45 -0
  27. package/dist/checkpoint-restore.js +54 -0
  28. package/dist/checkpoints.js +7 -11
  29. package/dist/context-budget.d.ts +2 -1
  30. package/dist/context-budget.js +24 -2
  31. package/dist/contracts-core/agent.d.ts +30 -0
  32. package/dist/contracts-core/attention.d.ts +95 -0
  33. package/dist/contracts-core/content.d.ts +15 -0
  34. package/dist/contracts-core/guardrail-packs.d.ts +41 -0
  35. package/dist/contracts-core/guardrail-packs.js +2 -0
  36. package/dist/contracts-core/loop.d.ts +42 -0
  37. package/dist/contracts-core/provider.d.ts +25 -0
  38. package/dist/contracts-core/run-limits.d.ts +21 -0
  39. package/dist/contracts-core/session.d.ts +23 -5
  40. package/dist/contracts-core/session.js +21 -2
  41. package/dist/contracts-core/usage.d.ts +40 -0
  42. package/dist/contracts-core/usage.js +8 -0
  43. package/dist/contracts-core.d.ts +2 -0
  44. package/dist/contracts-core.js +2 -0
  45. package/dist/contracts-protocol.d.ts +90 -4
  46. package/dist/contracts-run-state.d.ts +82 -6
  47. package/dist/evidence-grounding.d.ts +29 -0
  48. package/dist/evidence-grounding.js +162 -0
  49. package/dist/guardrail-packs/coding-standard.d.ts +3 -0
  50. package/dist/guardrail-packs/coding-standard.js +63 -0
  51. package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
  52. package/dist/guardrail-packs/destructive-commands.js +46 -0
  53. package/dist/guardrail-packs/errors.d.ts +7 -0
  54. package/dist/guardrail-packs/errors.js +9 -0
  55. package/dist/guardrail-packs/index.d.ts +4 -0
  56. package/dist/guardrail-packs/index.js +15 -0
  57. package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
  58. package/dist/guardrail-packs/secrets-hygiene.js +23 -0
  59. package/dist/guardrail-packs/types.d.ts +16 -0
  60. package/dist/guardrail-packs/types.js +2 -0
  61. package/dist/guardrail-packs/validation-respect.d.ts +3 -0
  62. package/dist/guardrail-packs/validation-respect.js +53 -0
  63. package/dist/guardrails.d.ts +20 -1
  64. package/dist/guardrails.js +268 -0
  65. package/dist/host-composition.d.ts +13 -0
  66. package/dist/host-composition.js +33 -2
  67. package/dist/index.d.ts +19 -10
  68. package/dist/index.js +11 -6
  69. package/dist/input.d.ts +8 -1
  70. package/dist/input.js +68 -6
  71. package/dist/middleware.d.ts +37 -2
  72. package/dist/middleware.js +41 -0
  73. package/dist/node/session-store-jsonl.js +18 -3
  74. package/dist/observability.js +6 -0
  75. package/dist/provider-events.d.ts +11 -3
  76. package/dist/provider-events.js +62 -4
  77. package/dist/providers/openai-compatible.js +6 -3
  78. package/dist/providers/transport.d.ts +3 -1
  79. package/dist/providers/transport.js +36 -0
  80. package/dist/redaction.js +18 -2
  81. package/dist/run-bundle.d.ts +89 -0
  82. package/dist/run-bundle.js +150 -0
  83. package/dist/run-limits.d.ts +11 -1
  84. package/dist/run-limits.js +46 -0
  85. package/dist/session-stores.d.ts +12 -1
  86. package/dist/session-stores.js +21 -4
  87. package/dist/testing/agent-event-source-conformance.js +41 -2
  88. package/dist/testing/prefix-stability-conformance.d.ts +30 -0
  89. package/dist/testing/prefix-stability-conformance.js +104 -0
  90. package/dist/testing/session-store-conformance.d.ts +3 -2
  91. package/dist/testing/session-store-conformance.js +48 -0
  92. package/dist/testing/state-concurrency-conformance.js +5 -12
  93. package/dist/tools.d.ts +5 -0
  94. package/dist/tools.js +11 -3
  95. package/dist/usage-estimation.d.ts +29 -0
  96. package/dist/usage-estimation.js +79 -0
  97. package/docs/ag-ui.md +5 -0
  98. package/docs/agent-events.md +68 -1
  99. package/docs/agent-loops.md +33 -0
  100. package/docs/agent-session-runtime.md +5 -3
  101. package/docs/attention-compiler.md +89 -8
  102. package/docs/coding-agent-tools.md +1 -1
  103. package/docs/coding-security.md +1 -0
  104. package/docs/coding-tools.md +0 -1
  105. package/docs/compaction-and-retry.md +1 -1
  106. package/docs/compaction-observational-memory.md +34 -7
  107. package/docs/connected-apps.md +116 -0
  108. package/docs/context-and-skills.md +13 -0
  109. package/docs/core.md +1 -1
  110. package/docs/diagrams.md +6 -6
  111. package/docs/document-reader.md +9 -9
  112. package/docs/documents.md +32 -11
  113. package/docs/durable-runs.md +129 -0
  114. package/docs/embeddings.md +5 -0
  115. package/docs/enterprise-postgres-state.md +4 -0
  116. package/docs/evaluations.md +5 -0
  117. package/docs/execution-timeline.md +84 -1
  118. package/docs/guardrails.md +71 -2
  119. package/docs/history/079-messaging-primitive-review.md +391 -0
  120. package/docs/history/080-messaging-followon-primitive-review.md +234 -0
  121. package/docs/history/081-connected-apps-primitive-review.md +74 -0
  122. package/docs/history/083-prism-work-primitive-review.md +84 -0
  123. package/docs/history/084-primitive-review.md +96 -0
  124. package/docs/history/085-honesty-and-cut-primitive-review.md +91 -0
  125. package/docs/history/README.md +5 -0
  126. package/docs/history/release-handoffs.md +38 -0
  127. package/docs/host-compositions.md +8 -6
  128. package/docs/host-security.md +2 -2
  129. package/docs/index.md +66 -29
  130. package/docs/input-and-prompt-assembly.md +3 -3
  131. package/docs/knowledge-sync.md +4 -0
  132. package/docs/live-testing.md +5 -3
  133. package/docs/mcp-tools.md +1 -0
  134. package/docs/messaging-channel-operations.md +166 -0
  135. package/docs/messaging-channels.md +150 -0
  136. package/docs/middleware-hooks.md +38 -2
  137. package/docs/migrate-to-0.8.md +124 -0
  138. package/docs/migrate-to-0.9.md +210 -0
  139. package/docs/migration.md +43 -0
  140. package/docs/model-registry.md +12 -2
  141. package/docs/multi-agent-patterns.md +25 -2
  142. package/docs/node-jsonl-session-store.md +7 -1
  143. package/docs/observability.md +7 -3
  144. package/docs/openapi-tools.md +1 -1
  145. package/docs/operations.md +1 -3
  146. package/docs/options-index.md +36 -3
  147. package/docs/peer-dependencies.md +6 -6
  148. package/docs/policy-and-audit.md +13 -1
  149. package/docs/postgres-persistence.md +1 -1
  150. package/docs/prefix-stability-conformance.md +93 -0
  151. package/docs/provider-caching.md +4 -4
  152. package/docs/provider-conformance.md +16 -0
  153. package/docs/provider-layer.md +2 -2
  154. package/docs/provider-packages.md +20 -20
  155. package/docs/providers/neuralwatt.md +5 -1
  156. package/docs/public-contracts.md +2 -2
  157. package/docs/rag.md +102 -4
  158. package/docs/release-and-install.md +55 -47
  159. package/docs/run-bundle.md +92 -0
  160. package/docs/runs-and-usage.md +57 -6
  161. package/docs/scoped-agent-memory.md +262 -0
  162. package/docs/server.md +2 -0
  163. package/docs/session-store-conformance.md +1 -2
  164. package/docs/session-stores.md +17 -17
  165. package/docs/sheets.md +9 -9
  166. package/docs/signal-channel.md +112 -0
  167. package/docs/speech.md +5 -1
  168. package/docs/sqlite-persistence.md +1 -1
  169. package/docs/supervisors.md +32 -12
  170. package/docs/telegram-channel.md +157 -0
  171. package/docs/testing.md +2 -2
  172. package/docs/tools.md +17 -0
  173. package/docs/wiki.md +1 -1
  174. package/docs/work-artifacts-and-review.md +1 -1
  175. package/docs/work-connectors.md +9 -9
  176. package/docs/work-sandbox.md +115 -0
  177. package/docs/work-tools.md +38 -16
  178. package/docs/workflows.md +5 -0
  179. package/package.json +9 -3
  180. package/templates/business-worker/manifest.json +2 -1
  181. package/templates/business-worker/src/agent.ts.tmpl +1 -1
  182. package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
@@ -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;
@@ -77,8 +85,67 @@ export async function suspendDurable(session, input) {
77
85
  counters: input.limits.snapshot(),
78
86
  });
79
87
  }
88
+ /**
89
+ * Turn-boundary crash-recovery checkpoint (plan 084 Task 1). Called before each provider
90
+ * request when `checkpointPolicy: "every-turn"`; a no-op otherwise, so default-policy runs keep
91
+ * the 0.8.x checkpoint shape and write count unchanged. Pending-decision markers are dropped:
92
+ * at a turn boundary every gated call has been resolved or the run already suspended, and a
93
+ * stale marker must never replay. The recorded `checkpointPolicy` makes the cadence survive
94
+ * into a resumed run, and loop-local state rides along exactly as it does at suspension.
95
+ */
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) {
114
+ const durable = session.activeDurable;
115
+ if (!durable)
116
+ return;
117
+ const loop = session.activeLoop;
118
+ const loopState = loop?.snapshot ? boundedLoopSnapshot(loop.name, loop.revision ?? "1", loop.snapshot()) : undefined;
119
+ const state = durable.state ??
120
+ initialAgentRunState({
121
+ agent: session.agent,
122
+ options: durable.options,
123
+ runId: input.runId,
124
+ sessionId: session.id,
125
+ leafId: session.currentLeafId,
126
+ model: input.model,
127
+ counters: input.limits.snapshot(),
128
+ deadlineAt: input.limits.deadlineAt,
129
+ status: "running",
130
+ interruptBeforeTool: durable.options.interruptBeforeTool,
131
+ });
132
+ await persistDurable(session, {
133
+ ...state,
134
+ leafId: session.currentLeafId,
135
+ status: "running",
136
+ interruption: undefined,
137
+ // The input messages are already in the session store by the time a turn boundary is
138
+ // reached; keeping them would re-append them on a later resume.
139
+ input: undefined,
140
+ pending: undefined,
141
+ pendingCalls: undefined,
142
+ ...(loopState ? { loopState } : {}),
143
+ counters: input.limits.snapshot(),
144
+ });
145
+ }
80
146
  export async function persistSucceeded(ctx, loopUsage) {
81
147
  const { session, runId, runUsage } = ctx;
148
+ const stop = ctx.runStop;
82
149
  const usage = runUsage.value() ?? loopUsage;
83
150
  if (usage && session.activeLedger) {
84
151
  const usageRecord = {
@@ -97,12 +164,15 @@ export async function persistSucceeded(ctx, loopUsage) {
97
164
  ? await persistDurable(session, {
98
165
  ...session.activeDurable.state,
99
166
  status: "succeeded",
167
+ // Plan 084 Task 2: a host-policy stop is terminal for the run but leaves the frontier
168
+ // intact — the loop state is kept and the state is marked continuable.
169
+ ...(stop ? { stopReason: "host_policy", leafId: session.currentLeafId } : {}),
100
170
  pending: undefined,
101
171
  pendingCalls: undefined,
102
172
  nestedRuns: undefined,
103
173
  stickyDecisions: undefined,
104
174
  interruption: undefined,
105
- loopState: undefined,
175
+ ...(stop ? {} : { loopState: undefined }),
106
176
  })
107
177
  : undefined;
108
178
  session.emit({
@@ -111,8 +181,17 @@ export async function persistSucceeded(ctx, loopUsage) {
111
181
  runId,
112
182
  usage,
113
183
  ...(ctx.loopCtx.finishReason ? { finishReason: ctx.loopCtx.finishReason } : {}),
184
+ ...(stop?.detail ? { stopDetail: stop.detail } : {}),
185
+ });
186
+ const stopReason = ctx.runStop?.reason ?? ctx.loopCtx.finishReason;
187
+ return session.buildRunResult({
188
+ runId,
189
+ status: "succeeded",
190
+ usage,
191
+ runState,
192
+ ...(stopReason ? { stopReason } : {}),
193
+ ...(stop?.detail ? { stopDetail: stop.detail } : {}),
114
194
  });
115
- return session.buildRunResult({ runId, status: "succeeded", usage, runState });
116
195
  }
117
196
  export async function cleanupRun(input) {
118
197
  const { session, controller, cleanupSignal, runId, model, startedAt, runStatus, runError } = input;
@@ -138,6 +217,8 @@ export async function cleanupRun(input) {
138
217
  status: runStatus,
139
218
  startedAt,
140
219
  finishedAt: new Date().toISOString(),
220
+ ...(input.runStatus === "succeeded" && input.stopReason ? { stopReason: input.stopReason } : {}),
221
+ ...(input.runStatus === "succeeded" && input.stopDetail ? { stopDetail: input.stopDetail } : {}),
141
222
  abortReason: controller.signal.aborted ? String(controller.signal.reason) : undefined,
142
223
  error: runError,
143
224
  ...(session.activePromptVersion ? { promptVersion: session.activePromptVersion } : {}),
@@ -160,6 +241,7 @@ export async function cleanupRun(input) {
160
241
  session.activeLimits?.dispose();
161
242
  session.activeToolNames = undefined;
162
243
  session.activeLimits = undefined;
244
+ session.activeRecentToolCalls = undefined;
163
245
  session.activeLimitOutputBuffer = false;
164
246
  session.activeRedactor = undefined;
165
247
  session.activeProvider = undefined;
@@ -1,6 +1,16 @@
1
1
  /** Provider-round phase of runInternal (plan 059). */
2
- import type { ProviderRequest, ProviderTurnResult, RunOptions, 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>): 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>;
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>;
@@ -1,9 +1,14 @@
1
1
  /** Provider-round phase of runInternal (plan 059). */
2
+ import { resolveInputCap } from "../../attention-compiler.js";
3
+ import { cacheUsageReport } from "../../cache-helpers.js";
4
+ import { estimateMessageTokens } from "../../context-budget.js";
2
5
  import { assertGuardrailsAllowed, GuardrailError, runGuardrails } from "../../guardrails.js";
6
+ import { validateDeterministicTurnAnswer } from "../../middleware.js";
3
7
  import { createProviderTurnMetadata, readProviderHttpStatus } from "../../observability.js";
4
- import { providerToolCallDeltaContent } from "../../provider-events.js";
8
+ import { providerError, providerToolCallDeltaContent } from "../../provider-events.js";
5
9
  import { errorToErrorInfo, redactRunLedgerRecord, redactSecrets } from "../../redaction.js";
6
10
  import { createDefaultRetryPolicy, waitForRetry } from "../../retry.js";
11
+ import { estimateTextTokensForFamily } from "../../usage-estimation.js";
7
12
  import { bridgeAbort, errorFromInfo, isSteerSoftInterrupt, jsonBytes, mergeRetry, ProviderTurnFailure, providerContent, randomId, reconstructMissingToolCalls, SteerSoftInterrupt, throwIfAborted, } from "../helpers.js";
8
13
  function pushCoalescedContent(content, block) {
9
14
  const last = content.at(-1);
@@ -21,6 +26,49 @@ function pushCoalescedContent(content, block) {
21
26
  }
22
27
  content.push(block);
23
28
  }
29
+ /** Resolve the per-request input cap for turn-budget metadata (plan 087 T1). A model without a
30
+ * derivable cap (or a bad attention setting on an unrelated run) omits the field instead of
31
+ * failing an emitting turn; the attention compiler, when enabled, is the cap authority. */
32
+ export function resolveTurnInputCap(session, model) {
33
+ const setting = session.agent.config.attentionCompiler;
34
+ const options = typeof setting === "object" && setting !== null ? setting : undefined;
35
+ try {
36
+ return resolveInputCap(options ? { maxInputTokens: options.maxInputTokens, reserveTokens: options.reserveTokens } : {}, model);
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ /** Effective budget snapshot at turn end (plan 087 T1): O(1) from the run limit tracker. */
43
+ function turnBudgets(session, model, usage) {
44
+ const tracker = session.activeLimits;
45
+ if (!tracker)
46
+ return undefined;
47
+ const snapshot = tracker.snapshot();
48
+ const inputCap = resolveTurnInputCap(session, model);
49
+ const runInputBudget = tracker.limits.maxInputTokens;
50
+ return {
51
+ ...(usage?.inputTokens === undefined ? {} : { inputTokens: usage.inputTokens }),
52
+ ...(inputCap === undefined ? {} : { inputCap }),
53
+ ...(runInputBudget === null ? {} : { runInputBudget }),
54
+ runInputUsed: snapshot.inputTokens,
55
+ turns: snapshot.turns,
56
+ maxTurns: tracker.limits.maxTurns,
57
+ };
58
+ }
59
+ function cacheMetadata(usage) {
60
+ const cache = cacheUsageReport(usage);
61
+ return cache === undefined ? {} : { cache };
62
+ }
63
+ /** Native reason wins, except a generic `end_turn` on a turn that produced tool calls: protocols
64
+ * with one generic completion value (Google `STOP`) are tool-call turns by content (plan 087 T1). */
65
+ function normalizeTurnStopReason(native, calls) {
66
+ if (native === undefined)
67
+ return calls.length > 0 ? "tool_calls" : "end_turn";
68
+ if (native === "end_turn" && calls.length > 0)
69
+ return "tool_calls";
70
+ return native;
71
+ }
24
72
  /**
25
73
  * Plan 062: price usage through the host's {@link CostCatalog} when the provider
26
74
  * did not report a cost itself. Stale/unknown quotes, catalog failures, or
@@ -46,17 +94,22 @@ async function withCatalogCost(catalog, model, usage, signal) {
46
94
  return usage; // catalog failure degrades to usage-only
47
95
  }
48
96
  }
49
- export async function recordProviderUsage(ctx, turnUsage, turn, attempt) {
97
+ export async function recordProviderUsage(ctx, turnUsage, turn, attempt, request) {
50
98
  const { session, limits, runUsage, runId } = ctx;
51
- const usage = turnUsage
52
- ? await withCatalogCost(session.agent.config.costCatalog, ctx.model, turnUsage, ctx.controller.signal)
53
- : undefined;
54
- limits.recordUsage(usage);
55
- if (!usage)
56
- return;
57
- runUsage.add(usage);
99
+ const usage = turnUsage ?? estimateTurnUsage(session, ctx.model, request);
100
+ // An estimate is never priced: a catalog quote on estimated tokens would invent billing.
101
+ const effective = usage && usage.estimated !== true
102
+ ? await withCatalogCost(session.agent.config.costCatalog, ctx.model, usage, ctx.controller.signal)
103
+ : usage;
104
+ limits.recordUsage(effective);
105
+ if (!effective)
106
+ return undefined;
107
+ if (effective.inputTokens !== undefined) {
108
+ session.activeInputMeter = { tokens: effective.inputTokens, source: effective.estimated === true ? "estimated" : "reported" };
109
+ }
110
+ runUsage.add(effective);
58
111
  if (!session.activeLedger)
59
- return;
112
+ return effective;
60
113
  const usageRecord = {
61
114
  id: randomId("usage"),
62
115
  sessionId: session.id,
@@ -64,19 +117,111 @@ export async function recordProviderUsage(ctx, turnUsage, turn, attempt) {
64
117
  scope: "provider_turn",
65
118
  turn,
66
119
  attempt,
67
- usage,
120
+ usage: effective,
68
121
  recordedAt: new Date().toISOString(),
69
122
  ...session.activeOwnership,
70
123
  };
71
124
  await session.activeLedger.appendUsage(redactRunLedgerRecord(usageRecord, session.activeRedactor));
125
+ return effective;
126
+ }
127
+ /**
128
+ * Plan 091 T2 missing-usage fallback: when the provider reported nothing and the
129
+ * agent did not turn estimation off, label an estimate of the turn's own request
130
+ * (messages + tool declarations + context blocks). Returns `undefined` when
131
+ * estimation is off or the request is unavailable — absent stays absent.
132
+ */
133
+ function estimateTurnUsage(session, model, request) {
134
+ if (!request || session.agent.config.usageEstimation === "off")
135
+ return undefined;
136
+ const estimate = estimateMessageTokens(request.messages, model.model);
137
+ const extras = request.tools?.length || request.context?.length ? JSON.stringify({ tools: request.tools, context: request.context }) : undefined;
138
+ return {
139
+ inputTokens: estimate.tokens + (extras === undefined ? 0 : estimateTextTokensForFamily(extras, model.model)),
140
+ estimated: true,
141
+ confidence: estimate.confidence,
142
+ };
143
+ }
144
+ /** Latest user-role text in the assembled request; steered messages included. */
145
+ function lastUserText(messages) {
146
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
147
+ const message = messages[i];
148
+ if (message?.role !== "user")
149
+ continue;
150
+ return message.content.map((block) => (block.type === "text" ? block.text : "")).join("");
151
+ }
152
+ return "";
153
+ }
154
+ /**
155
+ * Plan 096: host middleware may answer the turn deterministically at the `beforeProviderTurn` seam —
156
+ * no provider request, no usage, mandatory provenance. `undefined` sends the turn to the provider
157
+ * unchanged; a malformed answer fails the run closed instead of falling through to the provider.
158
+ */
159
+ export async function resolveDeterministicTurn(session, request, runId, turn, signal, toolResults = []) {
160
+ const middleware = session.agent.config.middleware;
161
+ if (!middleware)
162
+ return undefined;
163
+ const payload = await middleware.run("beforeProviderTurn", {
164
+ sessionId: session.id,
165
+ runId,
166
+ turn,
167
+ userText: lastUserText(request.messages),
168
+ });
169
+ const answer = payload?.answer;
170
+ if (answer === undefined)
171
+ return undefined;
172
+ const validated = validateDeterministicTurnAnswer(answer);
173
+ throwIfAborted(signal);
174
+ const messageId = randomId("msg");
175
+ // Same response-byte axis as provider output: a host answer must not bypass a run ceiling.
176
+ session.activeLimits?.charge("maxResponseBytes", jsonBytes(validated.content));
177
+ if (session.activeGuardrails?.output?.length) {
178
+ assertGuardrailsAllowed(await runGuardrails({
179
+ stage: "output",
180
+ guardrails: session.activeGuardrails,
181
+ value: { content: validated.content, calls: [], messageId, started: true, usage: undefined },
182
+ context: {
183
+ sessionId: session.id,
184
+ runId,
185
+ metadata: session.activeMetadata ?? {},
186
+ signal,
187
+ toolResults,
188
+ },
189
+ redactor: session.activeRedactor,
190
+ emit: (event) => session.emit(event),
191
+ }));
192
+ }
193
+ session.emit({
194
+ type: "deterministic_turn",
195
+ sessionId: session.id,
196
+ runId,
197
+ turn,
198
+ middleware: validated.provenance.middleware,
199
+ });
200
+ session.emit({
201
+ type: "message_started",
202
+ sessionId: session.id,
203
+ runId,
204
+ message: { id: messageId, role: "assistant", content: [] },
205
+ });
206
+ for (const block of validated.content)
207
+ session.emit({ type: "message_delta", sessionId: session.id, runId, content: block });
208
+ // Provenance rides the message into the store (plan 096 Task 2): the transcript alone proves no model ran.
209
+ return {
210
+ content: validated.content,
211
+ calls: [],
212
+ messageId,
213
+ started: true,
214
+ usage: undefined,
215
+ metadata: { deterministic: validated.provenance },
216
+ };
72
217
  }
73
- export async function generateWithRetry(session, request, runId, options, signal, requestSecrets = [], turn = 1, recordUsage) {
218
+ export async function generateWithRetry(session, request, runId, options, signal, requestSecrets = [], turn = 1, recordUsage, toolResults = []) {
74
219
  const retry = mergeRetry(session.agent.config.retry, options.retry);
75
220
  const secrets = [...requestSecrets, ...(retry?.secrets ?? [])];
76
221
  const policy = retry?.policy ?? (retry ? createDefaultRetryPolicy(retry) : undefined);
77
222
  for (let attempt = 1;; attempt += 1) {
78
223
  try {
79
- return await generateProviderTurn(session, request, runId, signal, secrets, turn, attempt, recordUsage);
224
+ return await generateProviderTurn(session, request, runId, signal, secrets, turn, attempt, recordUsage, toolResults);
80
225
  }
81
226
  catch (error) {
82
227
  if (error instanceof GuardrailError || isSteerSoftInterrupt(error))
@@ -100,7 +245,7 @@ export async function generateWithRetry(session, request, runId, options, signal
100
245
  }
101
246
  }
102
247
  }
103
- export async function generateProviderTurn(session, request, runId, signal, secrets = [], turn = 1, attempt = 1, recordUsage) {
248
+ export async function generateProviderTurn(session, request, runId, signal, secrets = [], turn = 1, attempt = 1, recordUsage, toolResults = []) {
104
249
  session.activeLimits.charge("maxProviderAttempts");
105
250
  session.activeLimits.charge("maxRequestBytes", jsonBytes(request));
106
251
  const startedAt = performance.now();
@@ -119,7 +264,9 @@ export async function generateProviderTurn(session, request, runId, signal, secr
119
264
  let messageId;
120
265
  let started = false;
121
266
  let usage;
267
+ let nativeStopReason;
122
268
  let usageRecorded = false;
269
+ let effectiveUsage;
123
270
  const bufferedOutput = [];
124
271
  const bufferOutput = Boolean(session.activeGuardrails?.output?.length || session.activeLimitOutputBuffer);
125
272
  const emitOutput = (event) => {
@@ -130,9 +277,11 @@ export async function generateProviderTurn(session, request, runId, signal, secr
130
277
  };
131
278
  const recordTurnUsage = async () => {
132
279
  if (usageRecorded)
133
- return;
280
+ return effectiveUsage;
134
281
  usageRecorded = true;
135
- await recordUsage?.(usage, turn, attempt);
282
+ // The seam may return a labeled estimate (plan 091 T2); without a callback the reported value stands.
283
+ effectiveUsage = (await recordUsage?.(usage, turn, attempt)) ?? usage;
284
+ return effectiveUsage;
136
285
  };
137
286
  const turnAbort = new AbortController();
138
287
  const cleanupTurn = bridgeAbort(signal, turnAbort);
@@ -153,6 +302,7 @@ export async function generateProviderTurn(session, request, runId, signal, secr
153
302
  usage = event.usage;
154
303
  if (event.type === "done") {
155
304
  usage = event.usage ?? usage;
305
+ nativeStopReason = event.stopReason;
156
306
  break;
157
307
  }
158
308
  if (event.type === "message_start") {
@@ -189,7 +339,13 @@ export async function generateProviderTurn(session, request, runId, signal, secr
189
339
  stage: "output",
190
340
  guardrails: session.activeGuardrails,
191
341
  value: { content, calls, messageId, started, usage },
192
- context: { sessionId: session.id, runId, metadata: session.activeMetadata ?? {}, signal: turnAbort.signal },
342
+ context: {
343
+ sessionId: session.id,
344
+ runId,
345
+ metadata: session.activeMetadata ?? {},
346
+ signal: turnAbort.signal,
347
+ toolResults,
348
+ },
193
349
  redactor: session.activeRedactor,
194
350
  emit: (event) => session.emit(event),
195
351
  }));
@@ -203,10 +359,15 @@ export async function generateProviderTurn(session, request, runId, signal, secr
203
359
  sessionId: session.id,
204
360
  runId,
205
361
  turn,
206
- metadata: buildMetadata({ latencyMs }),
207
- usage,
362
+ metadata: buildMetadata({
363
+ latencyMs,
364
+ stopReason: normalizeTurnStopReason(nativeStopReason, calls),
365
+ budgets: turnBudgets(session, request.model, effectiveUsage),
366
+ ...cacheMetadata(effectiveUsage),
367
+ }),
368
+ usage: effectiveUsage,
208
369
  });
209
- return { content, calls, messageId, started, usage };
370
+ return { content, calls, messageId, started, usage: effectiveUsage };
210
371
  }
211
372
  catch (error) {
212
373
  if (isSteerSoftInterrupt(error) || isSteerSoftInterrupt(turnAbort.signal.reason)) {
@@ -217,21 +378,32 @@ export async function generateProviderTurn(session, request, runId, signal, secr
217
378
  sessionId: session.id,
218
379
  runId,
219
380
  turn,
220
- metadata: buildMetadata({ latencyMs }),
221
- usage,
381
+ metadata: buildMetadata({
382
+ latencyMs,
383
+ stopReason: "abort",
384
+ budgets: turnBudgets(session, request.model, effectiveUsage),
385
+ ...cacheMetadata(effectiveUsage),
386
+ }),
387
+ usage: effectiveUsage,
222
388
  });
223
389
  throw new SteerSoftInterrupt();
224
390
  }
225
391
  const latencyMs = Math.round(performance.now() - startedAt);
226
- const info = error instanceof ProviderTurnFailure ? redactSecrets(error.info, secrets) : errorToErrorInfo(error, secrets);
392
+ const info = error instanceof ProviderTurnFailure ? redactSecrets(error.info, secrets) : providerError(error, secrets).error;
227
393
  await recordTurnUsage();
228
394
  session.emit({
229
395
  type: "provider_turn_finished",
230
396
  sessionId: session.id,
231
397
  runId,
232
398
  turn,
233
- metadata: buildMetadata({ latencyMs, httpStatus: readProviderHttpStatus(info) }),
234
- usage,
399
+ metadata: buildMetadata({
400
+ latencyMs,
401
+ httpStatus: readProviderHttpStatus(info),
402
+ stopReason: signal.aborted || turnAbort.signal.aborted ? "abort" : "provider_error",
403
+ budgets: turnBudgets(session, request.model, effectiveUsage),
404
+ ...cacheMetadata(effectiveUsage),
405
+ }),
406
+ usage: effectiveUsage,
235
407
  error: info,
236
408
  });
237
409
  if (error instanceof GuardrailError || error instanceof ProviderTurnFailure)
@@ -238,6 +238,24 @@ export function bindChargeToolRound(ctx) {
238
238
  }
239
239
  };
240
240
  }
241
+ /** Last-N dispatched tool calls kept for `budget_exhausted` attribution (plan 087 T2); the hash
242
+ * is the same canonical arguments hash the effect store uses, so raw args never enter events. */
243
+ const RECENT_TOOL_CALL_LIMIT = 10;
244
+ function recordRecentToolCall(session, call) {
245
+ const recent = (session.activeRecentToolCalls ??= []);
246
+ recent.push({ id: call.id, name: call.name, argHash: `sha256:${toolEffectArgumentsHash(call.arguments)}` });
247
+ if (recent.length > RECENT_TOOL_CALL_LIMIT)
248
+ recent.shift();
249
+ }
250
+ function dispatchFilter(ctx) {
251
+ const hiddenOk = ctx.options.allowHiddenToolCalls ?? ctx.session.agent.config.allowHiddenToolCalls;
252
+ if (hiddenOk || ctx.turnAllow === undefined) {
253
+ return ctx.tools.length > 0 ? { filter: { allow: ctx.tools.map((tool) => tool.name) } } : {};
254
+ }
255
+ if (ctx.turnAllow.length > 0)
256
+ return { filter: { allow: ctx.turnAllow } };
257
+ return ctx.tools.length > 0 ? { filter: { deny: ctx.tools.map((tool) => tool.name) } } : {};
258
+ }
241
259
  export function bindDispatchToolCall(ctx) {
242
260
  return async (call) => {
243
261
  const sticky = matchStickyDecision(ctx.session, call, ctx.registry);
@@ -251,8 +269,10 @@ export function bindDispatchToolCall(ctx) {
251
269
  if (ctx.session.activeGatedRound?.has(call.id)) {
252
270
  return { toolCallId: call.id, name: call.name, metadata: { approvalPending: true } };
253
271
  }
272
+ ctx.toolCalls += 1;
273
+ recordRecentToolCall(ctx.session, call);
254
274
  try {
255
- return await dispatchToolCall({
275
+ const result = await dispatchToolCall({
256
276
  call,
257
277
  registry: ctx.registry,
258
278
  context: {
@@ -278,7 +298,7 @@ export function bindDispatchToolCall(ctx) {
278
298
  ownership: ctx.session.activeOwnership,
279
299
  identity: ctx.session.activeIdentity,
280
300
  guardrails: ctx.session.activeGuardrails,
281
- ...(ctx.tools.length > 0 ? { filter: { allow: ctx.tools.map((tool) => tool.name) } } : {}),
301
+ ...dispatchFilter(ctx),
282
302
  limitTracker: ctx.limits,
283
303
  beforeExecute: async (mediatedCall) => {
284
304
  const durable = ctx.session.activeDurable;
@@ -329,6 +349,8 @@ export function bindDispatchToolCall(ctx) {
329
349
  },
330
350
  validate: ctx.validate,
331
351
  });
352
+ ctx.toolResults.push(result);
353
+ return result;
332
354
  }
333
355
  catch (error) {
334
356
  if (error instanceof AgentDelegationSuspendedError && !error.toolCall)
@@ -1,8 +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 { AttentionStickyFrontier, PersistedAttentionStickyFrontier } from "../../attention-compiler.js";
5
- 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 { AttentionFoldLedger, AttentionStickyFrontier, PersistedAttentionFoldLedger, 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, ToolCallSummary, ToolDefinition, ToolEffectStore, ToolRegistry, ToolResult, Usage } from "../../contracts.js";
6
6
  import type { AgentIdentity } from "../../identity.js";
7
7
  import type { AgentInput } from "../../input.js";
8
8
  import type { SecretRedactor } from "../../redaction.js";
@@ -33,9 +33,19 @@ export type SessionHost = {
33
33
  activeIdentity?: AgentIdentity;
34
34
  activeIdempotencyKey?: string;
35
35
  activeGuardrails?: Guardrails;
36
+ /** Plan 092 Task 2: packs compiled once at session construction; read-only for phases. */
37
+ readonly packGuardrails?: Guardrails;
36
38
  activeMetadata?: Readonly<Record<string, unknown>>;
37
39
  activePromptVersion?: PromptVersionRef;
38
40
  activeLimits?: RunLimitTracker;
41
+ /** Plan 091 T2: input tokens of the latest provider turn plus whether the
42
+ * provider reported them. Set by the usage seam; read by `contextMeter()`. */
43
+ activeInputMeter?: {
44
+ readonly tokens: number;
45
+ readonly source: "reported" | "estimated";
46
+ };
47
+ /** Bounded last-N tool-call summaries of the active run (plan 087 T2): ids, names, arg hashes. */
48
+ activeRecentToolCalls?: ToolCallSummary[];
39
49
  activeLimitOutputBuffer: boolean;
40
50
  activeDurable?: ActiveDurableRun;
41
51
  activeLoop?: AgentLoopStrategy;
@@ -45,6 +55,8 @@ export type SessionHost = {
45
55
  }>;
46
56
  activeLoopTurn: number;
47
57
  readonly loadedSkills: LoadedSkillSet;
58
+ /** Run-owned monotonic prompt tail; cleared before each new run. */
59
+ readonly tailSegments: Map<string, Message>;
48
60
  readonly activatedTools: ActiveToolSet;
49
61
  restoredSkillBodies: readonly LoadedSkillBodiesEntry[];
50
62
  activeRunSkills: readonly Skill[];
@@ -57,6 +69,15 @@ export type SessionHost = {
57
69
  serializedAttentionSticky(): PersistedAttentionStickyFrontier | undefined;
58
70
  /** Plan 074 P3: restore a frontier that was validated when the checkpoint was loaded. */
59
71
  restoreAttentionSticky(persisted: PersistedAttentionStickyFrontier): void;
72
+ /** Folded bodies for this session (plan 086 T3); session-owned so a resumed fold re-applies
73
+ * the same stub bytes instead of calling the host `summarize` again. Lazily created. */
74
+ attentionFoldFor(): AttentionFoldLedger;
75
+ /** Plan 086 T3: bounded ledger snapshot for a durable checkpoint (undefined before any fold). */
76
+ serializedAttentionFold(): PersistedAttentionFoldLedger | undefined;
77
+ /** Plan 086 T3: adopt a ledger validated when the checkpoint was loaded. */
78
+ restoreAttentionFold(ledger: AttentionFoldLedger): void;
79
+ /** Plan 086 T3: `attention.compiler.durable` for the current run; set by the run assembler. */
80
+ attentionDurable: boolean;
60
81
  invalidateSnapshot(): void;
61
82
  resolveRunProvider(options: RunOptions): void;
62
83
  emit(event: AgentEvent): void;
@@ -87,6 +108,11 @@ export type SessionHost = {
87
108
  snapshot(): Promise<SessionContextSnapshot>;
88
109
  };
89
110
  export declare function asSessionHost(session: unknown): SessionHost;
111
+ /** Why a run's loop ended, plus the host's stop detail when `RunOptions.turnPolicy` stopped it. */
112
+ export type RunStopInfo = {
113
+ readonly reason: AgentFinishReason;
114
+ readonly detail?: string;
115
+ };
90
116
  export type RoundContext = {
91
117
  session: SessionHost;
92
118
  input: AgentInput;
@@ -111,12 +137,20 @@ export type RoundContext = {
111
137
  loop: AgentLoopStrategy;
112
138
  toolConcurrency: number;
113
139
  toolsDisclosure: import("../../tool-search.js").ToolsDisclosure;
140
+ /** Per-turn dispatch overlay; undefined when `toolNarrowing` is unset. */
141
+ turnAllow?: readonly string[];
114
142
  assembledTurn: boolean;
115
143
  artifactFinished: boolean;
116
144
  artifactFailedInfo: {
117
145
  message: string;
118
146
  code?: string | number;
119
147
  } | undefined;
148
+ /** Host tool calls dispatched in this run; the turn-boundary context's `toolCalls` (plan 084 Task 2). */
149
+ toolCalls: number;
150
+ /** Completed host tool results from this run, for output evidence guardrails (plan 084 Task 5). */
151
+ toolResults: ToolResult[];
152
+ /** Set when a `RunOptions.turnPolicy` stop ended the loop (plan 084 Task 2). */
153
+ runStop?: RunStopInfo;
120
154
  runUsage: {
121
155
  add(usage: Usage): void;
122
156
  value(): Usage | undefined;