@arnilo/prism 0.8.0 → 0.10.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 (141) hide show
  1. package/CHANGELOG.md +62 -1
  2. package/README.md +13 -12
  3. package/dist/agent-approval.d.ts +17 -2
  4. package/dist/agent-approval.js +15 -6
  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 +82 -11
  10. package/dist/agent-run-state.d.ts +47 -6
  11. package/dist/agent-run-state.js +154 -6
  12. package/dist/agent-session/event-subscriber.d.ts +2 -0
  13. package/dist/agent-session/event-subscriber.js +3 -0
  14. package/dist/agent-session/helpers.js +14 -0
  15. package/dist/agent-session/session/assemble.js +281 -32
  16. package/dist/agent-session/session/persist.d.ts +11 -0
  17. package/dist/agent-session/session/persist.js +48 -16
  18. package/dist/agent-session/session/provider-round.d.ts +14 -4
  19. package/dist/agent-session/session/provider-round.js +226 -19
  20. package/dist/agent-session/session/tool-round.d.ts +2 -2
  21. package/dist/agent-session/session/tool-round.js +78 -6
  22. package/dist/agent-session/session/types.d.ts +44 -3
  23. package/dist/agent-session/session.d.ts +100 -5
  24. package/dist/agent-session/session.js +224 -13
  25. package/dist/attention-compiler.d.ts +51 -2
  26. package/dist/attention-compiler.js +282 -21
  27. package/dist/cache-helpers.d.ts +4 -2
  28. package/dist/cache-helpers.js +8 -6
  29. package/dist/checkpoint-restore.d.ts +45 -0
  30. package/dist/checkpoint-restore.js +54 -0
  31. package/dist/context-budget.d.ts +13 -1
  32. package/dist/context-budget.js +57 -4
  33. package/dist/contracts-core/agent.d.ts +52 -1
  34. package/dist/contracts-core/attention.d.ts +95 -0
  35. package/dist/contracts-core/content.d.ts +10 -0
  36. package/dist/contracts-core/extensions.d.ts +3 -0
  37. package/dist/contracts-core/guardrail-packs.d.ts +46 -0
  38. package/dist/contracts-core/guardrail-packs.js +2 -0
  39. package/dist/contracts-core/loop.d.ts +36 -0
  40. package/dist/contracts-core/provider.d.ts +30 -0
  41. package/dist/contracts-core/run-limits.d.ts +29 -1
  42. package/dist/contracts-core/session.d.ts +23 -5
  43. package/dist/contracts-core/session.js +21 -2
  44. package/dist/contracts-core/usage.d.ts +40 -0
  45. package/dist/contracts-core/usage.js +8 -0
  46. package/dist/contracts-core.d.ts +2 -0
  47. package/dist/contracts-core.js +2 -0
  48. package/dist/contracts-protocol.d.ts +81 -5
  49. package/dist/contracts-run-state.d.ts +91 -2
  50. package/dist/contributions.d.ts +2 -1
  51. package/dist/contributions.js +1 -0
  52. package/dist/extensions.d.ts +15 -1
  53. package/dist/extensions.js +68 -0
  54. package/dist/guardrail-packs/coding-standard.d.ts +3 -0
  55. package/dist/guardrail-packs/coding-standard.js +63 -0
  56. package/dist/guardrail-packs/destructive-commands.d.ts +3 -0
  57. package/dist/guardrail-packs/destructive-commands.js +46 -0
  58. package/dist/guardrail-packs/errors.d.ts +7 -0
  59. package/dist/guardrail-packs/errors.js +9 -0
  60. package/dist/guardrail-packs/index.d.ts +4 -0
  61. package/dist/guardrail-packs/index.js +15 -0
  62. package/dist/guardrail-packs/secrets-hygiene.d.ts +3 -0
  63. package/dist/guardrail-packs/secrets-hygiene.js +23 -0
  64. package/dist/guardrail-packs/types.d.ts +26 -0
  65. package/dist/guardrail-packs/types.js +2 -0
  66. package/dist/guardrail-packs/validation-respect.d.ts +3 -0
  67. package/dist/guardrail-packs/validation-respect.js +69 -0
  68. package/dist/guardrails.d.ts +61 -1
  69. package/dist/guardrails.js +377 -0
  70. package/dist/index.d.ts +16 -11
  71. package/dist/index.js +10 -7
  72. package/dist/input.d.ts +8 -1
  73. package/dist/input.js +68 -6
  74. package/dist/middleware.d.ts +37 -2
  75. package/dist/middleware.js +41 -0
  76. package/dist/node/session-store-jsonl.js +18 -3
  77. package/dist/observability.js +6 -0
  78. package/dist/provider-events.d.ts +8 -2
  79. package/dist/provider-events.js +60 -2
  80. package/dist/providers/openai-compatible.js +6 -3
  81. package/dist/run-bundle.d.ts +6 -1
  82. package/dist/run-bundle.js +5 -1
  83. package/dist/run-limits.d.ts +11 -1
  84. package/dist/run-limits.js +59 -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 +59 -0
  89. package/dist/testing/prefix-stability-conformance.js +172 -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/tools.d.ts +5 -0
  93. package/dist/tools.js +21 -6
  94. package/dist/usage-estimation.d.ts +29 -0
  95. package/dist/usage-estimation.js +79 -0
  96. package/docs/agent-events.md +75 -4
  97. package/docs/agent-session-runtime.md +10 -6
  98. package/docs/attention-compiler.md +89 -8
  99. package/docs/caveman.md +1 -1
  100. package/docs/coding-agent-tools.md +1 -1
  101. package/docs/compaction-and-retry.md +1 -1
  102. package/docs/compaction-llm.md +2 -0
  103. package/docs/compaction-observational-memory.md +54 -7
  104. package/docs/durable-runs.md +46 -3
  105. package/docs/embeddings.md +9 -0
  106. package/docs/evaluations.md +5 -0
  107. package/docs/execution-timeline.md +79 -1
  108. package/docs/extensions.md +20 -3
  109. package/docs/guardrails.md +50 -4
  110. package/docs/hooks.md +282 -0
  111. package/docs/index.md +37 -15
  112. package/docs/input-and-prompt-assembly.md +4 -4
  113. package/docs/instruction-injection.md +1 -0
  114. package/docs/knowledge-sync.md +4 -0
  115. package/docs/live-testing.md +3 -1
  116. package/docs/memory-fabric.md +28 -0
  117. package/docs/middleware-hooks.md +90 -4
  118. package/docs/migrate-to-0.9.md +210 -0
  119. package/docs/migration.md +26 -0
  120. package/docs/multi-agent-patterns.md +25 -2
  121. package/docs/node-jsonl-session-store.md +7 -1
  122. package/docs/observability.md +7 -3
  123. package/docs/options-index.md +4 -1
  124. package/docs/policy-and-audit.md +26 -1
  125. package/docs/prefix-stability-conformance.md +143 -0
  126. package/docs/provider-caching.md +4 -4
  127. package/docs/provider-conformance.md +16 -0
  128. package/docs/provider-packages.md +20 -20
  129. package/docs/public-contracts.md +3 -2
  130. package/docs/rag.md +188 -3
  131. package/docs/release-and-install.md +45 -40
  132. package/docs/runs-and-usage.md +56 -10
  133. package/docs/scoped-agent-memory.md +270 -0
  134. package/docs/scoped-memory.md +138 -0
  135. package/docs/session-store-conformance.md +1 -2
  136. package/docs/session-stores.md +17 -17
  137. package/docs/supervisors.md +32 -12
  138. package/docs/tools.md +18 -1
  139. package/docs/wiki.md +4 -2
  140. package/docs/workflows.md +5 -0
  141. package/package.json +8 -2
package/dist/input.js CHANGED
@@ -5,7 +5,7 @@ import { runInstructionInjectors } from "./instruction-injection.js";
5
5
  import { applyDefaultProviderRequestOptions } from "./provider-request-policy.js";
6
6
  import { redactMessage } from "./redaction.js";
7
7
  import { loadTextResource } from "./resources.js";
8
- import { skillMessages as buildSkillMessages } from "./skill-disclosure.js";
8
+ import { capSkillCatalog, selectSkillsForPrompt, skillHasRenderableBody, skillMessages as buildSkillMessages, } from "./skill-disclosure.js";
9
9
  import { composeSystemPrompt } from "./system-prompts.js";
10
10
  import { foldToolResultHistory, foldToolResults } from "./tool-result-fold.js";
11
11
  import { selectDisclosedTools } from "./tool-search.js";
@@ -51,11 +51,16 @@ export function createDefaultPromptBuilder() {
51
51
  // text-only (or unknown-capability) models — duplicating it doubles tool tokens per turn.
52
52
  const tools = request.model?.capabilities?.tools === true ? undefined : request.tools;
53
53
  const context = contextMessages(request.context);
54
- const skills = buildSkillMessages(request.skills, {
55
- disclosure: request.skillsDisclosure,
56
- loaded: request.loadedSkills,
57
- demotedBodies: request.demotedSkillBodies?.length ? new Set(request.demotedSkillBodies) : undefined,
58
- });
54
+ const skills = buildSkillMessages(request.skills, request.tailSkillBodies
55
+ ? {
56
+ disclosure: "progressive",
57
+ demotedBodies: request.demotedSkillBodies?.length ? new Set(request.demotedSkillBodies) : undefined,
58
+ }
59
+ : {
60
+ disclosure: request.skillsDisclosure,
61
+ loaded: request.loadedSkills,
62
+ demotedBodies: request.demotedSkillBodies?.length ? new Set(request.demotedSkillBodies) : undefined,
63
+ });
59
64
  const declarations = toolMessages(tools);
60
65
  if ((request.inputLayout ?? "cache_aware") === "legacy") {
61
66
  return [...context, ...skills, ...declarations, ...request.messages];
@@ -156,9 +161,11 @@ export async function assembleProviderInput(options) {
156
161
  tools,
157
162
  fold: options.toolResultFold,
158
163
  frontier: options.attentionSticky,
164
+ attentionFold: options.attentionFold,
159
165
  redactor: options.redactor,
160
166
  signal: options.signal,
161
167
  turn,
168
+ runInputTokens: options.runInputTokens,
162
169
  sessionId: options.sessionId,
163
170
  runId: options.runId,
164
171
  });
@@ -213,6 +220,14 @@ export async function assembleProviderInput(options) {
213
220
  ...baseContext,
214
221
  });
215
222
  }
223
+ const tailSegments = options.tailSegments;
224
+ const tailSkillBodies = tailSegments !== undefined;
225
+ if (tailSegments) {
226
+ const activeTailSegments = new Set();
227
+ messages = moveResourceMessagesToTail(messages, options, tailSegments, activeTailSegments);
228
+ appendSkillTailSegments(tailSegments, activeTailSegments, skills, options.skillsDisclosure, options.loadedSkills, demotedSkillBodies);
229
+ messages = [...messages, ...tailMessages(tailSegments, activeTailSegments)];
230
+ }
216
231
  const promptBuilder = options.promptBuilder ?? createDefaultPromptBuilder();
217
232
  const promptRequest = options.middleware
218
233
  ? await options.middleware.run("prompt_build", {
@@ -222,6 +237,7 @@ export async function assembleProviderInput(options) {
222
237
  skills,
223
238
  skillsDisclosure: options.skillsDisclosure,
224
239
  loadedSkills: options.loadedSkills,
240
+ tailSkillBodies,
225
241
  demotedSkillBodies,
226
242
  tools,
227
243
  metadata: options.metadata,
@@ -234,6 +250,7 @@ export async function assembleProviderInput(options) {
234
250
  skills,
235
251
  skillsDisclosure: options.skillsDisclosure,
236
252
  loadedSkills: options.loadedSkills,
253
+ tailSkillBodies,
237
254
  demotedSkillBodies,
238
255
  tools,
239
256
  metadata: options.metadata,
@@ -389,6 +406,51 @@ function textMessage(role, text, metadata) {
389
406
  function contextMessages(context) {
390
407
  return (context ?? []).map((block) => textMessage("system", `${block.title ? `${block.title}:\n` : "Context:\n"}${blockText(block)}`, block.metadata));
391
408
  }
409
+ function moveResourceMessagesToTail(messages, options, tailSegments, activeTailSegments) {
410
+ const resourceUris = new Set([
411
+ ...(options.resourceUris ?? []),
412
+ ...(options.attachments ?? []).flatMap((attachment) => attachment.uri !== undefined && attachment.text === undefined && attachment.content === undefined ? [attachment.uri] : []),
413
+ ]);
414
+ if (resourceUris.size === 0)
415
+ return messages;
416
+ return messages.filter((message) => {
417
+ const uri = message.metadata?.uri;
418
+ if (message.role !== "user" || typeof uri !== "string" || !resourceUris.has(uri))
419
+ return true;
420
+ appendTailSegment(tailSegments, activeTailSegments, `resource:${uri}`, message);
421
+ return false;
422
+ });
423
+ }
424
+ function appendSkillTailSegments(tailSegments, activeTailSegments, skills, disclosure, loaded, demotedSkillBodies) {
425
+ const renderContext = {
426
+ disclosure,
427
+ loaded,
428
+ demotedBodies: demotedSkillBodies?.length ? new Set(demotedSkillBodies) : undefined,
429
+ };
430
+ const selected = capSkillCatalog(selectSkillsForPrompt(skills ?? [], renderContext));
431
+ const byName = new Map(selected.map((skill) => [skill.name, skill]));
432
+ const ordered = disclosure === "eager"
433
+ ? selected
434
+ : (loaded?.list() ?? []).flatMap((name) => {
435
+ const skill = byName.get(name);
436
+ return skill === undefined ? [] : [skill];
437
+ });
438
+ for (const skill of ordered) {
439
+ if (!skillHasRenderableBody(skill, renderContext))
440
+ continue;
441
+ const message = buildSkillMessages([skill], renderContext)[0];
442
+ if (message)
443
+ appendTailSegment(tailSegments, activeTailSegments, `skill:${skill.name}`, message);
444
+ }
445
+ }
446
+ /** Map#set retains first-insertion order, so re-derivation replaces only this segment's bytes. */
447
+ function appendTailSegment(tailSegments, activeTailSegments, id, message) {
448
+ tailSegments.set(id, message);
449
+ activeTailSegments.add(id);
450
+ }
451
+ function tailMessages(tailSegments, activeTailSegments) {
452
+ return [...tailSegments].flatMap(([id, message]) => (activeTailSegments.has(id) ? [message] : []));
453
+ }
392
454
  function toolMessages(tools) {
393
455
  if (!tools?.length)
394
456
  return [];
@@ -1,5 +1,40 @@
1
- import type { ExtensionEvent } from "./contracts.js";
2
- export type MiddlewareHookName = "provider_request" | "input_assembly" | "prompt_build" | "context" | "tool_call" | "tool_result" | "retry" | "compaction" | "session_start" | "session_shutdown";
1
+ import type { ContentBlock, ExtensionEvent } from "./contracts.js";
2
+ export type MiddlewareHookName = "beforeProviderTurn" | "provider_request" | "input_assembly" | "prompt_build" | "context" | "tool_call" | "tool_result" | "retry" | "compaction_request" | "compaction" | "session_start" | "session_shutdown";
3
+ /** Provenance of a host-answered turn (plan 096). An id, never free host code. */
4
+ export interface DeterministicTurnProvenance {
5
+ /** Answering middleware id; bounded, replay-stable, and auditable. */
6
+ readonly middleware: string;
7
+ }
8
+ /** Host middleware answer that completes a turn without any provider request (plan 096). */
9
+ export interface DeterministicTurnAnswer {
10
+ /** Assistant content with provider content-block shape; tool calls are rejected (no provider ran). */
11
+ readonly content: readonly ContentBlock[];
12
+ /** Mandatory: a deterministic turn can never masquerade as model output. */
13
+ readonly provenance: DeterministicTurnProvenance;
14
+ }
15
+ /**
16
+ * Payload for the `beforeProviderTurn` hook (plan 096). Host middleware sets `answer` to complete the
17
+ * turn deterministically; leaving it absent/undefined sends the turn to the provider as usual.
18
+ */
19
+ export interface BeforeProviderTurnPayload {
20
+ readonly sessionId: string;
21
+ readonly runId: string;
22
+ readonly turn: number;
23
+ /** Concatenated text blocks of the latest user-role message in the assembled request. */
24
+ readonly userText: string;
25
+ /** Set by host middleware to answer without a provider call. */
26
+ readonly answer?: DeterministicTurnAnswer;
27
+ }
28
+ /** Host answered with a malformed deterministic turn. Fails the run closed — never falls through. */
29
+ export declare class DeterministicTurnError extends Error {
30
+ readonly code = "ERR_PRISM_DETERMINISTIC_TURN";
31
+ constructor(message: string);
32
+ }
33
+ /**
34
+ * Validate a host deterministic answer at the trust boundary: non-empty assistant-visible content and
35
+ * mandatory bounded provenance. Throws instead of degrading to a provider call.
36
+ */
37
+ export declare function validateDeterministicTurnAnswer(value: unknown): DeterministicTurnAnswer;
3
38
  export type MiddlewareNext<T> = (value: T) => Promise<T>;
4
39
  export type Middleware<T = unknown> = (value: T, next: MiddlewareNext<T>) => T | Promise<T>;
5
40
  export interface MiddlewareRegistryOptions {
@@ -1,4 +1,45 @@
1
1
  import { errorToErrorInfo } from "./redaction.js";
2
+ /** Host answered with a malformed deterministic turn. Fails the run closed — never falls through. */
3
+ export class DeterministicTurnError extends Error {
4
+ code = "ERR_PRISM_DETERMINISTIC_TURN";
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "DeterministicTurnError";
8
+ }
9
+ }
10
+ /** Provenance ids stay ids: bounded, no whitespace or separators outside the id alphabet. */
11
+ const DETERMINISTIC_MIDDLEWARE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
12
+ // Assistant-visible blocks only: a deterministic answer cannot smuggle tool execution/authority.
13
+ const DETERMINISTIC_BLOCK_TYPES = new Set(["text", "image", "audio", "file", "document", "video", "thinking"]);
14
+ /**
15
+ * Validate a host deterministic answer at the trust boundary: non-empty assistant-visible content and
16
+ * mandatory bounded provenance. Throws instead of degrading to a provider call.
17
+ */
18
+ export function validateDeterministicTurnAnswer(value) {
19
+ if (typeof value !== "object" || value === null)
20
+ throw new DeterministicTurnError("answer must be an object");
21
+ const answer = value;
22
+ if (!Array.isArray(answer.content) || answer.content.length === 0) {
23
+ throw new DeterministicTurnError("answer.content must be a non-empty content block array");
24
+ }
25
+ for (const block of answer.content) {
26
+ const type = block?.type;
27
+ if (typeof block !== "object" || block === null || typeof type !== "string" || !DETERMINISTIC_BLOCK_TYPES.has(type)) {
28
+ throw new DeterministicTurnError("answer.content blocks must be assistant-visible content blocks (text, image, audio, file, document, video, thinking)");
29
+ }
30
+ if (type === "text" && typeof block.text !== "string") {
31
+ throw new DeterministicTurnError("answer.content text blocks require a string text");
32
+ }
33
+ if (type === "thinking" && typeof block.text !== "string") {
34
+ throw new DeterministicTurnError("answer.content thinking blocks require a string text");
35
+ }
36
+ }
37
+ const middleware = answer.provenance?.middleware;
38
+ if (typeof middleware !== "string" || !DETERMINISTIC_MIDDLEWARE_ID.test(middleware)) {
39
+ throw new DeterministicTurnError("answer.provenance.middleware must be a bounded id (1-64 chars: letters, digits, . _ : -)");
40
+ }
41
+ return { content: answer.content, provenance: { middleware } };
42
+ }
2
43
  function middlewareError(error, hook, secrets) {
3
44
  return { type: "extension_error", extension: `middleware:${hook}`, error: errorToErrorInfo(error, secrets) };
4
45
  }
@@ -1,6 +1,7 @@
1
1
  import { appendFile, mkdir, readFile } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
- import { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, SessionSearchUnsupportedError, } from "../contracts.js";
3
+ import { isSessionEntryKind, SESSION_APPEND_CONFLICT_CODE, SESSION_ENTRY_SCHEMA_VERSION, SessionAppendConflictError, } from "../contracts.js";
4
+ import { searchLinearSessions } from "../session-stores.js";
4
5
  import { isNodeErrorCode } from "./config.js";
5
6
  export function createJsonlSessionStore(pathOrOptions) {
6
7
  const options = typeof pathOrOptions === "string" ? { path: pathOrOptions, createDirectory: true } : pathOrOptions;
@@ -50,8 +51,22 @@ export function createJsonlSessionStore(pathOrOptions) {
50
51
  async get(id) {
51
52
  return findEntry(path, id);
52
53
  },
53
- async searchSessions() {
54
- throw new SessionSearchUnsupportedError("JSONL session store does not support searchSessions");
54
+ async searchSessions(query) {
55
+ // ponytail: no index - every search reads and parses the file (O(corpus) time and memory), the
56
+ // recommended indexed paths are the SQLite/Postgres adapters. Corrupt lines are quarantined
57
+ // exactly as in list()/get(), and the contract linear caps bound entries/text scanned.
58
+ const { entries } = await readJsonlSessionEntries(path);
59
+ const bySession = new Map();
60
+ const leafBySession = new Map();
61
+ for (const entry of entries) {
62
+ const sessionEntries = bySession.get(entry.sessionId);
63
+ if (sessionEntries)
64
+ sessionEntries.push(entry);
65
+ else
66
+ bySession.set(entry.sessionId, [entry]);
67
+ leafBySession.set(entry.sessionId, entry.id);
68
+ }
69
+ return searchLinearSessions(bySession, leafBySession, query);
55
70
  },
56
71
  };
57
72
  }
@@ -1,9 +1,15 @@
1
+ import { createHash } from "node:crypto";
1
2
  export function createProviderTurnMetadata(request, providerId, fields = {}) {
3
+ const names = (request.tools ?? []).map((tool) => tool.name);
2
4
  return {
3
5
  providerId,
4
6
  model: request.model,
5
7
  requestId: readRequestId(request),
6
8
  ...fields,
9
+ tools: {
10
+ count: names.length,
11
+ idsHash: `sha256:${createHash("sha256").update(JSON.stringify(names), "utf8").digest("hex")}`,
12
+ },
7
13
  };
8
14
  }
9
15
  export function readProviderHttpStatus(error) {
@@ -1,4 +1,4 @@
1
- import type { ContentBlock, JsonObject, ProviderEvent, ToolCallContent, ToolCallDeltaContent, Usage } from "./contracts.js";
1
+ import type { ContentBlock, JsonObject, ProviderEvent, ProviderStopReason, ToolCallContent, ToolCallDeltaContent, Usage } from "./contracts.js";
2
2
  export declare function providerTextDelta(text: string): ProviderEvent;
3
3
  export declare function providerThinkingDelta(text: string, signature?: string): ProviderEvent;
4
4
  export declare function providerContentDelta(content: ContentBlock): ProviderEvent;
@@ -13,7 +13,13 @@ export declare function providerToolCallDeltaContent(delta: Omit<ToolCallDeltaCo
13
13
  export declare function providerContinuationRequired(cursor: string, reason?: string): ProviderEvent;
14
14
  export declare function reconstructToolCallDeltas(events: readonly ProviderEvent[]): readonly ToolCallContent[];
15
15
  export declare function providerUsage(usage: Usage): ProviderEvent;
16
- export declare function providerDone(usage?: Usage): ProviderEvent;
16
+ /**
17
+ * Map a native provider stop/finish reason onto the closed taxonomy. A missing, non-string, or
18
+ * unmapped value returns `"unknown"` rather than throwing, so a new wire value can never fail a
19
+ * run (plan 087 T1).
20
+ */
21
+ export declare function mapProviderStopReason(native: string | null | undefined): ProviderStopReason;
22
+ export declare function providerDone(usage?: Usage, stopReason?: ProviderStopReason): ProviderEvent;
17
23
  export declare function providerError(error: unknown, secrets?: readonly (string | undefined)[]): Extract<ProviderEvent, {
18
24
  type: "error";
19
25
  }>;
@@ -46,8 +46,66 @@ export function reconstructToolCallDeltas(events) {
46
46
  export function providerUsage(usage) {
47
47
  return { type: "usage", usage };
48
48
  }
49
- export function providerDone(usage) {
50
- return { type: "done", usage };
49
+ /**
50
+ * One shared native → taxonomy table (plan 087 T1). Every adapter routes its wire reason
51
+ * through `mapProviderStopReason`, so hosts switch on one closed union instead of per-provider
52
+ * strings. Keys are lowercased wire values; `unknown` never appears here — it is the fallback.
53
+ */
54
+ const PROVIDER_STOP_REASONS = Object.freeze({
55
+ // OpenAI Chat Completions (`finish_reason`) and generic OpenAI-compatible routes.
56
+ stop: "end_turn",
57
+ length: "max_output_tokens",
58
+ tool_calls: "tool_calls",
59
+ function_call: "tool_calls",
60
+ content_filter: "content_filter",
61
+ // Messages-style `stop_reason` routes and Bedrock Converse (`stopReason`).
62
+ end_turn: "end_turn",
63
+ stop_sequence: "end_turn",
64
+ pause_turn: "end_turn",
65
+ tool_use: "tool_calls",
66
+ max_tokens: "max_output_tokens",
67
+ refusal: "content_filter",
68
+ // Google generateContent (`finishReason`).
69
+ safety: "content_filter",
70
+ recitation: "content_filter",
71
+ blocklist: "content_filter",
72
+ prohibited_content: "content_filter",
73
+ spii: "content_filter",
74
+ image_safety: "content_filter",
75
+ language: "content_filter",
76
+ malformed_function_call: "provider_error",
77
+ unexpected_tool_call: "provider_error",
78
+ // OpenAI Responses (status / `incomplete_details.reason`).
79
+ completed: "end_turn",
80
+ failed: "provider_error",
81
+ cancelled: "abort",
82
+ canceled: "abort",
83
+ // Bedrock Converse guarded routes.
84
+ content_filtered: "content_filter",
85
+ guardrail_intervened: "content_filter",
86
+ malformed_model_output: "provider_error",
87
+ malformed_tool_use: "provider_error",
88
+ // AI SDK unified finish reasons (hyphenated).
89
+ "content-filter": "content_filter",
90
+ "tool-calls": "tool_calls",
91
+ error: "provider_error",
92
+ other: "unknown",
93
+ unknown: "unknown",
94
+ abort: "abort",
95
+ aborted: "abort",
96
+ });
97
+ /**
98
+ * Map a native provider stop/finish reason onto the closed taxonomy. A missing, non-string, or
99
+ * unmapped value returns `"unknown"` rather than throwing, so a new wire value can never fail a
100
+ * run (plan 087 T1).
101
+ */
102
+ export function mapProviderStopReason(native) {
103
+ if (typeof native !== "string")
104
+ return "unknown";
105
+ return PROVIDER_STOP_REASONS[native.trim().toLowerCase()] ?? "unknown";
106
+ }
107
+ export function providerDone(usage, stopReason) {
108
+ return { type: "done", usage, ...(stopReason === undefined ? {} : { stopReason }) };
51
109
  }
52
110
  export function providerError(error, secrets = []) {
53
111
  const info = errorToErrorInfo(error, secrets);
@@ -1,5 +1,5 @@
1
1
  import { resolveCredentialValue } from "../credentials.js";
2
- import { providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallFromArgumentsText, } from "../provider-events.js";
2
+ import { mapProviderStopReason, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallFromArgumentsText, } from "../provider-events.js";
3
3
  import { assertStructuredOutputRequestSupported } from "../structured-output.js";
4
4
  import { trimTrailingSlashes } from "../trim-trailing-slashes.js";
5
5
  import { applyOpenAIChatStructuredOutput, assertOpenAIChatMessage, mapOpenAIChatUsage, serializeOpenAIChatMessage, serializeOpenAITool, } from "./openai-primitives.js";
@@ -13,6 +13,7 @@ export async function* openAIChatEvents(body, options = {}) {
13
13
  let usage;
14
14
  let sawDoneMarker = false;
15
15
  let sawFinishReason = false;
16
+ let finishReason;
16
17
  for await (const sseEvent of readSseEvents(body, { signal: options.signal })) {
17
18
  if (options.onComment && sseEvent.comments?.length) {
18
19
  for (const text of sseEvent.comments) {
@@ -43,8 +44,10 @@ export async function* openAIChatEvents(body, options = {}) {
43
44
  yield providerUsage(mapped);
44
45
  }
45
46
  for (const choice of parsed.choices ?? []) {
46
- if (choice.finish_reason)
47
+ if (choice.finish_reason) {
47
48
  sawFinishReason = true;
49
+ finishReason = choice.finish_reason;
50
+ }
48
51
  const delta = choice.delta ?? {};
49
52
  if (typeof delta.content === "string" && delta.content)
50
53
  yield providerTextDelta(delta.content);
@@ -83,7 +86,7 @@ export async function* openAIChatEvents(body, options = {}) {
83
86
  for (const call of tools.values()) {
84
87
  yield providerToolCall(toolCallFromArgumentsText(call.id, call.name, call.argumentsText));
85
88
  }
86
- yield providerDone((options.strictCompletion ?? true) || options.doneUsage ? usage : undefined);
89
+ yield providerDone((options.strictCompletion ?? true) || options.doneUsage ? usage : undefined, finishReason === undefined ? undefined : mapProviderStopReason(finishReason));
87
90
  }
88
91
  export function createOpenAICompatibleProvider(options) {
89
92
  const providerId = options.id ?? "openai-compatible";
@@ -1,4 +1,4 @@
1
- import type { Agent, AgentSessionConfig, GuardrailStage, RunOptions } from "./contracts.js";
1
+ import type { Agent, AgentSessionConfig, GuardrailPackRef, GuardrailStage, RunOptions } from "./contracts.js";
2
2
  /** Report format revision. Any shape change bumps this so pinned digests cannot compare across formats. */
3
3
  export declare const RUN_BUNDLE_SCHEMA_VERSION = 1;
4
4
  /** Inspectable projection of the inputs a run actually resolves to. Frozen JSON, safe to persist and diff. */
@@ -81,6 +81,11 @@ export interface RunBundleSnapshotInput {
81
81
  readonly run?: RunOptions;
82
82
  /** Optional memory store instance; only its kind/durability label is read, never its contents. */
83
83
  readonly memory?: unknown;
84
+ /**
85
+ * Plan 104 Task 2: effective pack refs (`session.guardrailPackRefs`) to report instead of the
86
+ * caller-supplied `config.guardrailPacks` — a resumed session's enforced rows.
87
+ */
88
+ readonly packs?: readonly GuardrailPackRef[];
84
89
  }
85
90
  /**
86
91
  * Snapshots the effective run bundle: synchronous, in-memory, zero network and zero store reads.
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { agentFingerprint, BUILT_IN_LOOP_REVISIONS } from "./agent-run-state.js";
3
+ import { describeGuardrailPacks } from "./guardrails.js";
3
4
  import { describeStorage } from "./host-composition.js";
4
5
  import { canonicalizeJsonSchema } from "./providers/schema.js";
5
6
  import { resolveRunLimits } from "./run-limits.js";
@@ -51,7 +52,10 @@ export function snapshotRunBundle(input) {
51
52
  effect: tool.effect === undefined ? null : typeof tool.effect === "function" ? "classifier" : tool.effect.kind,
52
53
  })),
53
54
  activeSkills: run?.activeSkills ?? null,
54
- guardrails: guardrailRows(config.guardrails, run?.guardrails),
55
+ guardrails: [
56
+ ...guardrailRows(config.guardrails, run?.guardrails),
57
+ ...describeGuardrailPacks(input.packs ?? input.config?.guardrailPacks),
58
+ ],
55
59
  loop: loopIdentity(effectiveLoop),
56
60
  thinkingLevel: run?.thinkingLevel ?? config.thinkingLevel ?? null,
57
61
  limits: resolveRunLimits(config.limits, run?.limits),
@@ -1,4 +1,4 @@
1
- import type { ResolvedRunLimits, RunLimitBreach, RunLimitCounters, RunLimitName, RunLimits, Usage } from "./contracts.js";
1
+ import type { BudgetAxisUsage, BudgetConsumedCounters, ResolvedRunLimits, RunLimitBreach, RunLimitCounters, RunLimitName, RunLimits, ToolCallSummary, Usage } from "./contracts.js";
2
2
  export declare const DEFAULT_RUN_LIMITS: Readonly<{
3
3
  maxTurns: 16;
4
4
  maxProviderAttempts: 24;
@@ -51,3 +51,13 @@ export declare class RunLimitTracker {
51
51
  private exceed;
52
52
  }
53
53
  export declare function createRunLimitTracker(limits: RunLimits | undefined, options?: RunLimitTrackerOptions): RunLimitTracker;
54
+ /**
55
+ * Build the `budget_exhausted` payload (plan 087 T2): which axis fired, the counters a host reads
56
+ * first, the closest other axes, and the last dispatched tool calls (hashes only).
57
+ */
58
+ export declare function describeBudgetExhaustion(tracker: RunLimitTracker, breach: RunLimitBreach, recentToolCalls: readonly ToolCallSummary[]): {
59
+ limit: RunLimitName;
60
+ consumed: BudgetConsumedCounters;
61
+ closestOtherAxes: BudgetAxisUsage[];
62
+ recentToolCalls: ToolCallSummary[];
63
+ };
@@ -10,6 +10,8 @@ export const DEFAULT_RUN_LIMITS = Object.freeze({
10
10
  maxOutputTokens: 10_000,
11
11
  maxTotalTokens: 50_000,
12
12
  });
13
+ /** Stop-hook continuation cap when no layer configures one (plan 106 R1). Not a counter axis. */
14
+ const DEFAULT_MAX_STOP_CONTINUATIONS = 3;
13
15
  /**
14
16
  * Process-safety ceilings that exist so a bug cannot OOM the host via JSON.parse of giant
15
17
  * provider frames. Product axes (turns, wall time, tokens, …) have no hard cap: hosts set
@@ -83,8 +85,13 @@ export function resolveRunLimits(agent, run) {
83
85
  resolved.maxProviderAttempts = turns;
84
86
  }
85
87
  const maxCost = override?.maxCost ?? base?.maxCost;
88
+ // Stop-hook continuation cap (plan 106 R1): no counters-table row — the wrapper turns it into a
89
+ // clean `hook_limit` stop, not a breach — so it resolves outside the counter-backed axes and
90
+ // keeps the same narrowing-only law (min, `null` = uncapped).
91
+ const stopContinuations = minCap(base?.maxStopContinuations, override?.maxStopContinuations);
86
92
  return Object.freeze({
87
93
  ...resolved,
94
+ maxStopContinuations: stopContinuations !== undefined ? stopContinuations : DEFAULT_MAX_STOP_CONTINUATIONS,
88
95
  ...(maxCost
89
96
  ? {
90
97
  maxCost: base?.maxCost && override?.maxCost
@@ -121,6 +128,12 @@ function validateLimits(input) {
121
128
  if (!Number.isFinite(amount) || amount < 0 || !currency.trim())
122
129
  throw new TypeError("maxCost requires a finite non-negative amount and currency");
123
130
  }
131
+ const stopContinuations = input.maxStopContinuations;
132
+ if (stopContinuations !== undefined &&
133
+ stopContinuations !== null &&
134
+ (!Number.isSafeInteger(stopContinuations) || stopContinuations < 0)) {
135
+ throw new TypeError("maxStopContinuations must be a non-negative safe integer or null to disable the cap");
136
+ }
124
137
  return input;
125
138
  }
126
139
  export class RunLimitTracker {
@@ -263,4 +276,50 @@ export class RunLimitTracker {
263
276
  export function createRunLimitTracker(limits, options) {
264
277
  return new RunLimitTracker(resolveRunLimits(undefined, limits), options);
265
278
  }
279
+ /** Product axes reported as "how close was everything else" (plan 087 T2). Request/response bytes
280
+ * stay out: their caps are per-frame, so a run-lifetime ratio would be meaningless. */
281
+ const ATTRIBUTION_AXES = [
282
+ { axis: "maxTurns", counter: "turns" },
283
+ { axis: "maxProviderAttempts", counter: "providerAttempts" },
284
+ { axis: "maxToolRounds", counter: "toolRounds" },
285
+ { axis: "maxToolCalls", counter: "toolCalls" },
286
+ { axis: "maxWallTimeMs", counter: "wallTimeMs" },
287
+ { axis: "maxInputTokens", counter: "inputTokens" },
288
+ { axis: "maxOutputTokens", counter: "outputTokens" },
289
+ { axis: "maxTotalTokens", counter: "totalTokens" },
290
+ { axis: "maxCost", counter: "cost" },
291
+ ];
292
+ const CLOSEST_AXIS_COUNT = 3;
293
+ function axisCap(limits, axis) {
294
+ if (axis === "maxCost")
295
+ return limits.maxCost?.amount;
296
+ const value = limits[axis];
297
+ return typeof value === "number" ? value : null;
298
+ }
299
+ /**
300
+ * Build the `budget_exhausted` payload (plan 087 T2): which axis fired, the counters a host reads
301
+ * first, the closest other axes, and the last dispatched tool calls (hashes only).
302
+ */
303
+ export function describeBudgetExhaustion(tracker, breach, recentToolCalls) {
304
+ const counters = tracker.snapshot();
305
+ const closestOtherAxes = ATTRIBUTION_AXES.filter(({ axis }) => axis !== breach.limit)
306
+ .flatMap(({ axis, counter }) => {
307
+ const cap = axisCap(tracker.limits, axis);
308
+ return typeof cap === "number" && cap > 0 ? [{ axis, cap, used: counters[counter] }] : [];
309
+ })
310
+ .map(({ axis, cap, used }) => ({ axis, usedRatio: Math.round(Math.min(1, used / cap) * 10_000) / 10_000 }))
311
+ .sort((a, b) => b.usedRatio - a.usedRatio)
312
+ .slice(0, CLOSEST_AXIS_COUNT);
313
+ return {
314
+ limit: breach.limit,
315
+ consumed: {
316
+ turns: counters.turns,
317
+ inputTokens: counters.inputTokens,
318
+ providerAttempts: counters.providerAttempts,
319
+ requestBytes: counters.requestBytes,
320
+ },
321
+ closestOtherAxes,
322
+ recentToolCalls: [...recentToolCalls],
323
+ };
324
+ }
266
325
  //# sourceMappingURL=run-limits.js.map
@@ -1,4 +1,4 @@
1
- import { type BranchReader, type Message, type SessionBranchRead, type SessionEntry, type SessionStore } from "./contracts.js";
1
+ import { type BranchReader, type Message, type PersistencePage, type SessionBranchRead, type SessionEntry, type SessionSearchHit, type SessionSearchQuery, type SessionStore } from "./contracts.js";
2
2
  export interface CreateSessionEntryOptions extends Omit<SessionEntry, "id" | "timestamp"> {
3
3
  readonly id?: string;
4
4
  readonly timestamp?: string;
@@ -40,4 +40,15 @@ export interface CreateMemorySessionStoreOptions {
40
40
  readonly maxLinearBytes?: number;
41
41
  };
42
42
  }
43
+ /** Resolved bounds for the capped linear session scan. */
44
+ export interface LinearSearchCaps {
45
+ readonly sessions: number;
46
+ readonly entries: number;
47
+ readonly bytes: number;
48
+ }
43
49
  export declare function createMemorySessionStore(initialEntries?: readonly SessionEntry[], options?: CreateMemorySessionStoreOptions): SessionStore;
50
+ /**
51
+ * Shared linear (unindexed) session search over already-grouped entries: the memory store's default
52
+ * mode and the JSONL store's implementation. `caps` defaults to the contract linear caps.
53
+ */
54
+ export declare function searchLinearSessions(bySession: Map<string, SessionEntry[]>, leafBySession: Map<string, string>, query: SessionSearchQuery, caps?: LinearSearchCaps): PersistencePage<SessionSearchHit>;
@@ -133,7 +133,7 @@ export function createMemorySessionStore(initialEntries = [], options = {}) {
133
133
  async searchSessions(query) {
134
134
  if (mode === "unsupported")
135
135
  throw new SessionSearchUnsupportedError();
136
- return searchMemorySessionsLinear(bySession, leafBySession, query, searchCaps);
136
+ return searchLinearSessions(bySession, leafBySession, query, searchCaps);
137
137
  },
138
138
  };
139
139
  function add(entry, options) {
@@ -175,7 +175,11 @@ export function createMemorySessionStore(initialEntries = [], options = {}) {
175
175
  leafBySession.set(entry.sessionId, entry.id);
176
176
  }
177
177
  }
178
- function searchMemorySessionsLinear(bySession, leafBySession, query, caps) {
178
+ /**
179
+ * Shared linear (unindexed) session search over already-grouped entries: the memory store's default
180
+ * mode and the JSONL store's implementation. `caps` defaults to the contract linear caps.
181
+ */
182
+ export function searchLinearSessions(bySession, leafBySession, query, caps = resolveLinearSearchCaps(undefined)) {
179
183
  const q = resolveSessionSearchQuery(query);
180
184
  q.signal?.throwIfAborted();
181
185
  let sessionsScanned = 0;
@@ -203,13 +207,20 @@ function searchMemorySessionsLinear(bySession, leafBySession, query, caps) {
203
207
  let matchedQuery = false;
204
208
  let matchedProvider = false;
205
209
  let matchedModel = false;
210
+ let matchedKind = false;
211
+ let matchedEntry;
206
212
  let snippetSource;
213
+ let turn = 0;
207
214
  for (const entry of entries) {
208
215
  if (entriesScanned >= caps.entries)
209
216
  break;
210
217
  if (bytesScanned >= caps.bytes)
211
218
  break;
212
219
  entriesScanned += 1;
220
+ turn += 1;
221
+ const kindMatches = q.kind === undefined || q.kind.includes(entry.kind);
222
+ if (kindMatches)
223
+ matchedKind = true;
213
224
  const text = entrySearchText(entry);
214
225
  bytesScanned += utf8Bytes(text) + utf8Bytes(entry.label) + utf8Bytes(entry.summary);
215
226
  if (entry.timestamp > updatedAt)
@@ -234,11 +245,12 @@ function searchMemorySessionsLinear(bySession, leafBySession, query, caps) {
234
245
  matchedLabel = true;
235
246
  if (q.summary && entry.summary?.includes(q.summary))
236
247
  matchedSummary = true;
237
- if (q.query) {
248
+ if (q.query && kindMatches) {
238
249
  const hay = `${entry.label ?? ""}\n${entry.summary ?? ""}\n${text}`;
239
250
  if (hay.includes(q.query)) {
240
251
  matchedQuery = true;
241
- snippetSource ??= entry.label ?? entry.summary ?? text;
252
+ matchedEntry ??= { entry, turn };
253
+ snippetSource ??= [entry.label, entry.summary, text].find((part) => part?.includes(q.query)) ?? hay;
242
254
  }
243
255
  }
244
256
  if (q.provider && (entry.model?.provider === q.provider || metaProvider(entry) === q.provider))
@@ -260,6 +272,8 @@ function searchMemorySessionsLinear(bySession, leafBySession, query, caps) {
260
272
  continue;
261
273
  if (q.query && !matchedQuery)
262
274
  continue;
275
+ if (q.kind && (q.query ? matchedEntry === undefined : !matchedKind))
276
+ continue;
263
277
  if (q.provider && !matchedProvider)
264
278
  continue;
265
279
  if (q.model && !matchedModel)
@@ -271,6 +285,9 @@ function searchMemorySessionsLinear(bySession, leafBySession, query, caps) {
271
285
  matches.push({
272
286
  sessionId,
273
287
  leafId: leafBySession.get(sessionId),
288
+ entryId: matchedEntry?.entry.id,
289
+ runId: matchedEntry?.entry.runId,
290
+ turn: matchedEntry?.turn,
274
291
  updatedAt: updatedAt || undefined,
275
292
  label,
276
293
  summary,