@arnilo/prism 0.0.15 → 0.0.17

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 (119) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/agent-definitions.js +2 -3
  3. package/dist/agent-loops.js +12 -7
  4. package/dist/agent-run-lifecycle.d.ts +1 -2
  5. package/dist/agent-run-lifecycle.js +1 -1
  6. package/dist/agent-run-state.js +29 -4
  7. package/dist/agents.d.ts +1 -1
  8. package/dist/agents.js +163 -61
  9. package/dist/cache-helpers.js +18 -9
  10. package/dist/checkpoints.d.ts +4 -0
  11. package/dist/checkpoints.js +17 -9
  12. package/dist/cli-init.js +3 -7
  13. package/dist/cli-runner.d.ts +2 -6
  14. package/dist/cli-runner.js +71 -33
  15. package/dist/compaction.js +5 -4
  16. package/dist/config.js +7 -4
  17. package/dist/content.js +26 -24
  18. package/dist/context-budget.js +18 -11
  19. package/dist/contracts.d.ts +16 -3
  20. package/dist/contracts.js +4 -1
  21. package/dist/contribution-parsing.js +6 -2
  22. package/dist/contributions.d.ts +2 -0
  23. package/dist/contributions.js +3 -0
  24. package/dist/conversations.js +2 -1
  25. package/dist/credentials.d.ts +8 -2
  26. package/dist/credentials.js +9 -3
  27. package/dist/event-multiplexer.js +18 -4
  28. package/dist/extensions.d.ts +7 -1
  29. package/dist/extensions.js +64 -6
  30. package/dist/feedback.js +12 -10
  31. package/dist/guardrails.d.ts +1 -1
  32. package/dist/guardrails.js +26 -17
  33. package/dist/identity.js +10 -2
  34. package/dist/index.d.ts +82 -83
  35. package/dist/index.js +42 -42
  36. package/dist/input.d.ts +2 -2
  37. package/dist/input.js +50 -27
  38. package/dist/instruction-injection.d.ts +1 -1
  39. package/dist/middleware.js +9 -1
  40. package/dist/models.d.ts +2 -0
  41. package/dist/models.js +3 -0
  42. package/dist/node/agent-definitions.js +16 -8
  43. package/dist/node/contribution-discovery.d.ts +1 -2
  44. package/dist/node/contribution-discovery.js +3 -3
  45. package/dist/node/session-store-jsonl.js +10 -7
  46. package/dist/node/settings.d.ts +1 -1
  47. package/dist/node/settings.js +1 -1
  48. package/dist/node/system-project-prompts.js +2 -4
  49. package/dist/node/trust.js +1 -1
  50. package/dist/persistence-lifecycle.js +1 -3
  51. package/dist/provider-events.js +3 -1
  52. package/dist/provider-request-policy.js +3 -4
  53. package/dist/providers/media.d.ts +1 -1
  54. package/dist/providers/openai-compatible.d.ts +42 -1
  55. package/dist/providers/openai-compatible.js +110 -49
  56. package/dist/providers/openai-primitives.js +7 -7
  57. package/dist/providers/transport.d.ts +6 -0
  58. package/dist/providers/transport.js +21 -0
  59. package/dist/providers.d.ts +2 -0
  60. package/dist/providers.js +3 -0
  61. package/dist/redaction.d.ts +1 -0
  62. package/dist/redaction.js +26 -9
  63. package/dist/resources.d.ts +2 -2
  64. package/dist/resources.js +2 -2
  65. package/dist/retry.d.ts +5 -0
  66. package/dist/retry.js +8 -1
  67. package/dist/rpc.js +42 -9
  68. package/dist/run-ledger.d.ts +6 -0
  69. package/dist/run-ledger.js +16 -13
  70. package/dist/run-limits.js +49 -10
  71. package/dist/secure-agent.js +1 -1
  72. package/dist/security.js +7 -2
  73. package/dist/session-stores.d.ts +1 -1
  74. package/dist/session-stores.js +28 -24
  75. package/dist/structured-output.js +2 -2
  76. package/dist/system-prompts.js +7 -2
  77. package/dist/testing/compaction-conformance.js +5 -1
  78. package/dist/testing/extension-conformance.js +15 -3
  79. package/dist/testing/feedback.d.ts +1 -3
  80. package/dist/testing/feedback.js +1 -1
  81. package/dist/testing/persistence-schema.js +206 -37
  82. package/dist/testing/provider-conformance.js +3 -3
  83. package/dist/testing/run-ledger-conformance.js +1 -1
  84. package/dist/testing/session-store-conformance.js +1 -1
  85. package/dist/testing/tool-conformance.js +30 -5
  86. package/dist/thinking.js +4 -1
  87. package/dist/tools.d.ts +2 -2
  88. package/dist/tools.js +24 -5
  89. package/docs/0.1.0-readiness.md +139 -0
  90. package/docs/agent-events.md +2 -1
  91. package/docs/agent-session-runtime.md +2 -2
  92. package/docs/cli-rpc.md +1 -5
  93. package/docs/coding-agent-tools.md +2 -0
  94. package/docs/compaction-and-retry.md +3 -1
  95. package/docs/contribution-registries.md +1 -0
  96. package/docs/credentials-and-redaction.md +1 -1
  97. package/docs/extensions.md +1 -1
  98. package/docs/guardrails.md +13 -2
  99. package/docs/index.md +4 -2
  100. package/docs/input-and-prompt-assembly.md +3 -3
  101. package/docs/middleware-hooks.md +2 -2
  102. package/docs/migration.md +40 -0
  103. package/docs/performance.md +33 -0
  104. package/docs/providers/openai-compatible.md +28 -1
  105. package/docs/public-contracts.md +2 -2
  106. package/docs/release-and-install.md +127 -17
  107. package/docs/session-stores.md +1 -1
  108. package/package.json +14 -6
  109. package/docs/review-coverage-2026-07-14.md +0 -260
  110. package/docs/review-coverage-2026-07-15.md +0 -193
  111. package/docs/review-coverage-2026-07-17-provider-validation.md +0 -192
  112. package/docs/review-coverage-2026-07-19-phase-3.md +0 -174
  113. package/docs/review-coverage-2026-07-20-phase-4.md +0 -175
  114. package/docs/review-coverage-2026-07-21-phase-5.md +0 -172
  115. package/docs/review-coverage-2026-07-22-phase-6.md +0 -209
  116. package/docs/review-coverage-2026-07-22-phase-7.md +0 -173
  117. package/docs/review-coverage-2026-07-23-phase-8.md +0 -245
  118. package/docs/review-coverage-2026-07-25-phase-9.md +0 -256
  119. package/docs/review-coverage-2026-07-26-phase-10.md +0 -132
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.17] - 2026-07-29
4
+
5
+ ### Added
6
+ - Extension lifecycle: `ExtensionKernel.load()` returns `LoadedExtension[]` dispose handles; contribution/provider/model registries gain `unregister(...)`; a failed `setup` unwinds its partial registrations.
7
+ - `MemoryCredentialStoreOptions.allowProviderFallback` for strict provider-scoped credential resolution; `createMemoryCheckpointStore` `maxRecords`/`maxValueBytes` bounds; `ShellToolOptions.envAllowlist` (coding-agent); `ErrorInfo.retryAfterMs` plus `retryAfterMs`-aware `createDefaultRetryPolicy` with `jitter`/`random` options; guardrail `steer_rejected` event; `httpStatusError` provider transport helper wired into anthropic, google, kimi, openai, opencode-go, and the shared OpenAI-compatible transport.
8
+
9
+ ### Changed
10
+ - Durable runs: run-state load now bounds against the 1 MiB hard cap (states saved with a raised `maxStateBytes` resume correctly); agent fingerprint also covers instructions, system-prompt contributions, and skills; resume-after-interrupt is explicit implicit-approval.
11
+ - Retry/backpressure: HTTP provider errors carry numeric codes and `Retry-After` hints; default retry policy applies ±25% jitter.
12
+ - `input_assembly` middleware runs unconditionally (both plain and context-budget paths, any `InputBuilder`); memory session store rejects cross-session `expectedParentId`; context-budget eviction is O(n) instead of O(n²).
13
+ - Guardrails: `interrupt` errors name the stage; `guardrail_failed` records carry the underlying error message in `metadata.error`; steer `block`/`tripwire` drops the message and emits `steer_rejected` instead of failing the run.
14
+ - Default prompt builder omits the `Available tools:` text for tool-capable models (`capabilities.tools === true`).
15
+ - Middleware registry throws on double `next()` and diagnoses conflicting `next(v)` + return; event multiplexer keeps sorted delivery while a consumer is parked; batched run-ledger dead counters removed.
16
+
17
+ ### Breaking (minor, pre-1.0)
18
+ - CLI: `--config`, `--resource`, `--extension`, `--tool` are rejected (`<flag> is not supported in this build`); the dead `CliOptions.config/resources/extensions/tools` fields are removed.
19
+ - `ExtensionKernel.load()` resolves to `LoadedExtension[]` instead of `void`.
20
+
21
+ See [docs/migration.md](docs/migration.md) for the full 0.0.16 → 0.0.17 notes.
22
+
23
+ ## [0.0.16] - 2026-07-26
24
+
25
+ ### Added
26
+ - Phase 11 simplification/readiness: new public export `resolveRedactor` from `@arnilo/prism` (single survivor of four private copies across evals/memory/rag/workflows) and a new internal `@arnilo/prism-session-store-codecs` package (shared SQLite/Postgres row codecs, not enrolled in any profile family), bringing the exact graph to **44 publishable manifests**.
27
+ - Offline pre-publish release gates: `npm run release:gate` (API-surface `.d.ts` diff vs `scripts/compat-baseline/`, tarball deny-list, exact version ranges), wired into `npm run sdk:ready`.
28
+ - Performance budgets in `scripts/budgets.json`, enforced by `scripts/budget-gate.test.mjs` (in `npm test`) and `scripts/benchmark-0.0.16.mjs`.
29
+
30
+ ### Changed
31
+ - Dropped historical `docs/review-coverage-*.md` from the root tarball (11 files, ~283 KB): packed size 659,478 → ≈575,680 bytes, 281 → 270 files.
32
+ - All six profiles (`prism-all`, `prism-base`, `prism-code`, `prism-compaction`, `prism-providers`, `prism-sdk`) retained on adoption evidence; zero retirements. No runtime behavior changes.
33
+
3
34
  ## [0.0.15] - 2026-07-26
4
35
 
5
36
  ### Added
@@ -1,5 +1,5 @@
1
1
  import { createAgent } from "./agents.js";
2
- import { resolveActiveSkills, createSkillRegistry } from "./skills.js";
2
+ import { createSkillRegistry, resolveActiveSkills } from "./skills.js";
3
3
  import { createToolRegistry } from "./tools.js";
4
4
  /** Resolve an {@link AgentDefinition} into a runnable {@link Agent}.
5
5
  *
@@ -96,8 +96,7 @@ function hasList(value) {
96
96
  return typeof value === "object" && value !== null && "list" in value && typeof value.list === "function";
97
97
  }
98
98
  function resolveSkills(names, tools, context) {
99
- const registry = context.skillsRegistry ??
100
- (context.registries?.skills ? createSkillRegistry(context.registries.skills.list()) : undefined);
99
+ const registry = context.skillsRegistry ?? (context.registries?.skills ? createSkillRegistry(context.registries.skills.list()) : undefined);
101
100
  if (!names && !context.activateAllCapabilities)
102
101
  return undefined;
103
102
  if (!registry) {
@@ -1,5 +1,5 @@
1
- import { inputMessages } from "./input.js";
2
1
  import { createId } from "./ids.js";
2
+ import { inputMessages } from "./input.js";
3
3
  import { artifactStructuredOutputRequest, withoutStructuredOutput } from "./structured-output.js";
4
4
  function throwIfAborted(signal) {
5
5
  if (signal.aborted)
@@ -8,7 +8,10 @@ function throwIfAborted(signal) {
8
8
  function toolResultMessage(result) {
9
9
  return {
10
10
  role: "tool",
11
- content: [{ type: "tool_result", toolCallId: result.toolCallId, name: result.name, result: result.value, error: result.error }, ...(result.content ?? [])],
11
+ content: [
12
+ { type: "tool_result", toolCallId: result.toolCallId, name: result.name, result: result.value, error: result.error },
13
+ ...(result.content ?? []),
14
+ ],
12
15
  metadata: result.metadata,
13
16
  };
14
17
  }
@@ -121,7 +124,11 @@ export function generateValidateReviseLoop(opts) {
121
124
  continue;
122
125
  }
123
126
  if (toolRounds >= ctx.maxToolRounds) {
124
- const result = { ok: false, errors: [{ message: "maximum tool rounds exceeded" }], metadata: { reason: "tool_round_limit" } };
127
+ const result = {
128
+ ok: false,
129
+ errors: [{ message: "maximum tool rounds exceeded" }],
130
+ metadata: { reason: "tool_round_limit" },
131
+ };
125
132
  ctx.emit({ type: "artifact_failed", sessionId: ctx.sessionId, runId: ctx.runId, turn, attempt: attempts + 1, result });
126
133
  return usage;
127
134
  }
@@ -166,7 +173,7 @@ export function generateValidateReviseLoop(opts) {
166
173
  : undefined;
167
174
  const attempt = ++attempts;
168
175
  ctx.emit({ type: "artifact_validation_started", sessionId: ctx.sessionId, runId: ctx.runId, turn, attempt });
169
- const result = parseFailure ?? await opts.validator(parsed.value, artifactCtx);
176
+ const result = parseFailure ?? (await opts.validator(parsed.value, artifactCtx));
170
177
  ctx.emit({ type: "artifact_validation_finished", sessionId: ctx.sessionId, runId: ctx.runId, turn, attempt, result });
171
178
  if (result.ok) {
172
179
  ctx.emit({ type: "artifact_finished", sessionId: ctx.sessionId, runId: ctx.runId, turn, attempt, result });
@@ -213,9 +220,7 @@ export async function dispatchToolCallsInOrder(calls, ctx) {
213
220
  if (calls.length === 0)
214
221
  return;
215
222
  ctx.chargeToolRound?.(calls);
216
- const concurrency = calls.some((call) => ctx.isToolCallExclusive?.(call))
217
- ? 1
218
- : Math.max(1, Math.min(ctx.toolConcurrency, calls.length));
223
+ const concurrency = calls.some((call) => ctx.isToolCallExclusive?.(call)) ? 1 : Math.max(1, Math.min(ctx.toolConcurrency, calls.length));
219
224
  if (concurrency === 1) {
220
225
  for (const call of calls) {
221
226
  const result = await ctx.dispatchToolCall(call);
@@ -1,5 +1,4 @@
1
- import type { Agent, AgentEvent, AgentRunRef, AgentRunResult, AgentRunResume, AgentRunStatusResult, OwnershipScope, SubscribeOptions } from "./contracts.js";
2
- import type { CheckpointStore } from "./contracts.js";
1
+ import type { Agent, AgentEvent, AgentRunRef, AgentRunResult, AgentRunResume, AgentRunStatusResult, CheckpointStore, OwnershipScope, SubscribeOptions } from "./contracts.js";
3
2
  export interface AgentRunLifecycleAgent {
4
3
  readonly agent: Agent;
5
4
  /** Current host-authored revision; it must match the stored revision. */
@@ -1,6 +1,6 @@
1
- import { AgentRunStateError } from "./contracts.js";
2
1
  import { loadAgentRunState, publicState } from "./agent-run-state.js";
3
2
  import { resumeAgentRun, resumeAgentRunStream } from "./agents.js";
3
+ import { AgentRunStateError } from "./contracts.js";
4
4
  function assertAgentId(actual, expected) {
5
5
  if (expected !== undefined && actual !== expected)
6
6
  throw new AgentRunStateError("Agent run capability mismatch");
@@ -9,19 +9,30 @@ const MAX_PROPERTIES = 256;
9
9
  export function agentFingerprint(agent, revision) {
10
10
  const config = agent.config;
11
11
  const tools = !config.tools ? [] : "list" in config.tools ? config.tools.list() : config.tools;
12
+ const skills = !config.skills ? [] : "list" in config.skills ? config.skills.list() : config.skills;
12
13
  const guardrails = [
13
14
  ...(config.guardrails?.input ?? []),
14
15
  ...(config.guardrails?.output ?? []),
15
16
  ...(config.guardrails?.toolInput ?? []),
16
17
  ...(config.guardrails?.toolOutput ?? []),
17
18
  ];
19
+ const systemPrompt = config.systemPrompt === false || config.systemPrompt === undefined
20
+ ? (config.systemPrompt ?? null)
21
+ : (Array.isArray(config.systemPrompt) ? config.systemPrompt : [config.systemPrompt]).map((c) => ({ id: c.id, text: c.text }));
18
22
  const value = JSON.stringify({
19
23
  id: config.id ?? config.name ?? "agent",
20
24
  revision,
21
25
  model: config.model,
26
+ // Instructions/prompt text shapes agent behavior as much as the tool set; a change
27
+ // without a definitionRevision bump must not resume stale durable runs silently.
28
+ instructions: config.instructions ?? null,
29
+ systemPrompt,
30
+ skills: skills.map((skill) => ({ name: skill.name, instructions: skill.instructions, toolNames: skill.toolNames })),
22
31
  tools: tools.map((tool) => ({ name: tool.name, parameters: tool.parameters, exclusive: tool.exclusive })),
23
32
  guardrails: guardrails.map((guardrail) => ({ name: guardrail.name, stage: guardrail.stage, revision: guardrail.revision })),
24
- loop: typeof config.loop === "object" && config.loop && "strategy" in config.loop ? config.loop.strategy : config.loop?.name ?? "single-shot",
33
+ loop: typeof config.loop === "object" && config.loop && "strategy" in config.loop
34
+ ? config.loop.strategy
35
+ : (config.loop?.name ?? "single-shot"),
25
36
  });
26
37
  return createHash("sha256").update(value).digest("hex");
27
38
  }
@@ -43,7 +54,10 @@ export async function loadAgentRunState(checkpoints, ref, ownership) {
43
54
  const record = await checkpoints.loadCheckpoint({ namespace: AGENT_RUN_STATE_NAMESPACE, key: ref.runId, ...ownership });
44
55
  if (!record)
45
56
  throw new AgentRunStateError(`No durable agent run ${ref.runId}`);
46
- if (ref.sessionId && record.value && typeof record.value === "object" && record.value.sessionId !== ref.sessionId) {
57
+ if (ref.sessionId &&
58
+ record.value &&
59
+ typeof record.value === "object" &&
60
+ record.value.sessionId !== ref.sessionId) {
47
61
  throw new AgentRunStateError("Agent run session mismatch");
48
62
  }
49
63
  return { record, state: parseAgentRunState(record.value, record.version) };
@@ -95,10 +109,21 @@ export function parseAgentRunState(value, version) {
95
109
  const state = value;
96
110
  if (state.schemaVersion !== AGENT_RUN_STATE_SCHEMA_VERSION)
97
111
  throw new AgentRunStateError(`Unsupported agent run state schemaVersion ${String(state.schemaVersion)}`);
98
- if (!state.agentId || !state.definitionRevision || !state.fingerprint || !state.runId || !state.sessionId || !state.model || !state.status || !state.counters || !state.deadlineAt) {
112
+ if (!state.agentId ||
113
+ !state.definitionRevision ||
114
+ !state.fingerprint ||
115
+ !state.runId ||
116
+ !state.sessionId ||
117
+ !state.model ||
118
+ !state.status ||
119
+ !state.counters ||
120
+ !state.deadlineAt) {
99
121
  throw new AgentRunStateError("Malformed agent run state");
100
122
  }
101
- return boundState({ ...state, version }, DEFAULT_MAX_AGENT_RUN_STATE_BYTES);
123
+ // Load bounds against the hard cap, not the default: the configured maxStateBytes is a
124
+ // save-side policy knob, while the load-side bound is only a DoS ceiling. States saved
125
+ // with a raised maxStateBytes must remain resumable.
126
+ return boundState({ ...state, version }, HARD_MAX_AGENT_RUN_STATE_BYTES);
102
127
  }
103
128
  function boundState(state, maxBytes) {
104
129
  checkShape(state, 0);
package/dist/agents.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Agent, AgentConfig, AgentEvent, AgentRunResult, AgentRunResume, AgentRunResumeOptions, AgentRunResumeStreamOptions, AgentRunRef, AgentSession, AgentSessionConfig } from "./contracts.js";
1
+ import type { Agent, AgentConfig, AgentEvent, AgentRunRef, AgentRunResult, AgentRunResume, AgentRunResumeOptions, AgentRunResumeStreamOptions, AgentSession, AgentSessionConfig } from "./contracts.js";
2
2
  export declare function createAgent(config: AgentConfig): Agent;
3
3
  export declare function createAgentSession(config: AgentSessionConfig & {
4
4
  readonly agent: Agent;
package/dist/agents.js CHANGED
@@ -1,23 +1,23 @@
1
- import { AgentRunError, AgentRunStateError, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS, } from "./contracts.js";
2
1
  import { resolveLoop, resolveToolConcurrency } from "./agent-loops.js";
3
- import { createId } from "./ids.js";
4
- import { assertGuardrailsAllowed, GuardrailError, runGuardrails } from "./guardrails.js";
5
- import { createProviderTurnMetadata, readProviderHttpStatus } from "./observability.js";
2
+ import { agentFingerprint, initialAgentRunState, loadAgentRunState, publicState, saveAgentRunState, validateRunStateOptions, } from "./agent-run-state.js";
6
3
  import { createDefaultCompactionStrategy, isCompactionEntryData } from "./compaction.js";
4
+ import { AgentRunError, AgentRunStateError, DEFAULT_MAX_PENDING_STEER_BYTES, DEFAULT_MAX_PENDING_STEERS } from "./contracts.js";
5
+ import { assertGuardrailsAllowed, GuardrailError, runGuardrails } from "./guardrails.js";
6
+ import { identityTelemetryAttributes, ownershipFromIdentity, resolveRunIdentity } from "./identity.js";
7
+ import { createId } from "./ids.js";
7
8
  import { assembleProviderInput } from "./input.js";
9
+ import { createProviderTurnMetadata, readProviderHttpStatus } from "./observability.js";
8
10
  import { providerToolCallDeltaContent, reconstructToolCallDeltas } from "./provider-events.js";
9
11
  import { createProviderRequestPolicyChain, normalizeProviderRequestPolicyResult } from "./provider-request-policy.js";
10
- import { assertStructuredOutputRequestSupported, resolveRunProviderOptions } from "./structured-output.js";
11
- import { errorToErrorInfo, redactAgentEvent, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry } from "./redaction.js";
12
- import { composeSystemPrompt, mergeSystemPromptConfig } from "./system-prompts.js";
12
+ import { errorToErrorInfo, redactAgentEvent, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry, } from "./redaction.js";
13
13
  import { createDefaultRetryPolicy, waitForRetry } from "./retry.js";
14
- import { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, rebuildSessionContext } from "./session-stores.js";
15
14
  import { isFlushableRunLedger } from "./run-ledger.js";
16
- import { createToolRegistry, dispatchToolCall } from "./tools.js";
17
15
  import { RunLimitError, RunLimitTracker, resolveRunLimits } from "./run-limits.js";
18
- import { agentFingerprint, initialAgentRunState, loadAgentRunState, publicState, saveAgentRunState, validateRunStateOptions } from "./agent-run-state.js";
16
+ import { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, rebuildSessionContext, } from "./session-stores.js";
19
17
  import { resolveActiveSkills } from "./skills.js";
20
- import { identityTelemetryAttributes, ownershipFromIdentity, resolveRunIdentity, } from "./identity.js";
18
+ import { assertStructuredOutputRequestSupported, resolveRunProviderOptions } from "./structured-output.js";
19
+ import { composeSystemPrompt, mergeSystemPromptConfig } from "./system-prompts.js";
20
+ import { createToolRegistry, dispatchToolCall } from "./tools.js";
21
21
  export function createAgent(config) {
22
22
  return {
23
23
  config,
@@ -39,7 +39,9 @@ export async function* resumeAgentRunStream(agent, ref, resume, options) {
39
39
  const prepared = await prepareAgentRunResume(agent, ref, resume, options, options.signal);
40
40
  const subscription = prepared.session.subscribe(options);
41
41
  let settled = false;
42
- const runPromise = executePreparedAgentRunResume(prepared, options.signal).finally(() => { settled = true; });
42
+ const runPromise = executePreparedAgentRunResume(prepared, options.signal).finally(() => {
43
+ settled = true;
44
+ });
43
45
  try {
44
46
  for await (const event of subscription) {
45
47
  if ("runId" in event && event.runId !== ref.runId)
@@ -58,7 +60,9 @@ export async function* resumeAgentRunStream(agent, ref, resume, options) {
58
60
  async function prepareAgentRunResume(agent, ref, resume, options, signal) {
59
61
  throwIfAbortedSignal(signal);
60
62
  const { record, state } = await loadAgentRunState(options.checkpoints, ref, options.ownership);
61
- if (state.definitionRevision !== options.definitionRevision || state.agentId !== (agent.config.id ?? agent.config.name) || state.fingerprint !== agentFingerprint(agent, options.definitionRevision)) {
63
+ if (state.definitionRevision !== options.definitionRevision ||
64
+ state.agentId !== (agent.config.id ?? agent.config.name) ||
65
+ state.fingerprint !== agentFingerprint(agent, options.definitionRevision)) {
62
66
  throw new AgentRunStateError("Agent definition revision or fingerprint mismatch on resume");
63
67
  }
64
68
  if (record.version !== resume.expectedVersion || state.status !== "suspended") {
@@ -211,7 +215,11 @@ class RuntimeAgentSession {
211
215
  }
212
216
  }
213
217
  async resumeDurable(state, runState, ownership, signal) {
214
- return this.runInternal(state.input ?? [], { runState, ownership, signal }, state.runId, { options: runState, state, version: state.version });
218
+ return this.runInternal(state.input ?? [], { runState, ownership, signal }, state.runId, {
219
+ options: runState,
220
+ state,
221
+ version: state.version,
222
+ });
215
223
  }
216
224
  async recordDurableDenial(runId, interruption, version, ownership) {
217
225
  this.activeLedger = this.agent.config.runLedger;
@@ -229,7 +237,11 @@ class RuntimeAgentSession {
229
237
  }
230
238
  }
231
239
  async runInternal(input, options, runId, resumed) {
232
- if (this.agent.config.secure && (options.redactor !== undefined || options.ownership !== undefined || options.validate !== undefined || options.runState !== undefined)) {
240
+ if (this.agent.config.secure &&
241
+ (options.redactor !== undefined ||
242
+ options.ownership !== undefined ||
243
+ options.validate !== undefined ||
244
+ options.runState !== undefined)) {
233
245
  throw new AgentRunStateError("Secure agent defaults cannot be replaced per run");
234
246
  }
235
247
  const requestedLimits = options.maxToolRounds === undefined
@@ -316,7 +328,14 @@ class RuntimeAgentSession {
316
328
  const { registry, tools } = activeTools(this.agent.config.tools);
317
329
  const activeSkills = this.resolveRunSkills(options, tools);
318
330
  if (options.model && JSON.stringify(options.model) !== JSON.stringify(this.agent.config.model)) {
319
- await this.appendEntry(createSessionEntry({ sessionId: this.id, parentId: this.currentLeafId, runId, kind: "model_change", previousModel: this.agent.config.model, model: options.model }));
331
+ await this.appendEntry(createSessionEntry({
332
+ sessionId: this.id,
333
+ parentId: this.currentLeafId,
334
+ runId,
335
+ kind: "model_change",
336
+ previousModel: this.agent.config.model,
337
+ model: options.model,
338
+ }));
320
339
  }
321
340
  const inputMessages = inputToMessages(input).map((message) => this.redact(message));
322
341
  const inputGuardrails = await runGuardrails({
@@ -327,20 +346,24 @@ class RuntimeAgentSession {
327
346
  redactor: this.activeRedactor,
328
347
  emit: (event) => this.emit(event),
329
348
  });
330
- if (inputGuardrails.terminal?.action === "interrupt" && this.activeDurable) {
331
- if (!resumed) {
332
- const interruption = { kind: "input_guardrail", reason: inputGuardrails.terminal.reason ?? "Input requires approval" };
333
- throw new AgentRunSuspended(await this.suspendDurable({ runId, model, limits, interruption, messages: inputMessages }), interruption);
334
- }
349
+ // Input-guardrail decision table:
350
+ // - interrupt + durable + fresh run → suspend for approval.
351
+ // - interrupt + durable + resumed run → proceed: resuming IS the operator approval.
352
+ // - interrupt without durable, or block/tripwire → fail via assertGuardrailsAllowed.
353
+ const approvedByResume = resumed !== undefined && inputGuardrails.terminal?.action === "interrupt" && this.activeDurable !== undefined;
354
+ if (inputGuardrails.terminal?.action === "interrupt" && this.activeDurable && !approvedByResume) {
355
+ const interruption = { kind: "input_guardrail", reason: inputGuardrails.terminal.reason ?? "Input requires approval" };
356
+ throw new AgentRunSuspended(await this.suspendDurable({ runId, model, limits, interruption, messages: inputMessages }), interruption);
335
357
  }
336
- else {
358
+ if (inputGuardrails.terminal && !approvedByResume)
337
359
  assertGuardrailsAllowed(inputGuardrails);
338
- }
339
360
  for (const message of inputMessages)
340
361
  await this.appendMessage(message, runId);
341
362
  await this.autoCompact(runId, options, controller.signal, inputMessages);
342
363
  const maxToolRounds = resolvedLimits.maxToolRounds;
343
- const systemInstructions = composeSystemPrompt(mergeSystemPromptConfig(this.agent.config.systemPrompt, options.systemPrompt), { base: this.agent.config.instructions });
364
+ const systemInstructions = composeSystemPrompt(mergeSystemPromptConfig(this.agent.config.systemPrompt, options.systemPrompt), {
365
+ base: this.agent.config.instructions,
366
+ });
344
367
  const contextProviders = [
345
368
  ...(this.agent.config.context ?? []),
346
369
  // ponytail: skill context after host context; no per-skill token budget yet.
@@ -429,7 +452,7 @@ class RuntimeAgentSession {
429
452
  limits.charge("maxTurns");
430
453
  assembledTurn = false;
431
454
  const policyResult = await this.applyProviderRequestPolicies(request, runId, options, metadata, controller.signal);
432
- const middlewareRequest = await this.agent.config.middleware?.run("provider_request", policyResult.request) ?? policyResult.request;
455
+ const middlewareRequest = (await this.agent.config.middleware?.run("provider_request", policyResult.request)) ?? policyResult.request;
433
456
  try {
434
457
  return await this.generateWithRetry(this.redactProviderRequest(middlewareRequest), runId, options, controller.signal, policyResult.secrets, this.activeLoopTurn, recordProviderUsage);
435
458
  }
@@ -468,12 +491,22 @@ class RuntimeAgentSession {
468
491
  return;
469
492
  const pending = durable.state?.pending;
470
493
  if (pending?.call.id === mediatedCall.id && pending.status === "ready") {
471
- await this.persistDurable({ ...durable.state, status: "running", pending: { ...pending, status: "dispatched" }, interruption: undefined });
494
+ await this.persistDurable({
495
+ ...durable.state,
496
+ status: "running",
497
+ pending: { ...pending, status: "dispatched" },
498
+ interruption: undefined,
499
+ });
472
500
  return;
473
501
  }
474
502
  if (!durable.options.interruptBeforeTool)
475
503
  return;
476
- const interruption = { kind: "tool_approval", reason: "Tool side effect requires approval", toolCallId: mediatedCall.id, toolName: mediatedCall.name };
504
+ const interruption = {
505
+ kind: "tool_approval",
506
+ reason: "Tool side effect requires approval",
507
+ toolCallId: mediatedCall.id,
508
+ toolName: mediatedCall.name,
509
+ };
477
510
  throw new AgentRunSuspended(await this.suspendDurable({
478
511
  runId,
479
512
  model,
@@ -508,13 +541,19 @@ class RuntimeAgentSession {
508
541
  const result = await ctx.dispatchToolCall(resumed.state.pending.call);
509
542
  await ctx.appendMessage({
510
543
  role: "tool",
511
- content: [{ type: "tool_result", toolCallId: result.toolCallId, name: result.name, result: result.value, error: result.error }, ...(result.content ?? [])],
544
+ content: [
545
+ { type: "tool_result", toolCallId: result.toolCallId, name: result.name, result: result.value, error: result.error },
546
+ ...(result.content ?? []),
547
+ ],
512
548
  metadata: result.metadata,
513
549
  });
514
550
  }
515
551
  const loopUsage = await loop.run(ctx);
516
552
  if (loop.name === "generate-validate-revise" && !artifactFinished) {
517
- throw Object.assign(new Error(artifactFailedInfo?.message ?? "artifact loop ended without a validated artifact"), { name: "ArtifactFailed", code: artifactFailedInfo?.code ?? "artifact_failed" });
553
+ throw Object.assign(new Error(artifactFailedInfo?.message ?? "artifact loop ended without a validated artifact"), {
554
+ name: "ArtifactFailed",
555
+ code: artifactFailedInfo?.code ?? "artifact_failed",
556
+ });
518
557
  }
519
558
  usage = runUsage.value() ?? loopUsage;
520
559
  if (usage && this.activeLedger) {
@@ -661,21 +700,22 @@ class RuntimeAgentSession {
661
700
  const durable = this.activeDurable;
662
701
  if (!durable)
663
702
  throw new AgentRunStateError("Durable interruption is not configured");
664
- const state = durable.state ?? initialAgentRunState({
665
- agent: this.agent,
666
- options: durable.options,
667
- runId: input.runId,
668
- sessionId: this.id,
669
- leafId: this.currentLeafId,
670
- model: input.model,
671
- counters: input.limits.snapshot(),
672
- deadlineAt: input.limits.deadlineAt,
673
- status: "suspended",
674
- interruption: input.interruption,
675
- messages: input.messages,
676
- pending: input.pending,
677
- interruptBeforeTool: durable.options.interruptBeforeTool,
678
- });
703
+ const state = durable.state ??
704
+ initialAgentRunState({
705
+ agent: this.agent,
706
+ options: durable.options,
707
+ runId: input.runId,
708
+ sessionId: this.id,
709
+ leafId: this.currentLeafId,
710
+ model: input.model,
711
+ counters: input.limits.snapshot(),
712
+ deadlineAt: input.limits.deadlineAt,
713
+ status: "suspended",
714
+ interruption: input.interruption,
715
+ messages: input.messages,
716
+ pending: input.pending,
717
+ interruptBeforeTool: durable.options.interruptBeforeTool,
718
+ });
679
719
  return this.persistDurable({
680
720
  ...state,
681
721
  leafId: this.currentLeafId,
@@ -723,7 +763,13 @@ class RuntimeAgentSession {
723
763
  await this.rebuildHistory();
724
764
  }
725
765
  fork(options = {}) {
726
- return createAgentSession({ agent: this.agent, id: this.id, store: this.store, leafId: options.leafId ?? this.currentLeafId, metadata: this.metadata });
766
+ return createAgentSession({
767
+ agent: this.agent,
768
+ id: this.id,
769
+ store: this.store,
770
+ leafId: options.leafId ?? this.currentLeafId,
771
+ metadata: this.metadata,
772
+ });
727
773
  }
728
774
  async clone(options = {}) {
729
775
  const id = options.id ?? randomId("session");
@@ -739,7 +785,13 @@ class RuntimeAgentSession {
739
785
  const { id: _oldId, parentId: _oldParentId, sessionId: _oldSessionId, ...rest } = entry;
740
786
  await this.store.append({ ...rest, id: nextId, parentId: entry.parentId ? remap.get(entry.parentId) : undefined, sessionId: id });
741
787
  }
742
- return createAgentSession({ agent: this.agent, id, store: this.store, leafId: branch.length ? remap.get(branch[branch.length - 1].id) : undefined, metadata: this.metadata });
788
+ return createAgentSession({
789
+ agent: this.agent,
790
+ id,
791
+ store: this.store,
792
+ leafId: branch.length ? remap.get(branch[branch.length - 1].id) : undefined,
793
+ metadata: this.metadata,
794
+ });
743
795
  }
744
796
  branchReader() {
745
797
  // ponytail: prefer the store's readBranchPath (one ancestor-chain query) when present so a
@@ -753,9 +805,7 @@ class RuntimeAgentSession {
753
805
  // the resolver entirely; otherwise `RunOptions.providerSource` overrides
754
806
  // `AgentConfig.providerSource` for this run. A miss on every source fails
755
807
  // closed with `Unknown provider: ${model.provider}` before any provider turn.
756
- const provider = this.agent.config.provider ??
757
- options.providerSource?.(model) ??
758
- this.agent.config.providerSource?.(model);
808
+ const provider = this.agent.config.provider ?? options.providerSource?.(model) ?? this.agent.config.providerSource?.(model);
759
809
  if (!provider)
760
810
  throw new Error(`Unknown provider: ${model.provider}`);
761
811
  this.activeProvider = provider;
@@ -828,7 +878,10 @@ class RuntimeAgentSession {
828
878
  throw errorFromInfo(info);
829
879
  const context = { sessionId: this.id, runId, attempt, error: info, metadata: retry?.metadata, signal };
830
880
  let decision = await policy.decide(context);
831
- const payload = await this.agent.config.middleware?.run("retry", { context, decision }) ?? { context, decision };
881
+ const payload = (await this.agent.config.middleware?.run("retry", { context, decision })) ?? {
882
+ context,
883
+ decision,
884
+ };
832
885
  decision = payload.decision;
833
886
  if (!decision.retry)
834
887
  throw errorFromInfo(info);
@@ -998,7 +1051,22 @@ class RuntimeAgentSession {
998
1051
  redactor: this.activeRedactor,
999
1052
  emit: (event) => this.emit(event),
1000
1053
  });
1001
- assertGuardrailsAllowed(inputGuardrails);
1054
+ // Mid-run steer: a terminal decision drops the message (never enters history or
1055
+ // the session store) and the run continues. Run-start input blocking still fails
1056
+ // the run — only the blast radius of steered input is narrowed.
1057
+ const terminal = inputGuardrails.terminal;
1058
+ if (terminal) {
1059
+ if (terminal.action === "interrupt")
1060
+ throw new GuardrailError(terminal);
1061
+ this.emit({
1062
+ type: "steer_rejected",
1063
+ sessionId: this.id,
1064
+ runId,
1065
+ message: this.activeRedactor ? this.activeRedactor.redact(message) : message,
1066
+ record: terminal,
1067
+ });
1068
+ continue;
1069
+ }
1002
1070
  this.history.push(message);
1003
1071
  await this.appendMessage(message, runId);
1004
1072
  }
@@ -1029,16 +1097,35 @@ class RuntimeAgentSession {
1029
1097
  throwIfAbortedSignal(signal);
1030
1098
  const entries = await this.entries();
1031
1099
  const secrets = options.secrets ?? [];
1032
- const strategy = options.strategy ?? createDefaultCompactionStrategy({ keepRecentEntries: options.keepRecentEntries, maxSummaryChars: options.maxSummaryChars, secrets });
1033
- const context = { sessionId: this.id, entries, keepRecentEntries: options.keepRecentEntries, trigger, secrets, metadata: options.metadata, signal };
1100
+ const strategy = options.strategy ??
1101
+ createDefaultCompactionStrategy({ keepRecentEntries: options.keepRecentEntries, maxSummaryChars: options.maxSummaryChars, secrets });
1102
+ const context = {
1103
+ sessionId: this.id,
1104
+ entries,
1105
+ keepRecentEntries: options.keepRecentEntries,
1106
+ trigger,
1107
+ secrets,
1108
+ metadata: options.metadata,
1109
+ signal,
1110
+ };
1034
1111
  this.emit({ type: "compaction_started", sessionId: this.id, runId });
1035
1112
  let result = await strategy.compact(context);
1036
1113
  result = { ...result, summary: redactSecrets(result.summary, secrets) };
1037
- const payload = await this.agent.config.middleware?.run("compaction", { context, result }) ?? { context, result };
1114
+ const payload = (await this.agent.config.middleware?.run("compaction", { context, result })) ?? {
1115
+ context,
1116
+ result,
1117
+ };
1038
1118
  result = { ...payload.result, summary: redactSecrets(payload.result.summary, secrets) };
1039
1119
  const source = result.entries?.find((entry) => entry.kind === "compaction");
1040
1120
  const data = isCompactionEntryData(source?.data) ? source.data : undefined;
1041
- const entry = createSessionEntry({ sessionId: this.id, parentId: this.currentLeafId, runId, kind: "compaction", summary: result.summary, data });
1121
+ const entry = createSessionEntry({
1122
+ sessionId: this.id,
1123
+ parentId: this.currentLeafId,
1124
+ runId,
1125
+ kind: "compaction",
1126
+ summary: result.summary,
1127
+ data,
1128
+ });
1042
1129
  await this.appendEntry(entry);
1043
1130
  const finalResult = { ...result, entries: [entry] };
1044
1131
  this.emit({ type: "compaction_finished", sessionId: this.id, runId, summary: finalResult.summary });
@@ -1181,8 +1268,8 @@ class SteerSoftInterrupt extends Error {
1181
1268
  }
1182
1269
  }
1183
1270
  function isSteerSoftInterrupt(error) {
1184
- return error instanceof SteerSoftInterrupt
1185
- || (typeof error === "object" && error !== null && error.code === STEER_SOFT_INTERRUPT_CODE);
1271
+ return (error instanceof SteerSoftInterrupt ||
1272
+ (typeof error === "object" && error !== null && error.code === STEER_SOFT_INTERRUPT_CODE));
1186
1273
  }
1187
1274
  function finalAssistantMessage(history) {
1188
1275
  for (let index = history.length - 1; index >= 0; index -= 1) {
@@ -1254,11 +1341,25 @@ function withoutTrailingInput(messages, input) {
1254
1341
  const next = [...messages];
1255
1342
  for (let i = input.length - 1; i >= 0; i -= 1) {
1256
1343
  const last = next.at(-1);
1257
- if (last && JSON.stringify(last) === JSON.stringify(input[i]))
1344
+ if (last && stableMessageKey(last) === stableMessageKey(input[i]))
1258
1345
  next.pop();
1259
1346
  }
1260
1347
  return next;
1261
1348
  }
1349
+ // Key-order-insensitive comparison: a redacted-then-reassembled message with reordered
1350
+ // keys must still dedupe against the trailing input, or auto-compaction duplicates it.
1351
+ function stableMessageKey(value) {
1352
+ if (Array.isArray(value))
1353
+ return `[${value.map(stableMessageKey).join(",")}]`;
1354
+ if (value !== null && typeof value === "object") {
1355
+ const record = value;
1356
+ return `{${Object.keys(record)
1357
+ .sort()
1358
+ .map((key) => `${JSON.stringify(key)}:${stableMessageKey(record[key])}`)
1359
+ .join(",")}}`;
1360
+ }
1361
+ return JSON.stringify(value) ?? "null";
1362
+ }
1262
1363
  function bridgeAbort(signal, controller) {
1263
1364
  if (!signal)
1264
1365
  return () => undefined;
@@ -1276,9 +1377,10 @@ function throwIfAbortedSignal(signal) {
1276
1377
  if (signal?.aborted)
1277
1378
  throw signal.reason instanceof Error ? signal.reason : new Error("Agent run aborted");
1278
1379
  }
1380
+ const jsonTextEncoder = new TextEncoder();
1279
1381
  function jsonBytes(value) {
1280
1382
  try {
1281
- return new TextEncoder().encode(JSON.stringify(value)).byteLength;
1383
+ return jsonTextEncoder.encode(JSON.stringify(value)).byteLength;
1282
1384
  }
1283
1385
  catch {
1284
1386
  throw new TypeError("Provider request or event must be JSON-serializable for run limits");
@@ -1295,8 +1397,8 @@ function createUsageAccumulator() {
1295
1397
  if (value !== undefined)
1296
1398
  sums.set(key, (sums.get(key) ?? 0) + value);
1297
1399
  }
1298
- const total = usage.totalTokens
1299
- ?? (usage.inputTokens !== undefined || usage.outputTokens !== undefined
1400
+ const total = usage.totalTokens ??
1401
+ (usage.inputTokens !== undefined || usage.outputTokens !== undefined
1300
1402
  ? (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0)
1301
1403
  : undefined);
1302
1404
  if (total !== undefined)