@arnilo/prism 0.0.16 → 0.0.18

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 (61) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +9 -2
  3. package/dist/agent-run-state.js +13 -1
  4. package/dist/agents.js +42 -10
  5. package/dist/checkpoints.d.ts +4 -0
  6. package/dist/checkpoints.js +12 -0
  7. package/dist/cli-runner.d.ts +1 -5
  8. package/dist/cli-runner.js +5 -28
  9. package/dist/context-budget.js +10 -7
  10. package/dist/contracts.d.ts +13 -0
  11. package/dist/contributions.d.ts +2 -0
  12. package/dist/contributions.js +3 -0
  13. package/dist/credentials.d.ts +7 -1
  14. package/dist/credentials.js +6 -2
  15. package/dist/event-multiplexer.js +17 -1
  16. package/dist/extensions.d.ts +7 -1
  17. package/dist/extensions.js +64 -6
  18. package/dist/feedback.js +1 -1
  19. package/dist/guardrails.js +9 -3
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +1 -1
  22. package/dist/input.js +12 -5
  23. package/dist/middleware.js +9 -1
  24. package/dist/models.d.ts +2 -0
  25. package/dist/models.js +3 -0
  26. package/dist/providers/openai-compatible.d.ts +42 -1
  27. package/dist/providers/openai-compatible.js +109 -47
  28. package/dist/providers/transport.d.ts +6 -0
  29. package/dist/providers/transport.js +21 -0
  30. package/dist/providers.d.ts +2 -0
  31. package/dist/providers.js +3 -0
  32. package/dist/redaction.js +21 -7
  33. package/dist/retry.d.ts +5 -0
  34. package/dist/retry.js +8 -1
  35. package/dist/run-ledger.d.ts +6 -0
  36. package/dist/run-ledger.js +3 -9
  37. package/dist/session-stores.js +15 -11
  38. package/docs/0.1.0-readiness.md +35 -21
  39. package/docs/agent-events.md +2 -1
  40. package/docs/agent-session-runtime.md +3 -3
  41. package/docs/cli-rpc.md +1 -5
  42. package/docs/coding-agent-tools.md +10 -6
  43. package/docs/compaction-and-retry.md +3 -1
  44. package/docs/contribution-registries.md +1 -0
  45. package/docs/credentials-and-redaction.md +1 -1
  46. package/docs/extensions.md +1 -1
  47. package/docs/guardrails.md +13 -2
  48. package/docs/index.md +4 -4
  49. package/docs/input-and-prompt-assembly.md +6 -8
  50. package/docs/mcp-tools.md +3 -3
  51. package/docs/middleware-hooks.md +2 -2
  52. package/docs/migration.md +24 -1
  53. package/docs/provider-caching.md +1 -1
  54. package/docs/provider-conformance.md +1 -1
  55. package/docs/provider-packages.md +1 -1
  56. package/docs/providers/ai-sdk.md +2 -1
  57. package/docs/providers/openai-compatible.md +28 -1
  58. package/docs/public-contracts.md +2 -2
  59. package/docs/release-and-install.md +59 -17
  60. package/docs/session-stores.md +1 -1
  61. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.18] - 2026-07-30
4
+
5
+ ### Changed
6
+ - Default `inputLayout` is `cache_aware` (unset `AgentConfig` / `RunOptions` use cache-stable message order); set `inputLayout: "legacy"` to restore prior ordering.
7
+ - `applyContextBudget` evicts oldest history messages first under pressure (was newest-first).
8
+ - `@arnilo/prism-mcp` pins `@modelcontextprotocol/sdk` **1.30.0** (clears moderate `@hono/node-server` path-traversal advisory on the MCP HTTP stack).
9
+
10
+ ### Breaking (minor, pre-1.0)
11
+ - `@arnilo/prism-coding-agent` `repo_search` is literal-only: `mode: "regex"` removed; `compileSearchPattern` drops the `mode` argument (ReDoS mitigation).
12
+ - Default local `write` / `edit` operations use same-directory temp + `rename` for crash-safe replacement.
13
+
14
+ See [docs/migration.md](docs/migration.md) for the full 0.0.17 → 0.0.18 notes.
15
+
16
+ ## [0.0.17] - 2026-07-29
17
+
18
+ ### Added
19
+ - Extension lifecycle: `ExtensionKernel.load()` returns `LoadedExtension[]` dispose handles; contribution/provider/model registries gain `unregister(...)`; a failed `setup` unwinds its partial registrations.
20
+ - `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.
21
+
22
+ ### Changed
23
+ - 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.
24
+ - Retry/backpressure: HTTP provider errors carry numeric codes and `Retry-After` hints; default retry policy applies ±25% jitter.
25
+ - `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²).
26
+ - 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.
27
+ - Default prompt builder omits the `Available tools:` text for tool-capable models (`capabilities.tools === true`).
28
+ - 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.
29
+
30
+ ### Breaking (minor, pre-1.0)
31
+ - CLI: `--config`, `--resource`, `--extension`, `--tool` are rejected (`<flag> is not supported in this build`); the dead `CliOptions.config/resources/extensions/tools` fields are removed.
32
+ - `ExtensionKernel.load()` resolves to `LoadedExtension[]` instead of `void`.
33
+
34
+ See [docs/migration.md](docs/migration.md) for the full 0.0.16 → 0.0.17 notes.
35
+
3
36
  ## [0.0.16] - 2026-07-26
4
37
 
5
38
  ### Added
package/README.md CHANGED
@@ -157,6 +157,13 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
157
157
  | `@arnilo/prism-provider-neuralwatt` | NeuralWatt provider with implicit vLLM prefix caching |
158
158
  | `@arnilo/prism-provider-alibaba` | Alibaba Cloud (Model Studio / DashScope + Coding Plan) provider with dynamic discovery and explicit/implicit caching |
159
159
  | `@arnilo/prism-provider-ollama` | Ollama Cloud / local provider with dynamic discovery and implicit-only caching |
160
+ | `@arnilo/prism-provider-anthropic` | Anthropic Messages provider |
161
+ | `@arnilo/prism-provider-google` | Google Gemini provider |
162
+ | `@arnilo/prism-provider-azure` | Azure OpenAI provider |
163
+ | `@arnilo/prism-provider-bedrock` | AWS Bedrock provider |
164
+ | `@arnilo/prism-provider-vertex` | Google Vertex provider |
165
+ | `@arnilo/prism-provider-ai-sdk` | AI SDK interoperability adapter |
166
+ | `@arnilo/prism-browser` | optional host-wired Playwright browser automation (not core; not auto-activated) |
160
167
  | `@arnilo/prism-compaction-llm` | provider-backed compaction strategy |
161
168
  | `@arnilo/prism-compaction-observational-memory` | source-backed memory + recall tool |
162
169
  | `@arnilo/prism-coding-agent` | bounded shell/read/write/edit tools |
@@ -170,7 +177,7 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
170
177
  | `@arnilo/prism-credentials-node` | encrypted-file and keychain credentials |
171
178
  | `@arnilo/prism-session-store-sqlite` | SQLite persistence/checkpoints/leases/owned run feedback |
172
179
  | `@arnilo/prism-session-store-postgres` | PostgreSQL persistence/checkpoints/leases/owned run feedback |
173
- | `@arnilo/prism-providers` | family: all 11 provider adapters, including AI SDK interoperability |
180
+ | `@arnilo/prism-providers` | family: all 14 first-party provider adapters, including AI SDK interoperability |
174
181
  | `@arnilo/prism-compaction` | family: both compaction strategies |
175
182
  | `@arnilo/prism-base` | profile: core + compaction + JSON Schema validation |
176
183
  | `@arnilo/prism-code` | profile: base + coding tools/security + MCP |
@@ -189,6 +196,6 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
189
196
  ## Non-goals (v1)
190
197
 
191
198
  - Privileged tools, MCP servers, telemetry, credentials, or databases activated by install — hosts explicitly configure and register every capability.
192
- - Browser automation or interactive terminal UI — CLI/RPC and workflow control APIs only.
199
+ - Browser automation or interactive terminal UI in core — hosts may opt into `@arnilo/prism-browser` with their own Playwright lifecycle; Prism does not auto-start browsers or ship a TUI.
193
200
  - Provider, credential, extension, or package auto-discovery.
194
201
  - Core-owned database drivers, secret persistence, sandbox, or application policy — optional packages implement adapters over host-owned boundaries.
@@ -9,16 +9,25 @@ 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
33
  loop: typeof config.loop === "object" && config.loop && "strategy" in config.loop
@@ -111,7 +120,10 @@ export function parseAgentRunState(value, version) {
111
120
  !state.deadlineAt) {
112
121
  throw new AgentRunStateError("Malformed agent run state");
113
122
  }
114
- 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);
115
127
  }
116
128
  function boundState(state, maxBytes) {
117
129
  checkShape(state, 0);
package/dist/agents.js CHANGED
@@ -346,15 +346,17 @@ class RuntimeAgentSession {
346
346
  redactor: this.activeRedactor,
347
347
  emit: (event) => this.emit(event),
348
348
  });
349
- if (inputGuardrails.terminal?.action === "interrupt" && this.activeDurable) {
350
- if (!resumed) {
351
- const interruption = { kind: "input_guardrail", reason: inputGuardrails.terminal.reason ?? "Input requires approval" };
352
- throw new AgentRunSuspended(await this.suspendDurable({ runId, model, limits, interruption, messages: inputMessages }), interruption);
353
- }
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);
354
357
  }
355
- else {
358
+ if (inputGuardrails.terminal && !approvedByResume)
356
359
  assertGuardrailsAllowed(inputGuardrails);
357
- }
358
360
  for (const message of inputMessages)
359
361
  await this.appendMessage(message, runId);
360
362
  await this.autoCompact(runId, options, controller.signal, inputMessages);
@@ -1049,7 +1051,22 @@ class RuntimeAgentSession {
1049
1051
  redactor: this.activeRedactor,
1050
1052
  emit: (event) => this.emit(event),
1051
1053
  });
1052
- 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
+ }
1053
1070
  this.history.push(message);
1054
1071
  await this.appendMessage(message, runId);
1055
1072
  }
@@ -1324,11 +1341,25 @@ function withoutTrailingInput(messages, input) {
1324
1341
  const next = [...messages];
1325
1342
  for (let i = input.length - 1; i >= 0; i -= 1) {
1326
1343
  const last = next.at(-1);
1327
- if (last && JSON.stringify(last) === JSON.stringify(input[i]))
1344
+ if (last && stableMessageKey(last) === stableMessageKey(input[i]))
1328
1345
  next.pop();
1329
1346
  }
1330
1347
  return next;
1331
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
+ }
1332
1363
  function bridgeAbort(signal, controller) {
1333
1364
  if (!signal)
1334
1365
  return () => undefined;
@@ -1346,9 +1377,10 @@ function throwIfAbortedSignal(signal) {
1346
1377
  if (signal?.aborted)
1347
1378
  throw signal.reason instanceof Error ? signal.reason : new Error("Agent run aborted");
1348
1379
  }
1380
+ const jsonTextEncoder = new TextEncoder();
1349
1381
  function jsonBytes(value) {
1350
1382
  try {
1351
- return new TextEncoder().encode(JSON.stringify(value)).byteLength;
1383
+ return jsonTextEncoder.encode(JSON.stringify(value)).byteLength;
1352
1384
  }
1353
1385
  catch {
1354
1386
  throw new TypeError("Provider request or event must be JSON-serializable for run limits");
@@ -6,6 +6,10 @@ export declare class CheckpointConflictError extends Error {
6
6
  }
7
7
  export interface MemoryCheckpointStoreOptions {
8
8
  readonly maxPageSize?: number;
9
+ /** Capacity bound: least-recently-saved records are evicted past this count (default 10_000). */
10
+ readonly maxRecords?: number;
11
+ /** Per-record bound on the JSON-encoded value in bytes (default 1 MiB); oversized values are rejected. */
12
+ readonly maxValueBytes?: number;
9
13
  }
10
14
  /** In-process reference implementation of the generic checkpoint contract. */
11
15
  export declare function createMemoryCheckpointStore(options?: MemoryCheckpointStoreOptions): CheckpointStore;
@@ -6,10 +6,14 @@ export class CheckpointConflictError extends Error {
6
6
  this.name = "CheckpointConflictError";
7
7
  }
8
8
  }
9
+ const DEFAULT_MAX_CHECKPOINT_RECORDS = 10_000;
10
+ const DEFAULT_MAX_CHECKPOINT_VALUE_BYTES = 1024 * 1024;
9
11
  /** In-process reference implementation of the generic checkpoint contract. */
10
12
  export function createMemoryCheckpointStore(options = {}) {
11
13
  const records = new Map();
12
14
  const maxPageSize = Math.max(1, options.maxPageSize ?? 500);
15
+ const maxRecords = Math.max(1, options.maxRecords ?? DEFAULT_MAX_CHECKPOINT_RECORDS);
16
+ const maxValueBytes = Math.max(1, options.maxValueBytes ?? DEFAULT_MAX_CHECKPOINT_VALUE_BYTES);
13
17
  return {
14
18
  async saveCheckpoint(input) {
15
19
  throwIfAborted(input.signal);
@@ -32,6 +36,9 @@ export function createMemoryCheckpointStore(options = {}) {
32
36
  }
33
37
  const now = new Date().toISOString();
34
38
  const value = cloneJson(input.value, "Checkpoint value");
39
+ if (new TextEncoder().encode(JSON.stringify(value)).length > maxValueBytes) {
40
+ throw new RangeError(`Checkpoint value exceeds maxValueBytes (${maxValueBytes})`);
41
+ }
35
42
  const metadata = input.metadata === undefined ? undefined : cloneJson(input.metadata, "Checkpoint metadata");
36
43
  const record = {
37
44
  namespace: input.namespace,
@@ -45,7 +52,12 @@ export function createMemoryCheckpointStore(options = {}) {
45
52
  updatedAt: now,
46
53
  ...(metadata === undefined ? {} : { metadata }),
47
54
  };
55
+ // delete+set moves an updated record to the end, keeping Map order = least- to
56
+ // most-recently-saved, so capacity eviction is O(1) off the front.
57
+ records.delete(id);
48
58
  records.set(id, record);
59
+ while (records.size > maxRecords)
60
+ records.delete(records.keys().next().value);
49
61
  return record;
50
62
  },
51
63
  async loadCheckpoint(input) {
@@ -9,10 +9,6 @@ export interface CliOptions {
9
9
  readonly provider?: string;
10
10
  readonly model?: string;
11
11
  readonly session?: string;
12
- readonly config: readonly string[];
13
- readonly resources: readonly string[];
14
- readonly extensions: readonly string[];
15
- readonly tools: readonly string[];
16
12
  readonly system?: string;
17
13
  readonly context: readonly string[];
18
14
  readonly compact?: number;
@@ -67,7 +63,7 @@ export interface CliRuntime {
67
63
  /** Working directory for relative `prism init` destinations (tests). */
68
64
  readonly cwd?: string;
69
65
  }
70
- export declare const usage = "Usage: prism [--mode print|json|rpc] [-p prompt] [options]\n prism init <dir> [--provider <name>] [--with-workflows] [--with-evals] [--force]\n\nOptions:\n -p, --prompt <text> Prompt to run in print/json mode\n --provider <name> Explicit provider id (mock is built in for smoke tests)\n --model <name> Explicit model name\n --session <id> Session id\n --config <path> Explicit config path (recorded, not auto-loaded)\n --resource <uri> Explicit resource URI (recorded, not auto-loaded)\n --extension <name> Explicit extension name (recorded, not auto-loaded)\n --tool <name> Explicit tool name (recorded, not auto-enabled)\n --system <text> System instructions\n --context <text> Context text\n --compact <entries> Auto-compaction threshold\n --max-tool-rounds <n> Maximum tool rounds\n --discover Enable workspace contribution discovery (opt-in)\n --discover-kinds <csv> Kinds to discover (default: skill; skill,tool,context,instructions)\n --no-discovery Disable discovery even if --discover is set\n --agents-config <path> App config root holding agents/<name>/AGENT.md bundles (opt-in)\n --no-agents-md Skip auto-loading <workspaceRoot>/AGENTS.md\n --no-system-md Skip auto-loading the global SYSTEM.md layer\n --agents-md-file <path> Read AGENTS.md from <path> instead (trust-gated, source: app)\n --system-md-file <path> Read SYSTEM.md from <path> instead (source: user)\n -h, --help Show this help\n";
66
+ export declare const usage = "Usage: prism [--mode print|json|rpc] [-p prompt] [options]\n prism init <dir> [--provider <name>] [--with-workflows] [--with-evals] [--force]\n\nOptions:\n -p, --prompt <text> Prompt to run in print/json mode\n --provider <name> Explicit provider id (mock is built in for smoke tests)\n --model <name> Explicit model name\n --session <id> Session id\n --system <text> System instructions\n --context <text> Context text\n --compact <entries> Auto-compaction threshold\n --max-tool-rounds <n> Maximum tool rounds\n --discover Enable workspace contribution discovery (opt-in)\n --discover-kinds <csv> Kinds to discover (default: skill; skill,tool,context,instructions)\n --no-discovery Disable discovery even if --discover is set\n --agents-config <path> App config root holding agents/<name>/AGENT.md bundles (opt-in)\n --no-agents-md Skip auto-loading <workspaceRoot>/AGENTS.md\n --no-system-md Skip auto-loading the global SYSTEM.md layer\n --agents-md-file <path> Read AGENTS.md from <path> instead (trust-gated, source: app)\n --system-md-file <path> Read SYSTEM.md from <path> instead (source: user)\n -h, --help Show this help\n";
71
67
  export declare function parseCliArgs(argv: readonly string[]): CliOptions;
72
68
  export declare function runCli(argv: readonly string[], runtime: CliRuntime): Promise<number>;
73
69
  export declare function runPromptMode(session: AgentSession, options: CliOptions, stdout: Writable, mode: "print" | "json"): Promise<void>;
@@ -19,10 +19,6 @@ Options:
19
19
  --provider <name> Explicit provider id (mock is built in for smoke tests)
20
20
  --model <name> Explicit model name
21
21
  --session <id> Session id
22
- --config <path> Explicit config path (recorded, not auto-loaded)
23
- --resource <uri> Explicit resource URI (recorded, not auto-loaded)
24
- --extension <name> Explicit extension name (recorded, not auto-loaded)
25
- --tool <name> Explicit tool name (recorded, not auto-enabled)
26
22
  --system <text> System instructions
27
23
  --context <text> Context text
28
24
  --compact <entries> Auto-compaction threshold
@@ -43,10 +39,6 @@ const valueFlags = new Set([
43
39
  "--provider",
44
40
  "--model",
45
41
  "--session",
46
- "--config",
47
- "--resource",
48
- "--extension",
49
- "--tool",
50
42
  "--system",
51
43
  "--context",
52
44
  "--compact",
@@ -59,6 +51,9 @@ const valueFlags = new Set([
59
51
  "--agents-config",
60
52
  ]);
61
53
  const boolFlags = new Set(["--discover", "--no-discovery", "--no-agents-md", "--no-system-md"]);
54
+ // Known-but-inert flags: parsed by earlier builds, never wired to any behavior.
55
+ // Rejected loudly (rather than silently ignored) until a CLI-harness plan wires them.
56
+ const unsupportedFlags = new Set(["--config", "--resource", "--extension", "--tool"]);
62
57
  const ALL_KINDS = ["skill", "tool", "context", "instructions"];
63
58
  export function parseCliArgs(argv) {
64
59
  let mode = "print";
@@ -78,10 +73,6 @@ export function parseCliArgs(argv) {
78
73
  let agentsMdFile;
79
74
  let systemMdFile;
80
75
  let agentsConfig;
81
- const config = [];
82
- const resources = [];
83
- const extensions = [];
84
- const tools = [];
85
76
  const context = [];
86
77
  const instructions = [];
87
78
  const injectorFiles = [];
@@ -92,6 +83,8 @@ export function parseCliArgs(argv) {
92
83
  continue;
93
84
  }
94
85
  const name = flag === "-p" ? "--prompt" : flag;
86
+ if (unsupportedFlags.has(name))
87
+ throw new CliUsageError(`${name} is not supported in this build`);
95
88
  if (boolFlags.has(name)) {
96
89
  switch (name) {
97
90
  case "--discover":
@@ -133,18 +126,6 @@ export function parseCliArgs(argv) {
133
126
  case "--session":
134
127
  session = value;
135
128
  break;
136
- case "--config":
137
- config.push(value);
138
- break;
139
- case "--resource":
140
- resources.push(value);
141
- break;
142
- case "--extension":
143
- extensions.push(value);
144
- break;
145
- case "--tool":
146
- tools.push(value);
147
- break;
148
129
  case "--system":
149
130
  system = value;
150
131
  break;
@@ -183,10 +164,6 @@ export function parseCliArgs(argv) {
183
164
  provider,
184
165
  model,
185
166
  session,
186
- config,
187
- resources,
188
- extensions,
189
- tools,
190
167
  system,
191
168
  context,
192
169
  compact,
@@ -48,7 +48,7 @@ export function getContextBudgetReport(request) {
48
48
  }
49
49
  export function applyContextBudget(options) {
50
50
  const budget = resolveContextBudget(options.budget);
51
- const layout = options.layout ?? "legacy";
51
+ const layout = options.layout ?? "cache_aware";
52
52
  const groups = {
53
53
  instructions: [...options.groups.instructions],
54
54
  summaries: [...options.groups.summaries],
@@ -61,16 +61,19 @@ export function applyContextBudget(options) {
61
61
  const skills = [...(options.skills ?? [])].filter((skill) => skill.instructions);
62
62
  const tools = options.tools ? [...options.tools] : undefined;
63
63
  const omitted = [];
64
- const cost = () => measureAll(groups, context, skills, tools);
65
- while (overBudget(cost(), budget)) {
64
+ // Measure once, then subtract each dropped item's own estimate (dropNext computes it
65
+ // with the same estimators) — avoids an O(n²) re-scan of the full keep-set per drop.
66
+ const kept = measureAll(groups, context, skills, tools);
67
+ while (overBudget(kept, budget)) {
66
68
  const drop = dropNext(groups, context, skills, layout);
67
69
  if (!drop) {
68
70
  throw new ContextBudgetError();
69
71
  }
72
+ kept.tokens -= drop.tokenEstimate;
73
+ kept.bytes -= drop.byteLength;
70
74
  if (omitted.length < HARD_MAX_CONTEXT_BUDGET_OMISSIONS)
71
75
  omitted.push(drop);
72
76
  }
73
- const kept = cost();
74
77
  const reportOmissions = omitted.slice(0, DEFAULT_MAX_CONTEXT_BUDGET_OMISSIONS);
75
78
  return {
76
79
  groups,
@@ -88,8 +91,8 @@ export function applyContextBudget(options) {
88
91
  };
89
92
  }
90
93
  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.
94
+ // ponytail: drop droppable groups in layout order; within history, drop oldest first (shift).
95
+ // cache_aware keeps attachments longer so stable prefix stays intact while budget still allows it.
93
96
  const order = layout === "cache_aware"
94
97
  ? ["tool_results", "history", "summaries", "context", "skills", "attachments"]
95
98
  : ["tool_results", "history", "summaries", "attachments", "context", "skills"];
@@ -99,7 +102,7 @@ function dropNext(groups, context, skills, layout) {
99
102
  return omission("tool_results", message.id ?? toolResultId(message), message);
100
103
  }
101
104
  if (kind === "history" && groups.history.length > 0) {
102
- const message = groups.history.pop();
105
+ const message = groups.history.shift();
103
106
  return omission("history", message.id, message);
104
107
  }
105
108
  if (kind === "summaries" && groups.summaries.length > 0) {
@@ -15,6 +15,9 @@ export interface ErrorInfo {
15
15
  readonly name?: string;
16
16
  readonly message: string;
17
17
  readonly code?: string | number;
18
+ /** Provider backpressure hint (e.g. from a `Retry-After` header); retry policies
19
+ * honor it capped at their own `maxDelayMs`. */
20
+ readonly retryAfterMs?: number;
18
21
  readonly cause?: unknown;
19
22
  }
20
23
  export type { AudioContent, DocumentContent, FileContent } from "./content.js";
@@ -749,6 +752,13 @@ export type AgentEvent = {
749
752
  readonly sessionId: string;
750
753
  readonly runId: string;
751
754
  readonly size: number;
755
+ } | {
756
+ /** A steered message was dropped by a terminal input guardrail; the run continues without it. */
757
+ readonly type: "steer_rejected";
758
+ readonly sessionId: string;
759
+ readonly runId: string;
760
+ readonly message: Message;
761
+ readonly record: GuardrailRecord;
752
762
  } | {
753
763
  readonly type: "event_subscriber_overflow";
754
764
  readonly sessionId: string;
@@ -937,6 +947,9 @@ export interface PromptBuildRequest {
937
947
  readonly context?: readonly ContextBlock[];
938
948
  readonly skills?: readonly Skill[];
939
949
  readonly tools?: readonly ToolDefinition[];
950
+ /** Model being prompted; lets builders adapt composition to declared capabilities
951
+ * (e.g. the default builder omits the `Available tools:` text for tool-capable models). */
952
+ readonly model?: ModelConfig;
940
953
  readonly metadata?: Readonly<Record<string, unknown>>;
941
954
  readonly signal?: AbortSignal;
942
955
  }
@@ -4,6 +4,8 @@ import { type ProviderRegistry } from "./providers.js";
4
4
  import { type DuplicateRegistrationOptions } from "./registry-options.js";
5
5
  export interface ContributionRegistry<T> {
6
6
  register(key: string, contribution: T): void;
7
+ /** Remove a contribution; returns false when the key was not registered. */
8
+ unregister(key: string): boolean;
7
9
  get(key: string): T | undefined;
8
10
  resolve(key: string): T;
9
11
  list(): readonly T[];
@@ -9,6 +9,9 @@ export function createContributionRegistry(options = {}) {
9
9
  assertCanRegister(byKey, key, label, key, options.duplicate);
10
10
  byKey.set(key, contribution);
11
11
  },
12
+ unregister(key) {
13
+ return byKey.delete(key);
14
+ },
12
15
  get(key) {
13
16
  return byKey.get(key);
14
17
  },
@@ -10,7 +10,13 @@ export interface MemoryCredentialStore extends CredentialResolver {
10
10
  delete(request: Pick<CredentialRequest, "name" | "provider">): boolean;
11
11
  clear(): void;
12
12
  }
13
- export declare function createMemoryCredentialStore(initial?: readonly CredentialRecord[]): MemoryCredentialStore;
13
+ export interface MemoryCredentialStoreOptions {
14
+ /** When true (default, backward compatible), a provider-scoped request falls back to a
15
+ * providerless record of the same name — that record is then served to EVERY provider.
16
+ * Set false for exact-match-only resolution (strict provider scoping). */
17
+ readonly allowProviderFallback?: boolean;
18
+ }
19
+ export declare function createMemoryCredentialStore(initial?: readonly CredentialRecord[], options?: MemoryCredentialStoreOptions): MemoryCredentialStore;
14
20
  export declare function createChainedCredentialResolver(resolvers: readonly CredentialResolver[]): CredentialResolver;
15
21
  export declare function createExplicitCredentialResolver(sources: readonly CredentialResolverSource[]): CredentialResolver;
16
22
  export declare function createEnvCredentialResolver(env: Readonly<Record<string, string | undefined>>, map: Readonly<Record<string, string>>): CredentialResolver;
@@ -1,5 +1,6 @@
1
- export function createMemoryCredentialStore(initial = []) {
1
+ export function createMemoryCredentialStore(initial = [], options = {}) {
2
2
  const records = new Map();
3
+ const allowProviderFallback = options.allowProviderFallback ?? true;
3
4
  const key = (name, provider) => `${provider ?? ""}\u0000${name}`;
4
5
  const store = {
5
6
  set(record) {
@@ -12,7 +13,10 @@ export function createMemoryCredentialStore(initial = []) {
12
13
  records.clear();
13
14
  },
14
15
  resolve(request) {
15
- return records.get(key(request.name, request.provider)) ?? records.get(key(request.name));
16
+ const exact = records.get(key(request.name, request.provider));
17
+ if (exact !== undefined || !allowProviderFallback || request.provider === undefined)
18
+ return exact;
19
+ return records.get(key(request.name));
16
20
  },
17
21
  };
18
22
  for (const record of initial)
@@ -13,10 +13,21 @@ export function createEventMultiplexer(options = {}) {
13
13
  isClosed = true;
14
14
  else
15
15
  options.signal?.addEventListener("abort", abort, { once: true });
16
+ // With compare set, publish always enqueues and the parked consumer is woken with
17
+ // this token instead of a value, so every event drains through the sorted queue —
18
+ // delivery order follows the comparator even when the consumer is caught up.
19
+ const WAKE = Symbol("wake");
20
+ const wakeWaiter = () => {
21
+ if (!waiter)
22
+ return;
23
+ const resolve = waiter;
24
+ waiter = undefined;
25
+ resolve({ value: WAKE, done: false });
26
+ };
16
27
  function publish(event) {
17
28
  if (isClosed)
18
29
  return;
19
- if (waiter) {
30
+ if (!options.compare && waiter) {
20
31
  const resolve = waiter;
21
32
  waiter = undefined;
22
33
  resolve({ value: event, done: false });
@@ -24,6 +35,7 @@ export function createEventMultiplexer(options = {}) {
24
35
  }
25
36
  if (queue.length < maxQueuedEvents) {
26
37
  queue.push(event);
38
+ wakeWaiter();
27
39
  return;
28
40
  }
29
41
  droppedEvents += 1;
@@ -41,6 +53,7 @@ export function createEventMultiplexer(options = {}) {
41
53
  if (overflow === "drop_newest") {
42
54
  if (notice !== undefined)
43
55
  queue[queue.length - 1] = notice;
56
+ wakeWaiter();
44
57
  return;
45
58
  }
46
59
  queue.shift();
@@ -50,6 +63,7 @@ export function createEventMultiplexer(options = {}) {
50
63
  queue.shift();
51
64
  }
52
65
  queue.push(event);
66
+ wakeWaiter();
53
67
  }
54
68
  function observe(source, map) {
55
69
  if (isClosed)
@@ -115,6 +129,8 @@ export function createEventMultiplexer(options = {}) {
115
129
  });
116
130
  if (next.done)
117
131
  return;
132
+ if (next.value === WAKE)
133
+ continue;
118
134
  yield next.value;
119
135
  }
120
136
  }
@@ -26,11 +26,17 @@ export interface ExtensionEventBus {
26
26
  on(type: ExtensionLifecycleEventName | string, handler: ExtensionEventHandler): () => void;
27
27
  emit(event: ExtensionEvent): Promise<void>;
28
28
  }
29
+ export interface LoadedExtension {
30
+ readonly name: string;
31
+ /** Remove this extension's registry contributions and middleware/event subscriptions.
32
+ * Best-effort and idempotent; side effects outside the registries are NOT unwound. */
33
+ dispose(): void;
34
+ }
29
35
  export interface ExtensionKernel {
30
36
  readonly registries: ContributionRegistries;
31
37
  readonly middleware: MiddlewareRegistry;
32
38
  readonly events: ExtensionEventBus;
33
- load(extensions: readonly Extension[]): Promise<void>;
39
+ load(extensions: readonly Extension[]): Promise<LoadedExtension[]>;
34
40
  }
35
41
  export declare function createExtensionEventBus(options?: Pick<ExtensionKernelOptions, "errorPolicy" | "secrets">): ExtensionEventBus;
36
42
  export declare function createExtensionKernel(options?: ExtensionKernelOptions): ExtensionKernel;