@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
@@ -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
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,11 +117,103 @@ 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
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);
@@ -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") {
@@ -209,10 +359,15 @@ export async function generateProviderTurn(session, request, runId, signal, secr
209
359
  sessionId: session.id,
210
360
  runId,
211
361
  turn,
212
- metadata: buildMetadata({ latencyMs }),
213
- 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,
214
369
  });
215
- return { content, calls, messageId, started, usage };
370
+ return { content, calls, messageId, started, usage: effectiveUsage };
216
371
  }
217
372
  catch (error) {
218
373
  if (isSteerSoftInterrupt(error) || isSteerSoftInterrupt(turnAbort.signal.reason)) {
@@ -223,8 +378,13 @@ export async function generateProviderTurn(session, request, runId, signal, secr
223
378
  sessionId: session.id,
224
379
  runId,
225
380
  turn,
226
- metadata: buildMetadata({ latencyMs }),
227
- usage,
381
+ metadata: buildMetadata({
382
+ latencyMs,
383
+ stopReason: "abort",
384
+ budgets: turnBudgets(session, request.model, effectiveUsage),
385
+ ...cacheMetadata(effectiveUsage),
386
+ }),
387
+ usage: effectiveUsage,
228
388
  });
229
389
  throw new SteerSoftInterrupt();
230
390
  }
@@ -236,8 +396,14 @@ export async function generateProviderTurn(session, request, runId, signal, secr
236
396
  sessionId: session.id,
237
397
  runId,
238
398
  turn,
239
- metadata: buildMetadata({ latencyMs, httpStatus: readProviderHttpStatus(info) }),
240
- 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,
241
407
  error: info,
242
408
  });
243
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);
@@ -252,6 +270,7 @@ export function bindDispatchToolCall(ctx) {
252
270
  return { toolCallId: call.id, name: call.name, metadata: { approvalPending: true } };
253
271
  }
254
272
  ctx.toolCalls += 1;
273
+ recordRecentToolCall(ctx.session, call);
255
274
  try {
256
275
  const result = await dispatchToolCall({
257
276
  call,
@@ -279,7 +298,7 @@ export function bindDispatchToolCall(ctx) {
279
298
  ownership: ctx.session.activeOwnership,
280
299
  identity: ctx.session.activeIdentity,
281
300
  guardrails: ctx.session.activeGuardrails,
282
- ...(ctx.tools.length > 0 ? { filter: { allow: ctx.tools.map((tool) => tool.name) } } : {}),
301
+ ...dispatchFilter(ctx),
283
302
  limitTracker: ctx.limits,
284
303
  beforeExecute: async (mediatedCall) => {
285
304
  const durable = ctx.session.activeDurable;
@@ -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, 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";
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;
@@ -116,6 +137,8 @@ export type RoundContext = {
116
137
  loop: AgentLoopStrategy;
117
138
  toolConcurrency: number;
118
139
  toolsDisclosure: import("../../tool-search.js").ToolsDisclosure;
140
+ /** Per-turn dispatch overlay; undefined when `toolNarrowing` is unset. */
141
+ turnAllow?: readonly string[];
119
142
  assembledTurn: boolean;
120
143
  artifactFinished: boolean;
121
144
  artifactFailedInfo: {
@@ -1,8 +1,8 @@
1
1
  /** session (0.2.5 plan 025 Task 1 split). Moved verbatim from agent-session.ts; public surface unchanged behind the barrel. */
2
- import { ActiveDurableRun } from "../agent-approval.js";
2
+ import { ActiveDurableRun, ActiveDurableRunExtras } 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";
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";
4
+ import { type AttentionFoldLedger, type AttentionStickyFrontier, type PersistedAttentionFoldLedger, type PersistedAttentionStickyFrontier } from "../attention-compiler.js";
5
+ import type { Agent, AgentEvent, AgentRunResult, AgentRunState, AgentRunStateOptions, AgentSession, AgentSessionConfig, AIProvider, CompactionOptions, CompactionResult, ContextMeter, ErrorInfo, Guardrails, Message, OwnershipScope, PendingDecision, PromptVersionRef, ProviderRequest, RunDecision, RunOptions, SessionEntry, Skill, SteerOptions, SubscribeOptions, ToolCallSummary, ToolDefinition, ToolEffectStore, Usage } from "../contracts.js";
6
6
  import type { AgentIdentity } from "../identity.js";
7
7
  import type { AgentInput } from "../input.js";
8
8
  import type { RunLimitTracker } from "../run-limits.js";
@@ -30,9 +30,21 @@ export declare class RuntimeAgentSession implements AgentSession {
30
30
  activeIdentity?: AgentIdentity;
31
31
  private activeIdempotencyKey?;
32
32
  private activeGuardrails?;
33
+ /** Plan 092 Task 2: guardrail packs compiled once in the constructor; merged into every run's `activeGuardrails`. */
34
+ readonly packGuardrails?: Guardrails;
35
+ /** Original pack refs, carried into `fork()`/`clone()` so a branch cannot silently lose its policy. */
36
+ private readonly guardrailPackRefs?;
33
37
  activeMetadata?: Readonly<Record<string, unknown>>;
34
38
  activePromptVersion?: PromptVersionRef;
35
39
  activeLimits?: RunLimitTracker;
40
+ /** Plan 091 T2: input tokens of the latest provider turn plus whether the provider reported
41
+ * them; set by the usage seam, read by `contextMeter()`. */
42
+ activeInputMeter?: {
43
+ readonly tokens: number;
44
+ readonly source: "reported" | "estimated";
45
+ };
46
+ /** Bounded last-N tool-call summaries of the active run (plan 087 T2): ids, names, arg hashes. */
47
+ activeRecentToolCalls?: ToolCallSummary[];
36
48
  activeLimitOutputBuffer: boolean;
37
49
  activeDurable?: ActiveDurableRun;
38
50
  activeLoop?: import("../contracts.js").AgentLoopStrategy;
@@ -43,6 +55,8 @@ export declare class RuntimeAgentSession implements AgentSession {
43
55
  }>;
44
56
  activeLoopTurn: number;
45
57
  private readonly loadedSkills;
58
+ /** Run-owned only: loaded bodies and URI resources retain first insertion order within one provider loop. */
59
+ readonly tailSegments: Map<string, Message>;
46
60
  /** Tools activated via `search_tools` this session (plan 041); names-only in persistence. */
47
61
  readonly activatedTools: import("../tool-search.js").ActiveToolSet;
48
62
  /** Plan 018 Task 6 (closeout `checkpoint-bodies`): persisted exact instructions, registry-independent. */
@@ -62,6 +76,17 @@ export declare class RuntimeAgentSession implements AgentSession {
62
76
  /** Plan 074 P3: restore a frontier validated at checkpoint load, so a resumed run keeps its
63
77
  * stubs instead of re-deciding its first turn from the ratio. */
64
78
  restoreAttentionSticky(persisted: PersistedAttentionStickyFrontier): void;
79
+ /** Session-owned folded bodies (plan 086 T3); created on first use like the frontier, so a
80
+ * compiler-off session allocates nothing. */
81
+ private attentionFold?;
82
+ /** Set per run from the resolved compiler: `durable: true` opts the fold ledger and its
83
+ * frontier into checkpoints even when `persistSessionState` is off. */
84
+ attentionDurable: boolean;
85
+ attentionFoldFor(): AttentionFoldLedger;
86
+ /** Plan 086 T3: bounded ledger snapshot for a durable checkpoint; `undefined` before any fold. */
87
+ serializedAttentionFold(): PersistedAttentionFoldLedger | undefined;
88
+ /** Plan 086 T3: adopt a ledger validated at checkpoint load, so a resumed fold is byte-identical. */
89
+ restoreAttentionFold(ledger: AttentionFoldLedger): void;
65
90
  /** Plan 015 Task 4: re-add persisted loaded-skill names (names only; bodies re-resolve on demand). */
66
91
  restoreLoadedSkills(names: readonly string[]): void;
67
92
  /** Plan 041: re-add persisted activated-tool names (names only; inert for absent tools). */
@@ -79,10 +104,19 @@ export declare class RuntimeAgentSession implements AgentSession {
79
104
  readonly agent: Agent;
80
105
  });
81
106
  get leafId(): string | undefined;
107
+ /**
108
+ * Context-fill read (plan 091 T2): the latest provider turn's input tokens —
109
+ * provider-reported when it reported, else a labeled estimate — plus the
110
+ * per-request cap and cumulative run input budget, resolved exactly as
111
+ * `provider_turn_finished.budgets` resolves them. Before any provider turn in
112
+ * this session it estimates stored history, so a non-reporting model still
113
+ * shows a working meter instead of zero. Never billing; estimates are labeled.
114
+ */
115
+ contextMeter(): ContextMeter;
82
116
  subscribe(options?: SubscribeOptions): AsyncIterable<AgentEvent>;
83
117
  run(input: AgentInput, options?: RunOptions): Promise<AgentRunResult>;
84
118
  steer(input: AgentInput, options?: SteerOptions): void;
85
- resumeDurable(state: StoredAgentRunState, runState: AgentRunStateOptions, ownership?: OwnershipScope, signal?: AbortSignal, decisions?: ReadonlyMap<string, RunDecision>): Promise<AgentRunResult>;
119
+ resumeDurable(state: StoredAgentRunState, runState: AgentRunStateOptions, ownership?: OwnershipScope, signal?: AbortSignal, decisions?: ReadonlyMap<string, RunDecision>, extras?: ActiveDurableRunExtras): Promise<AgentRunResult>;
86
120
  recordDurableResumption(runId: string, interruption: import("../contracts.js").AgentRunInterruption, version: number, ownership?: OwnershipScope): Promise<void>;
87
121
  recordDurableDenial(runId: string, interruption: import("../contracts.js").AgentRunInterruption, version: number, ownership?: OwnershipScope): Promise<void>;
88
122
  private runInternal;
@@ -1,10 +1,10 @@
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 { policyList } from "../agent-tool-dispatch.js";
3
- import { createAttentionStickyFrontier, resolveInputCap, restoreAttentionStickyFrontier, serializeAttentionStickyFrontier, } from "../attention-compiler.js";
3
+ import { createAttentionFoldLedger, createAttentionStickyFrontier, resolveInputCap, restoreAttentionStickyFrontier, serializeAttentionFoldLedger, serializeAttentionStickyFrontier, } from "../attention-compiler.js";
4
4
  import { createDefaultCompactionStrategy, isCompactionEntryData } from "../compaction.js";
5
- import { estimateAssemblyTokens, estimateTextTokens } from "../context-budget.js";
5
+ import { estimateAssemblyTokens, estimateMessageTokens, estimateTextTokens } from "../context-budget.js";
6
6
  import { DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, DEFAULT_SNAPSHOT_CACHE_TTL_MS, HARD_MAX_SNAPSHOT_CACHE_TTL_MS, resolveShouldCompact, } from "../contracts.js";
7
- import { GuardrailError, runGuardrails } from "../guardrails.js";
7
+ import { compileGuardrailPacks, GuardrailError, runGuardrails } from "../guardrails.js";
8
8
  import { applyDefaultProviderRequestOptions, createProviderRequestPolicyChain, normalizeProviderRequestPolicyResult, } from "../provider-request-policy.js";
9
9
  import { redactAgentEvent, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry } from "../redaction.js";
10
10
  import { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, rebuildSessionContext } from "../session-stores.js";
@@ -39,9 +39,18 @@ export class RuntimeAgentSession {
39
39
  activeIdentity;
40
40
  activeIdempotencyKey;
41
41
  activeGuardrails;
42
+ /** Plan 092 Task 2: guardrail packs compiled once in the constructor; merged into every run's `activeGuardrails`. */
43
+ packGuardrails;
44
+ /** Original pack refs, carried into `fork()`/`clone()` so a branch cannot silently lose its policy. */
45
+ guardrailPackRefs;
42
46
  activeMetadata;
43
47
  activePromptVersion;
44
48
  activeLimits;
49
+ /** Plan 091 T2: input tokens of the latest provider turn plus whether the provider reported
50
+ * them; set by the usage seam, read by `contextMeter()`. */
51
+ activeInputMeter;
52
+ /** Bounded last-N tool-call summaries of the active run (plan 087 T2): ids, names, arg hashes. */
53
+ activeRecentToolCalls;
45
54
  activeLimitOutputBuffer = false;
46
55
  activeDurable;
47
56
  activeLoop;
@@ -49,6 +58,8 @@ export class RuntimeAgentSession {
49
58
  activeGatedRound;
50
59
  activeLoopTurn = 1;
51
60
  loadedSkills = createLoadedSkillSet();
61
+ /** Run-owned only: loaded bodies and URI resources retain first insertion order within one provider loop. */
62
+ tailSegments = new Map();
52
63
  /** Tools activated via `search_tools` this session (plan 041); names-only in persistence. */
53
64
  activatedTools = createActiveToolSet();
54
65
  /** Plan 018 Task 6 (closeout `checkpoint-bodies`): persisted exact instructions, registry-independent. */
@@ -77,6 +88,24 @@ export class RuntimeAgentSession {
77
88
  restoreAttentionSticky(persisted) {
78
89
  this.attentionSticky = restoreAttentionStickyFrontier(persisted);
79
90
  }
91
+ /** Session-owned folded bodies (plan 086 T3); created on first use like the frontier, so a
92
+ * compiler-off session allocates nothing. */
93
+ attentionFold;
94
+ /** Set per run from the resolved compiler: `durable: true` opts the fold ledger and its
95
+ * frontier into checkpoints even when `persistSessionState` is off. */
96
+ attentionDurable = false;
97
+ attentionFoldFor() {
98
+ this.attentionFold ??= createAttentionFoldLedger();
99
+ return this.attentionFold;
100
+ }
101
+ /** Plan 086 T3: bounded ledger snapshot for a durable checkpoint; `undefined` before any fold. */
102
+ serializedAttentionFold() {
103
+ return this.attentionFold && this.attentionFold.bodies.size > 0 ? serializeAttentionFoldLedger(this.attentionFold) : undefined;
104
+ }
105
+ /** Plan 086 T3: adopt a ledger validated at checkpoint load, so a resumed fold is byte-identical. */
106
+ restoreAttentionFold(ledger) {
107
+ this.attentionFold = ledger;
108
+ }
80
109
  /** Plan 015 Task 4: re-add persisted loaded-skill names (names only; bodies re-resolve on demand). */
81
110
  restoreLoadedSkills(names) {
82
111
  for (const name of names)
@@ -110,10 +139,46 @@ export class RuntimeAgentSession {
110
139
  this.store = config.store ?? config.agent.config.store ?? createMemorySessionStore();
111
140
  this.currentLeafId = config.leafId;
112
141
  this.snapshotCacheTtlMs = resolveSnapshotCacheTtlMs(config.snapshotCacheTtlMs);
142
+ this.packGuardrails = compileGuardrailPacks(config.guardrailPacks);
143
+ this.guardrailPackRefs = config.guardrailPacks;
144
+ const usageEstimation = config.agent.config.usageEstimation;
145
+ if (usageEstimation !== undefined && usageEstimation !== "fallback" && usageEstimation !== "off") {
146
+ throw new TypeError('usageEstimation must be "fallback" or "off"');
147
+ }
113
148
  }
114
149
  get leafId() {
115
150
  return this.currentLeafId;
116
151
  }
152
+ /**
153
+ * Context-fill read (plan 091 T2): the latest provider turn's input tokens —
154
+ * provider-reported when it reported, else a labeled estimate — plus the
155
+ * per-request cap and cumulative run input budget, resolved exactly as
156
+ * `provider_turn_finished.budgets` resolves them. Before any provider turn in
157
+ * this session it estimates stored history, so a non-reporting model still
158
+ * shows a working meter instead of zero. Never billing; estimates are labeled.
159
+ */
160
+ contextMeter() {
161
+ const model = this.agent.config.model;
162
+ const inputTokens = this.activeInputMeter?.tokens ?? estimateMessageTokens(this.history, model.model).tokens;
163
+ const source = this.activeInputMeter?.source ?? "estimated";
164
+ let inputCap;
165
+ try {
166
+ const setting = this.agent.config.attentionCompiler;
167
+ const options = typeof setting === "object" && setting !== null ? setting : undefined;
168
+ inputCap = resolveInputCap(options ? { maxInputTokens: options.maxInputTokens, reserveTokens: options.reserveTokens } : {}, model);
169
+ }
170
+ catch {
171
+ inputCap = undefined; // undialed model: omit instead of throwing a state read
172
+ }
173
+ const runInputBudget = this.activeLimits?.limits.maxInputTokens ?? undefined;
174
+ return {
175
+ inputTokens,
176
+ source,
177
+ ...(inputCap === undefined ? {} : { inputCap }),
178
+ ...(runInputBudget == null ? {} : { runInputBudget }),
179
+ ...(inputCap === undefined ? {} : { usedRatio: inputTokens / inputCap }),
180
+ };
181
+ }
117
182
  subscribe(options = {}) {
118
183
  const subscriber = new EventSubscriber(this.id, options, () => this.subscribers.delete(subscriber));
119
184
  this.subscribers.add(subscriber);
@@ -147,12 +212,13 @@ export class RuntimeAgentSession {
147
212
  this.pendingSoftInterrupt = true;
148
213
  }
149
214
  }
150
- async resumeDurable(state, runState, ownership, signal, decisions) {
215
+ async resumeDurable(state, runState, ownership, signal, decisions, extras) {
151
216
  return this.runInternal(state.input ?? [], { runState, ownership, signal }, state.runId, {
152
217
  options: runState,
153
218
  state,
154
219
  version: state.version,
155
220
  decisions,
221
+ ...extras,
156
222
  });
157
223
  }
158
224
  async recordDurableResumption(runId, interruption, version, ownership) {
@@ -241,7 +307,10 @@ export class RuntimeAgentSession {
241
307
  async compact(options = {}) {
242
308
  if (this.activeRun)
243
309
  throw new Error("Agent session already has an active run");
244
- return this.compactBranch(options, undefined, options.signal, "manual");
310
+ const result = await this.compactBranch(options, undefined, options.signal, "manual");
311
+ // Plan 091 T2: history changed, so a pre-compaction meter reading would overstate the context.
312
+ this.activeInputMeter = undefined;
313
+ return result;
245
314
  }
246
315
  abort(reason) {
247
316
  this.activeRun?.abort(reason);
@@ -264,6 +333,7 @@ export class RuntimeAgentSession {
264
333
  store: this.store,
265
334
  leafId: options.leafId ?? this.currentLeafId,
266
335
  metadata: this.metadata,
336
+ ...(this.guardrailPackRefs ? { guardrailPacks: this.guardrailPackRefs } : {}),
267
337
  });
268
338
  }
269
339
  async clone(options = {}) {
@@ -286,6 +356,7 @@ export class RuntimeAgentSession {
286
356
  store: this.store,
287
357
  leafId: branch.length ? remap.get(branch[branch.length - 1].id) : undefined,
288
358
  metadata: this.metadata,
359
+ ...(this.guardrailPackRefs ? { guardrailPacks: this.guardrailPackRefs } : {}),
289
360
  });
290
361
  }
291
362
  branchReader() {