@arnilo/prism 0.0.10 → 0.0.11

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 (45) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/agent-loops.d.ts +1 -0
  3. package/dist/agent-loops.js +37 -2
  4. package/dist/agents.js +125 -6
  5. package/dist/context-budget.d.ts +63 -0
  6. package/dist/context-budget.js +235 -0
  7. package/dist/contracts.d.ts +105 -0
  8. package/dist/contracts.js +77 -0
  9. package/dist/index.d.ts +6 -4
  10. package/dist/index.js +4 -3
  11. package/dist/input.d.ts +3 -0
  12. package/dist/input.js +71 -28
  13. package/dist/node/session-store-jsonl.js +4 -1
  14. package/dist/rpc.js +13 -2
  15. package/dist/session-stores.d.ts +7 -2
  16. package/dist/session-stores.js +174 -4
  17. package/dist/structured-output.d.ts +5 -1
  18. package/dist/structured-output.js +18 -0
  19. package/dist/testing/persistence-schema.d.ts +1 -1
  20. package/dist/testing/persistence-schema.js +8 -2
  21. package/dist/testing/session-store-conformance.d.ts +6 -0
  22. package/dist/testing/session-store-conformance.js +36 -1
  23. package/docs/agent-loops.md +8 -1
  24. package/docs/agent-session-runtime.md +5 -1
  25. package/docs/cli-rpc.md +2 -1
  26. package/docs/coding-agent-tools.md +68 -1
  27. package/docs/evaluations.md +1 -1
  28. package/docs/index.md +12 -11
  29. package/docs/input-and-prompt-assembly.md +4 -1
  30. package/docs/migration.md +16 -0
  31. package/docs/node-jsonl-session-store.md +1 -1
  32. package/docs/performance.md +17 -0
  33. package/docs/postgres-persistence.md +3 -3
  34. package/docs/provider-packages.md +1 -1
  35. package/docs/providers/anthropic.md +92 -0
  36. package/docs/providers/google.md +87 -0
  37. package/docs/public-contracts.md +4 -0
  38. package/docs/release-and-install.md +159 -70
  39. package/docs/review-coverage-2026-07-22-phase-6.md +209 -0
  40. package/docs/session-store-conformance.md +2 -0
  41. package/docs/session-stores.md +40 -1
  42. package/docs/sqlite-persistence.md +3 -3
  43. package/docs/structured-output.md +7 -1
  44. package/docs/workflows.md +2 -0
  45. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5
5
 
6
6
  All notable changes to this project will be documented in this file.
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.0.11] - 2026-07-22
11
+
12
+ ### Added
13
+
14
+ - Coding harness fundamentals for 0.0.11 (Plan 074): bounded `SessionIndex`/`searchSessions` (SQLite/Postgres FTS migration 004; memory linear|unsupported; JSONL unsupported), assembler `contextBudget` + omission reports, `@arnilo/prism-provider-anthropic` + `@arnilo/prism-provider-google`, mid-run `AgentSession.steer` / RPC steer, coding-agent `runCodingGoalVerify` and opt-in `ask_user_decision` (multi/free-text/durable suspend glue).
15
+ - Opt-in `structuredOutputTiming: "final-turn-only"` on `generate-validate-revise` (default `"every-turn"`): tool-eligible turns omit native schema so models can call tools; artifact/revision turns attach schema and withdraw tools.
16
+
17
+ ### Changed
18
+
19
+ - Versioned all 34 first-party manifests and exact internal ranges to `0.0.11` (adds `@arnilo/prism-provider-anthropic` + `@arnilo/prism-provider-google` to the publishable graph and `@arnilo/prism-providers` umbrella).
20
+ - Network-free search/budget evidence: `scripts/benchmark-0.0.11.mjs`.
21
+
8
22
  ## [0.0.10] - 2026-07-21
9
23
 
10
24
  ### Changed
@@ -7,6 +7,7 @@ export declare function generateValidateReviseLoop(opts: {
7
7
  readonly repairer?: ArtifactRepairer<unknown>;
8
8
  readonly maxRevisions?: number;
9
9
  readonly toolCalls?: "disabled" | "bounded";
10
+ readonly structuredOutputTiming?: "every-turn" | "final-turn-only";
10
11
  }): AgentLoopStrategy;
11
12
  export declare function resolveToolConcurrency(options: {
12
13
  loop?: AgentLoopStrategy | AgentLoopOptions;
@@ -1,5 +1,6 @@
1
1
  import { inputMessages } from "./input.js";
2
2
  import { createId } from "./ids.js";
3
+ import { artifactStructuredOutputRequest, withoutStructuredOutput } from "./structured-output.js";
3
4
  function throwIfAborted(signal) {
4
5
  if (signal.aborted)
5
6
  throw signal.reason instanceof Error ? signal.reason : new Error("Agent run aborted");
@@ -23,6 +24,7 @@ export const singleShotLoop = {
23
24
  let nextInput = ctx.input;
24
25
  for (let turn = 1;; turn += 1) {
25
26
  throwIfAborted(ctx.signal);
27
+ await ctx.applyPendingSteers?.();
26
28
  ctx.emit({ type: "turn_started", sessionId: ctx.sessionId, runId: ctx.runId, turn });
27
29
  const request = await ctx.assemble(nextInput, undefined, turn);
28
30
  throwIfAborted(ctx.signal);
@@ -37,8 +39,14 @@ export const singleShotLoop = {
37
39
  ctx.emit({ type: "message_finished", sessionId: ctx.sessionId, runId: ctx.runId, message });
38
40
  }
39
41
  ctx.emit({ type: "turn_finished", sessionId: ctx.sessionId, runId: ctx.runId, turn });
40
- if (calls.length === 0 || toolRounds >= ctx.maxToolRounds)
42
+ if (calls.length === 0 || toolRounds >= ctx.maxToolRounds) {
43
+ // Soft-interrupt / late steer: keep same run going when queue still has text.
44
+ if (await ctx.applyPendingSteers?.()) {
45
+ nextInput = [];
46
+ continue;
47
+ }
41
48
  break;
49
+ }
42
50
  toolRounds += 1;
43
51
  await dispatchToolCallsInOrder(calls, ctx);
44
52
  nextInput = [];
@@ -61,6 +69,7 @@ function defaultRepairer() {
61
69
  export function generateValidateReviseLoop(opts) {
62
70
  const max = opts.maxRevisions ?? 3;
63
71
  const repairer = opts.repairer ?? defaultRepairer();
72
+ const finalOnly = opts.structuredOutputTiming === "final-turn-only" && opts.toolCalls === "bounded";
64
73
  return {
65
74
  name: "generate-validate-revise",
66
75
  async run(ctx) {
@@ -69,10 +78,24 @@ export function generateValidateReviseLoop(opts) {
69
78
  let pendingHistory = [];
70
79
  let toolRounds = 0;
71
80
  let attempts = 0;
81
+ let artifactPhase = !finalOnly;
82
+ let savedSchema;
72
83
  for (let turn = 1; attempts <= max; turn += 1) {
73
84
  throwIfAborted(ctx.signal);
85
+ await ctx.applyPendingSteers?.();
74
86
  ctx.emit({ type: "turn_started", sessionId: ctx.sessionId, runId: ctx.runId, turn });
75
- const request = await ctx.assemble(nextInput, undefined, turn);
87
+ let request = await ctx.assemble(nextInput, undefined, turn);
88
+ if (request.options?.structuredOutput)
89
+ savedSchema ??= request.options.structuredOutput;
90
+ if (finalOnly) {
91
+ if (!artifactPhase && toolRounds < ctx.maxToolRounds) {
92
+ request = withoutStructuredOutput(request);
93
+ }
94
+ else {
95
+ artifactPhase = true;
96
+ request = artifactStructuredOutputRequest(request, savedSchema);
97
+ }
98
+ }
76
99
  throwIfAborted(ctx.signal);
77
100
  const { content, calls, messageId, started, usage: turnUsage } = await ctx.generate(request);
78
101
  usage = turnUsage ?? usage;
@@ -100,6 +123,17 @@ export function generateValidateReviseLoop(opts) {
100
123
  nextInput = [];
101
124
  continue;
102
125
  }
126
+ // Soft-interrupt / late steer before treating empty/final output as artifact work.
127
+ if (await ctx.applyPendingSteers?.()) {
128
+ nextInput = [];
129
+ continue;
130
+ }
131
+ // Call-free during tool phase → one more turn with schema on / tools off.
132
+ if (finalOnly && !artifactPhase) {
133
+ artifactPhase = true;
134
+ nextInput = [];
135
+ continue;
136
+ }
103
137
  const artifactCtx = {
104
138
  sessionId: ctx.sessionId,
105
139
  runId: ctx.runId,
@@ -214,6 +248,7 @@ export function resolveLoop(options, config) {
214
248
  repairer: loop.repairer,
215
249
  maxRevisions: loop.maxRevisions,
216
250
  toolCalls: loop.toolCalls,
251
+ structuredOutputTiming: loop.structuredOutputTiming,
217
252
  });
218
253
  }
219
254
  throw new Error(`Unknown agent loop strategy: ${strategy}`);
package/dist/agents.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AgentRunError, AgentRunStateError } from "./contracts.js";
1
+ import { AgentRunError, AgentRunStateError, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, } from "./contracts.js";
2
2
  import { resolveLoop, resolveToolConcurrency } from "./agent-loops.js";
3
3
  import { createId } from "./ids.js";
4
4
  import { assertGuardrailsAllowed, GuardrailError, runGuardrails } from "./guardrails.js";
@@ -91,6 +91,11 @@ class RuntimeAgentSession {
91
91
  currentLeafId;
92
92
  history = [];
93
93
  activeRun;
94
+ activeRunId;
95
+ activeProviderTurnAbort;
96
+ pendingSoftInterrupt = false;
97
+ pendingSteers = [];
98
+ pendingSteerBytes = 0;
94
99
  activeRedactor;
95
100
  activeProvider;
96
101
  activeLedger;
@@ -124,6 +129,31 @@ class RuntimeAgentSession {
124
129
  async run(input, options = {}) {
125
130
  return this.runInternal(input, options, randomId("run"));
126
131
  }
132
+ steer(input, options = {}) {
133
+ if (!this.activeRun || !this.activeRunId)
134
+ throw new Error("Agent session has no active run to steer");
135
+ const messages = inputToMessages(input).map((message) => this.redact(message));
136
+ if (messages.length === 0)
137
+ throw new Error("steer requires non-empty input");
138
+ let addBytes = 0;
139
+ for (const message of messages)
140
+ addBytes += messageTextBytes(message);
141
+ if (this.pendingSteers.length + messages.length > DEFAULT_MAX_PENDING_STEERS) {
142
+ throw new Error(`steer queue exceeds max pending messages (${DEFAULT_MAX_PENDING_STEERS})`);
143
+ }
144
+ if (this.pendingSteerBytes + addBytes > DEFAULT_MAX_PENDING_STEER_BYTES) {
145
+ throw new Error(`steer queue exceeds max pending bytes (${DEFAULT_MAX_PENDING_STEER_BYTES})`);
146
+ }
147
+ this.pendingSteers.push(...messages);
148
+ this.pendingSteerBytes += addBytes;
149
+ this.emit({ type: "queue_updated", sessionId: this.id, runId: this.activeRunId, size: this.pendingSteers.length });
150
+ if (options.softInterrupt) {
151
+ if (this.activeProviderTurnAbort)
152
+ this.activeProviderTurnAbort.abort(new SteerSoftInterrupt());
153
+ else
154
+ this.pendingSoftInterrupt = true;
155
+ }
156
+ }
127
157
  async resumeDurable(state, runState, ownership) {
128
158
  return this.runInternal(state.input ?? [], { runState, ownership }, state.runId, { options: runState, state, version: state.version });
129
159
  }
@@ -165,6 +195,10 @@ class RuntimeAgentSession {
165
195
  const controller = new AbortController();
166
196
  const cleanupSignal = bridgeAbort(options.signal, controller);
167
197
  this.activeRun = controller;
198
+ this.activeRunId = runId;
199
+ this.pendingSteers = [];
200
+ this.pendingSteerBytes = 0;
201
+ this.pendingSoftInterrupt = false;
168
202
  this.activeRedactor = options.redactor ?? this.agent.config.redactor;
169
203
  this.activeLedger = options.runLedger ?? this.agent.config.runLedger;
170
204
  this.activeOwnership = options.ownership ?? this.agent.config.ownership;
@@ -327,7 +361,15 @@ class RuntimeAgentSession {
327
361
  assembledTurn = false;
328
362
  const policyResult = await this.applyProviderRequestPolicies(request, runId, options, metadata, controller.signal);
329
363
  const middlewareRequest = await this.agent.config.middleware?.run("provider_request", policyResult.request) ?? policyResult.request;
330
- return this.generateWithRetry(this.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, this.activeLoopTurn, recordProviderUsage);
364
+ try {
365
+ return await this.generateWithRetry(this.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, this.activeLoopTurn, recordProviderUsage);
366
+ }
367
+ catch (error) {
368
+ if (isSteerSoftInterrupt(error)) {
369
+ return { content: [], calls: [], started: false, usage: undefined };
370
+ }
371
+ throw error;
372
+ }
331
373
  },
332
374
  isToolCallExclusive: (call) => registry.get(call.name)?.exclusive === true,
333
375
  dispatchToolCall: (call) => dispatchToolCall({
@@ -367,6 +409,8 @@ class RuntimeAgentSession {
367
409
  validate,
368
410
  }),
369
411
  appendMessage: (message) => this.appendMessage(message, runId),
412
+ hasPendingSteers: () => this.pendingSteers.length > 0,
413
+ applyPendingSteers: () => this.applyPendingSteers(runId, metadata, controller.signal),
370
414
  emit: (event) => {
371
415
  if (event.type === "turn_started")
372
416
  this.activeLoopTurn = event.turn;
@@ -443,6 +487,11 @@ class RuntimeAgentSession {
443
487
  finally {
444
488
  if (this.activeRun === controller)
445
489
  this.activeRun = undefined;
490
+ this.activeRunId = undefined;
491
+ this.activeProviderTurnAbort = undefined;
492
+ this.pendingSoftInterrupt = false;
493
+ this.pendingSteers = [];
494
+ this.pendingSteerBytes = 0;
446
495
  try {
447
496
  await this.drainLedger();
448
497
  if (this.activeLedger) {
@@ -693,7 +742,7 @@ class RuntimeAgentSession {
693
742
  return await this.generateProviderTurn(request, runId, signal, secrets, turn, attempt, recordUsage);
694
743
  }
695
744
  catch (error) {
696
- if (error instanceof GuardrailError)
745
+ if (error instanceof GuardrailError || isSteerSoftInterrupt(error))
697
746
  throw error;
698
747
  const failure = error instanceof ProviderTurnFailure ? error : undefined;
699
748
  const info = failure ? redactSecrets(failure.info, secrets) : errorToErrorInfo(error, secrets);
@@ -745,9 +794,18 @@ class RuntimeAgentSession {
745
794
  usageRecorded = true;
746
795
  await recordUsage?.(usage, turn, attempt);
747
796
  };
797
+ const turnAbort = new AbortController();
798
+ const cleanupTurn = bridgeAbort(signal, turnAbort);
799
+ this.activeProviderTurnAbort = turnAbort;
800
+ if (this.pendingSoftInterrupt) {
801
+ this.pendingSoftInterrupt = false;
802
+ turnAbort.abort(new SteerSoftInterrupt());
803
+ }
804
+ const turnRequest = { ...request, signal: turnAbort.signal };
748
805
  try {
749
- for await (const event of this.activeProvider.generate(request)) {
750
- throwIfAborted(signal);
806
+ throwIfAborted(turnAbort.signal);
807
+ for await (const event of this.activeProvider.generate(turnRequest)) {
808
+ throwIfAborted(turnAbort.signal);
751
809
  this.activeLimits.charge("maxResponseBytes", jsonBytes(event));
752
810
  if (event.type === "error")
753
811
  throw new ProviderTurnFailure(event.error, started);
@@ -791,7 +849,7 @@ class RuntimeAgentSession {
791
849
  stage: "output",
792
850
  guardrails: this.activeGuardrails,
793
851
  value: { content, calls, messageId, started, usage },
794
- context: { sessionId: this.id, runId, metadata: this.activeMetadata ?? {}, signal },
852
+ context: { sessionId: this.id, runId, metadata: this.activeMetadata ?? {}, signal: turnAbort.signal },
795
853
  redactor: this.activeRedactor,
796
854
  emit: (event) => this.emit(event),
797
855
  }));
@@ -811,6 +869,19 @@ class RuntimeAgentSession {
811
869
  return { content, calls, messageId, started, usage };
812
870
  }
813
871
  catch (error) {
872
+ if (isSteerSoftInterrupt(error) || isSteerSoftInterrupt(turnAbort.signal.reason)) {
873
+ await recordTurnUsage();
874
+ const latencyMs = Math.round(performance.now() - startedAt);
875
+ this.emit({
876
+ type: "provider_turn_finished",
877
+ sessionId: this.id,
878
+ runId,
879
+ turn,
880
+ metadata: buildMetadata({ latencyMs }),
881
+ usage,
882
+ });
883
+ throw new SteerSoftInterrupt();
884
+ }
814
885
  const latencyMs = Math.round(performance.now() - startedAt);
815
886
  const info = error instanceof ProviderTurnFailure ? redactSecrets(error.info, secrets) : errorToErrorInfo(error, secrets);
816
887
  await recordTurnUsage();
@@ -827,6 +898,33 @@ class RuntimeAgentSession {
827
898
  throw error;
828
899
  throw new ProviderTurnFailure(info, started);
829
900
  }
901
+ finally {
902
+ cleanupTurn();
903
+ if (this.activeProviderTurnAbort === turnAbort)
904
+ this.activeProviderTurnAbort = undefined;
905
+ }
906
+ }
907
+ async applyPendingSteers(runId, metadata, signal) {
908
+ if (this.pendingSteers.length === 0)
909
+ return false;
910
+ const drained = this.pendingSteers.splice(0);
911
+ this.pendingSteerBytes = 0;
912
+ this.emit({ type: "queue_updated", sessionId: this.id, runId, size: 0 });
913
+ for (const message of drained) {
914
+ throwIfAborted(signal);
915
+ const inputGuardrails = await runGuardrails({
916
+ stage: "input",
917
+ guardrails: this.activeGuardrails,
918
+ value: [message],
919
+ context: { sessionId: this.id, runId, metadata, signal },
920
+ redactor: this.activeRedactor,
921
+ emit: (event) => this.emit(event),
922
+ });
923
+ assertGuardrailsAllowed(inputGuardrails);
924
+ this.history.push(message);
925
+ await this.appendMessage(message, runId);
926
+ }
927
+ return true;
830
928
  }
831
929
  async applyProviderRequestPolicies(request, runId, options, metadata, signal) {
832
930
  const policies = [...policyList(this.agent.config.providerRequestPolicies), ...policyList(options.providerRequestPolicies)];
@@ -987,6 +1085,27 @@ function inputToMessages(input) {
987
1085
  return [input];
988
1086
  return [...input];
989
1087
  }
1088
+ const steerTextEncoder = new TextEncoder();
1089
+ function messageTextBytes(message) {
1090
+ let total = 0;
1091
+ for (const block of message.content) {
1092
+ if (block.type === "text")
1093
+ total += steerTextEncoder.encode(block.text).byteLength;
1094
+ }
1095
+ return total;
1096
+ }
1097
+ const STEER_SOFT_INTERRUPT_CODE = "steer_soft_interrupt";
1098
+ class SteerSoftInterrupt extends Error {
1099
+ code = STEER_SOFT_INTERRUPT_CODE;
1100
+ constructor() {
1101
+ super("Provider turn soft-interrupted by steer");
1102
+ this.name = "SteerSoftInterrupt";
1103
+ }
1104
+ }
1105
+ function isSteerSoftInterrupt(error) {
1106
+ return error instanceof SteerSoftInterrupt
1107
+ || (typeof error === "object" && error !== null && error.code === STEER_SOFT_INTERRUPT_CODE);
1108
+ }
990
1109
  function finalAssistantMessage(history) {
991
1110
  for (let index = history.length - 1; index >= 0; index -= 1) {
992
1111
  const message = history[index];
@@ -0,0 +1,63 @@
1
+ import type { ContextBlock, InputAssemblyLayout, Message, ProviderRequest, Skill, ToolDefinition } from "./contracts.js";
2
+ /** Assembler-time input budget. At least one max required when present. */
3
+ export interface ContextBudget {
4
+ readonly maxInputTokens?: number;
5
+ readonly maxInputBytes?: number;
6
+ readonly reportOmissions?: boolean;
7
+ }
8
+ export type ContextBudgetOmissionKind = "skills" | "context" | "history" | "tool_results" | "summaries" | "attachments" | "tools";
9
+ export interface ContextBudgetOmission {
10
+ readonly kind: ContextBudgetOmissionKind;
11
+ readonly id?: string;
12
+ readonly tokenEstimate: number;
13
+ readonly byteLength: number;
14
+ }
15
+ export interface ContextBudgetReport {
16
+ readonly omitted: readonly ContextBudgetOmission[];
17
+ readonly keptTokens: number;
18
+ readonly keptBytes: number;
19
+ readonly maxInputTokens?: number;
20
+ readonly maxInputBytes?: number;
21
+ readonly truncated: boolean;
22
+ }
23
+ export interface ContextBudgetMessageGroups {
24
+ readonly instructions: readonly Message[];
25
+ readonly summaries: readonly Message[];
26
+ readonly history: readonly Message[];
27
+ readonly input: readonly Message[];
28
+ readonly attachments: readonly Message[];
29
+ readonly toolResults: readonly Message[];
30
+ }
31
+ export declare const CONTEXT_BUDGET_REPORT_METADATA_KEY: "contextBudgetReport";
32
+ export declare const HARD_MAX_CONTEXT_BUDGET_TOKENS = 2000000;
33
+ export declare const HARD_MAX_CONTEXT_BUDGET_BYTES: number;
34
+ export declare const DEFAULT_MAX_CONTEXT_BUDGET_OMISSIONS = 256;
35
+ export declare const HARD_MAX_CONTEXT_BUDGET_OMISSIONS = 1024;
36
+ export declare const CONTEXT_BUDGET_ERROR_CODE: "context_budget_exceeded";
37
+ export declare class ContextBudgetError extends Error {
38
+ readonly code: "context_budget_exceeded";
39
+ constructor(message?: string);
40
+ }
41
+ export declare function isContextBudgetError(error: unknown): error is ContextBudgetError;
42
+ /** UTF-16 code units / 4. Estimate only — not billing. */
43
+ export declare function estimateTextTokens(text: string): number;
44
+ export declare function estimateTextBytes(text: string): number;
45
+ export declare function estimateMessageTokens(message: Message): number;
46
+ export declare function estimateMessageBytes(message: Message): number;
47
+ export declare function estimateAssemblyTokens(messages: readonly Message[]): number;
48
+ export declare function resolveContextBudget(budget: ContextBudget): Required<Pick<ContextBudget, "reportOmissions">> & ContextBudget;
49
+ export declare function getContextBudgetReport(request: ProviderRequest): ContextBudgetReport | undefined;
50
+ export declare function applyContextBudget(options: {
51
+ readonly groups: ContextBudgetMessageGroups;
52
+ readonly context?: readonly ContextBlock[];
53
+ readonly skills?: readonly Skill[];
54
+ readonly tools?: readonly ToolDefinition[];
55
+ readonly budget: ContextBudget;
56
+ readonly layout?: InputAssemblyLayout;
57
+ }): {
58
+ readonly groups: ContextBudgetMessageGroups;
59
+ readonly context: readonly ContextBlock[];
60
+ readonly skills: readonly Skill[];
61
+ readonly tools: readonly ToolDefinition[] | undefined;
62
+ readonly report: ContextBudgetReport;
63
+ };
@@ -0,0 +1,235 @@
1
+ export const CONTEXT_BUDGET_REPORT_METADATA_KEY = "contextBudgetReport";
2
+ export const HARD_MAX_CONTEXT_BUDGET_TOKENS = 2_000_000;
3
+ export const HARD_MAX_CONTEXT_BUDGET_BYTES = 32 * 1024 * 1024;
4
+ export const DEFAULT_MAX_CONTEXT_BUDGET_OMISSIONS = 256;
5
+ export const HARD_MAX_CONTEXT_BUDGET_OMISSIONS = 1_024;
6
+ export const CONTEXT_BUDGET_ERROR_CODE = "context_budget_exceeded";
7
+ export class ContextBudgetError extends Error {
8
+ code = CONTEXT_BUDGET_ERROR_CODE;
9
+ constructor(message = "context budget exceeded: mandatory prefix cannot fit") {
10
+ super(message);
11
+ this.name = "ContextBudgetError";
12
+ }
13
+ }
14
+ export function isContextBudgetError(error) {
15
+ return error instanceof Error && error.code === CONTEXT_BUDGET_ERROR_CODE;
16
+ }
17
+ /** UTF-16 code units / 4. Estimate only — not billing. */
18
+ export function estimateTextTokens(text) {
19
+ return Math.ceil(text.length / 4);
20
+ }
21
+ export function estimateTextBytes(text) {
22
+ return new TextEncoder().encode(text).byteLength;
23
+ }
24
+ export function estimateMessageTokens(message) {
25
+ return estimateTextTokens(messageText(message));
26
+ }
27
+ export function estimateMessageBytes(message) {
28
+ return estimateTextBytes(messageText(message));
29
+ }
30
+ export function estimateAssemblyTokens(messages) {
31
+ return messages.reduce((sum, message) => sum + estimateMessageTokens(message), 0);
32
+ }
33
+ export function resolveContextBudget(budget) {
34
+ const hasTokens = budget.maxInputTokens !== undefined;
35
+ const hasBytes = budget.maxInputBytes !== undefined;
36
+ if (!hasTokens && !hasBytes) {
37
+ throw new TypeError("contextBudget requires maxInputTokens and/or maxInputBytes");
38
+ }
39
+ if (hasTokens)
40
+ assertPositiveCap(budget.maxInputTokens, "maxInputTokens", HARD_MAX_CONTEXT_BUDGET_TOKENS);
41
+ if (hasBytes)
42
+ assertPositiveCap(budget.maxInputBytes, "maxInputBytes", HARD_MAX_CONTEXT_BUDGET_BYTES);
43
+ return { ...budget, reportOmissions: budget.reportOmissions === true };
44
+ }
45
+ export function getContextBudgetReport(request) {
46
+ const value = request.metadata?.[CONTEXT_BUDGET_REPORT_METADATA_KEY];
47
+ return isContextBudgetReport(value) ? value : undefined;
48
+ }
49
+ export function applyContextBudget(options) {
50
+ const budget = resolveContextBudget(options.budget);
51
+ const layout = options.layout ?? "legacy";
52
+ const groups = {
53
+ instructions: [...options.groups.instructions],
54
+ summaries: [...options.groups.summaries],
55
+ history: [...options.groups.history],
56
+ input: [...options.groups.input],
57
+ attachments: [...options.groups.attachments],
58
+ toolResults: [...options.groups.toolResults],
59
+ };
60
+ const context = [...(options.context ?? [])];
61
+ const skills = [...(options.skills ?? [])].filter((skill) => skill.instructions);
62
+ const tools = options.tools ? [...options.tools] : undefined;
63
+ const omitted = [];
64
+ const cost = () => measureAll(groups, context, skills, tools);
65
+ while (overBudget(cost(), budget)) {
66
+ const drop = dropNext(groups, context, skills, layout);
67
+ if (!drop) {
68
+ throw new ContextBudgetError();
69
+ }
70
+ if (omitted.length < HARD_MAX_CONTEXT_BUDGET_OMISSIONS)
71
+ omitted.push(drop);
72
+ }
73
+ const kept = cost();
74
+ const reportOmissions = omitted.slice(0, DEFAULT_MAX_CONTEXT_BUDGET_OMISSIONS);
75
+ return {
76
+ groups,
77
+ context,
78
+ skills,
79
+ tools,
80
+ report: {
81
+ omitted: budget.reportOmissions ? reportOmissions : [],
82
+ keptTokens: kept.tokens,
83
+ keptBytes: kept.bytes,
84
+ maxInputTokens: budget.maxInputTokens,
85
+ maxInputBytes: budget.maxInputBytes,
86
+ truncated: omitted.length > 0,
87
+ },
88
+ };
89
+ }
90
+ function dropNext(groups, context, skills, layout) {
91
+ // ponytail: drop from end of keep-stack (history/tool_results first). cache_aware keeps
92
+ // attachments longer so stable prefix stays intact while budget still allows it.
93
+ const order = layout === "cache_aware"
94
+ ? ["tool_results", "history", "summaries", "context", "skills", "attachments"]
95
+ : ["tool_results", "history", "summaries", "attachments", "context", "skills"];
96
+ for (const kind of order) {
97
+ if (kind === "tool_results" && groups.toolResults.length > 0) {
98
+ const message = groups.toolResults.pop();
99
+ return omission("tool_results", message.id ?? toolResultId(message), message);
100
+ }
101
+ if (kind === "history" && groups.history.length > 0) {
102
+ const message = groups.history.pop();
103
+ return omission("history", message.id, message);
104
+ }
105
+ if (kind === "summaries" && groups.summaries.length > 0) {
106
+ const message = groups.summaries.pop();
107
+ return omission("summaries", message.id, message);
108
+ }
109
+ if (kind === "attachments" && groups.attachments.length > 0) {
110
+ const message = groups.attachments.pop();
111
+ return omission("attachments", message.id, message);
112
+ }
113
+ if (kind === "context" && context.length > 0) {
114
+ const block = context.pop();
115
+ const text = `${block.title ? `${block.title}:\n` : "Context:\n"}${contextBlockText(block)}`;
116
+ return {
117
+ kind: "context",
118
+ id: block.id ?? block.title,
119
+ tokenEstimate: estimateTextTokens(text),
120
+ byteLength: estimateTextBytes(text),
121
+ };
122
+ }
123
+ if (kind === "skills" && skills.length > 0) {
124
+ const skill = skills.pop();
125
+ const text = `Skill ${skill.name}:\n${skill.instructions ?? ""}`;
126
+ return {
127
+ kind: "skills",
128
+ id: skill.name,
129
+ tokenEstimate: estimateTextTokens(text),
130
+ byteLength: estimateTextBytes(text),
131
+ };
132
+ }
133
+ }
134
+ return undefined;
135
+ }
136
+ function omission(kind, id, message) {
137
+ return {
138
+ kind,
139
+ id,
140
+ tokenEstimate: estimateMessageTokens(message),
141
+ byteLength: estimateMessageBytes(message),
142
+ };
143
+ }
144
+ function toolResultId(message) {
145
+ const block = message.content.find((part) => part.type === "tool_result");
146
+ return block && block.type === "tool_result" ? block.toolCallId : undefined;
147
+ }
148
+ function measureAll(groups, context, skills, tools) {
149
+ let tokens = 0;
150
+ let bytes = 0;
151
+ const addMessage = (message) => {
152
+ tokens += estimateMessageTokens(message);
153
+ bytes += estimateMessageBytes(message);
154
+ };
155
+ for (const message of groups.instructions)
156
+ addMessage(message);
157
+ for (const message of groups.summaries)
158
+ addMessage(message);
159
+ for (const message of groups.history)
160
+ addMessage(message);
161
+ for (const message of groups.input)
162
+ addMessage(message);
163
+ for (const message of groups.attachments)
164
+ addMessage(message);
165
+ for (const message of groups.toolResults)
166
+ addMessage(message);
167
+ for (const block of context) {
168
+ const text = `${block.title ? `${block.title}:\n` : "Context:\n"}${contextBlockText(block)}`;
169
+ tokens += estimateTextTokens(text);
170
+ bytes += estimateTextBytes(text);
171
+ }
172
+ for (const skill of skills) {
173
+ const text = `Skill ${skill.name}:\n${skill.instructions ?? ""}`;
174
+ tokens += estimateTextTokens(text);
175
+ bytes += estimateTextBytes(text);
176
+ }
177
+ if (tools?.length) {
178
+ const text = `Available tools:\n${tools.map((tool) => `- ${tool.name}${tool.description ? `: ${tool.description}` : ""}`).join("\n")}`;
179
+ tokens += estimateTextTokens(text);
180
+ bytes += estimateTextBytes(text);
181
+ }
182
+ return { tokens, bytes };
183
+ }
184
+ function overBudget(cost, budget) {
185
+ if (budget.maxInputTokens !== undefined && cost.tokens > budget.maxInputTokens)
186
+ return true;
187
+ if (budget.maxInputBytes !== undefined && cost.bytes > budget.maxInputBytes)
188
+ return true;
189
+ return false;
190
+ }
191
+ function assertPositiveCap(value, name, hardMax) {
192
+ if (!Number.isSafeInteger(value) || value < 1 || value > hardMax) {
193
+ throw new TypeError(`contextBudget.${name} must be a safe integer from 1 to ${hardMax}`);
194
+ }
195
+ }
196
+ function isContextBudgetReport(value) {
197
+ if (!value || typeof value !== "object" || Array.isArray(value))
198
+ return false;
199
+ const report = value;
200
+ return Array.isArray(report.omitted)
201
+ && typeof report.keptTokens === "number"
202
+ && typeof report.keptBytes === "number"
203
+ && typeof report.truncated === "boolean";
204
+ }
205
+ function messageText(message) {
206
+ return message.content.map((part) => {
207
+ if (part.type === "text" || part.type === "thinking")
208
+ return part.text;
209
+ if (part.type === "tool_result")
210
+ return JSON.stringify(part.result ?? part.error ?? null);
211
+ if (part.type === "tool_call")
212
+ return `${part.name}(${JSON.stringify(part.arguments)})`;
213
+ if (part.type === "tool_call_delta")
214
+ return `${part.name ?? "tool"}(${part.argumentsText ?? ""})`;
215
+ if (part.type === "image")
216
+ return part.url ?? part.resourceUri ?? part.mimeType ?? "[image]";
217
+ if (part.type === "audio")
218
+ return part.transcript ?? part.name ?? part.url ?? part.resourceUri ?? part.mediaType ?? "[audio]";
219
+ if (part.type === "file")
220
+ return part.name ?? part.url ?? part.resourceUri ?? part.mediaType ?? "[file]";
221
+ if (part.type === "document")
222
+ return part.transcript ?? part.name ?? part.url ?? part.resourceUri ?? part.mediaType ?? "[document]";
223
+ return "[content]";
224
+ }).join("\n");
225
+ }
226
+ function contextBlockText(block) {
227
+ if (typeof block.content === "string")
228
+ return block.content;
229
+ return block.content.map((part) => {
230
+ if (part.type === "text" || part.type === "thinking")
231
+ return part.text;
232
+ return "[content]";
233
+ }).join("\n");
234
+ }
235
+ //# sourceMappingURL=context-budget.js.map