@arnilo/prism 0.0.16 → 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 (54) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/agent-run-state.js +13 -1
  3. package/dist/agents.js +42 -10
  4. package/dist/checkpoints.d.ts +4 -0
  5. package/dist/checkpoints.js +12 -0
  6. package/dist/cli-runner.d.ts +1 -5
  7. package/dist/cli-runner.js +5 -28
  8. package/dist/context-budget.js +6 -3
  9. package/dist/contracts.d.ts +13 -0
  10. package/dist/contributions.d.ts +2 -0
  11. package/dist/contributions.js +3 -0
  12. package/dist/credentials.d.ts +7 -1
  13. package/dist/credentials.js +6 -2
  14. package/dist/event-multiplexer.js +17 -1
  15. package/dist/extensions.d.ts +7 -1
  16. package/dist/extensions.js +64 -6
  17. package/dist/feedback.js +1 -1
  18. package/dist/guardrails.js +9 -3
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js +1 -1
  21. package/dist/input.js +11 -4
  22. package/dist/middleware.js +9 -1
  23. package/dist/models.d.ts +2 -0
  24. package/dist/models.js +3 -0
  25. package/dist/providers/openai-compatible.d.ts +42 -1
  26. package/dist/providers/openai-compatible.js +109 -47
  27. package/dist/providers/transport.d.ts +6 -0
  28. package/dist/providers/transport.js +21 -0
  29. package/dist/providers.d.ts +2 -0
  30. package/dist/providers.js +3 -0
  31. package/dist/redaction.js +21 -7
  32. package/dist/retry.d.ts +5 -0
  33. package/dist/retry.js +8 -1
  34. package/dist/run-ledger.d.ts +6 -0
  35. package/dist/run-ledger.js +3 -9
  36. package/dist/session-stores.js +15 -11
  37. package/docs/agent-events.md +2 -1
  38. package/docs/agent-session-runtime.md +2 -2
  39. package/docs/cli-rpc.md +1 -5
  40. package/docs/coding-agent-tools.md +2 -0
  41. package/docs/compaction-and-retry.md +3 -1
  42. package/docs/contribution-registries.md +1 -0
  43. package/docs/credentials-and-redaction.md +1 -1
  44. package/docs/extensions.md +1 -1
  45. package/docs/guardrails.md +13 -2
  46. package/docs/index.md +2 -2
  47. package/docs/input-and-prompt-assembly.md +3 -3
  48. package/docs/middleware-hooks.md +2 -2
  49. package/docs/migration.md +11 -0
  50. package/docs/providers/openai-compatible.md +28 -1
  51. package/docs/public-contracts.md +1 -1
  52. package/docs/release-and-install.md +36 -15
  53. package/docs/session-stores.md +1 -1
  54. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
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
+
3
23
  ## [0.0.16] - 2026-07-26
4
24
 
5
25
  ### Added
@@ -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,
@@ -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,
@@ -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;
@@ -45,90 +45,148 @@ export function createExtensionKernel(options = {}) {
45
45
  const middleware = options.middleware ?? createMiddlewareRegistry({ ...options, onError: events.emit });
46
46
  const errorPolicy = options.errorPolicy ?? "event";
47
47
  const secrets = options.secrets ?? [];
48
- const api = {
48
+ // Per-extension tracked API: every registration records an undo so a dispose handle
49
+ // (or a failed setup) can unwind exactly what that extension added.
50
+ const createApi = (track) => ({
49
51
  registries,
50
52
  middleware,
51
- on: events.on,
53
+ on(type, handler) {
54
+ const off = events.on(type, handler);
55
+ track?.(off);
56
+ return off;
57
+ },
52
58
  emit: events.emit,
53
- use: middleware.use,
59
+ use(hook, mw) {
60
+ const off = middleware.use(hook, mw);
61
+ track?.(off);
62
+ return off;
63
+ },
54
64
  registerProvider(provider) {
55
65
  registries.providers.register(provider);
66
+ track?.(() => registries.providers.unregister(provider.id));
56
67
  },
57
68
  registerModel(model) {
58
69
  registries.models.register(model);
70
+ track?.(() => registries.models.unregister(model.provider, model.model));
59
71
  },
60
72
  registerTool(tool) {
61
73
  registries.tools.register(tool.name, tool);
74
+ track?.(() => registries.tools.unregister(tool.name));
62
75
  },
63
76
  registerContextProvider(provider) {
64
77
  registries.contextProviders.register(provider.name, provider);
78
+ track?.(() => registries.contextProviders.unregister(provider.name));
65
79
  },
66
80
  registerSkill(skill) {
67
81
  registries.skills.register(skill.name, skill);
82
+ track?.(() => registries.skills.unregister(skill.name));
68
83
  },
69
84
  registerCommand(command) {
70
85
  registries.commands.register(command.name, command);
86
+ track?.(() => registries.commands.unregister(command.name));
71
87
  },
72
88
  registerAgent(agent) {
73
89
  registries.agents.register(agent.name, agent);
90
+ track?.(() => registries.agents.unregister(agent.name));
74
91
  },
75
92
  registerInputBuilder(builder) {
76
93
  registries.inputBuilders.register(builder.name, builder);
94
+ track?.(() => registries.inputBuilders.unregister(builder.name));
77
95
  },
78
96
  registerPromptBuilder(builder) {
79
97
  registries.promptBuilders.register(builder.name, builder);
98
+ track?.(() => registries.promptBuilders.unregister(builder.name));
80
99
  },
81
100
  registerCompactionStrategy(strategy) {
82
101
  registries.compactionStrategies.register(strategy.name, strategy);
102
+ track?.(() => registries.compactionStrategies.unregister(strategy.name));
83
103
  },
84
104
  registerRetryPolicy(policy) {
85
105
  registries.retryPolicies.register(policy.name, policy);
106
+ track?.(() => registries.retryPolicies.unregister(policy.name));
86
107
  },
87
108
  registerStoreFactory(factory) {
88
109
  registries.storeFactories.register(factory.name, factory);
110
+ track?.(() => registries.storeFactories.unregister(factory.name));
89
111
  },
90
112
  registerResourceLoader(key, loader) {
91
113
  registries.resourceLoaders.register(key, loader);
114
+ track?.(() => registries.resourceLoaders.unregister(key));
92
115
  },
93
116
  registerSettingsProvider(key, provider) {
94
117
  registries.settingsProviders.register(key, provider);
118
+ track?.(() => registries.settingsProviders.unregister(key));
95
119
  },
96
120
  registerCredentialResolver(key, resolver) {
97
121
  registries.credentialResolvers.register(key, resolver);
122
+ track?.(() => registries.credentialResolvers.unregister(key));
98
123
  },
99
124
  registerProviderPackage(providerPackage) {
100
125
  registries.providerPackages.register(providerPackage.name, providerPackage);
126
+ track?.(() => registries.providerPackages.unregister(providerPackage.name));
101
127
  },
102
128
  registerAuthMethod(method) {
103
- registries.authMethods.register(authMethodKey(method), method);
129
+ const key = authMethodKey(method);
130
+ registries.authMethods.register(key, method);
131
+ track?.(() => registries.authMethods.unregister(key));
104
132
  },
105
133
  registerProviderRequestPolicy(policy) {
106
134
  registries.providerRequestPolicies.register(policy.name, policy);
135
+ track?.(() => registries.providerRequestPolicies.unregister(policy.name));
107
136
  },
108
137
  registerSystemPromptContribution(contribution) {
109
- registries.systemPromptContributions.register(systemPromptContributionKey(contribution), contribution);
138
+ const key = systemPromptContributionKey(contribution);
139
+ registries.systemPromptContributions.register(key, contribution);
140
+ track?.(() => registries.systemPromptContributions.unregister(key));
110
141
  },
111
142
  registerInstructionInjector(injector) {
112
143
  registries.instructionInjectors.register(injector.name, injector);
144
+ track?.(() => registries.instructionInjectors.unregister(injector.name));
113
145
  },
146
+ });
147
+ const unwind = (undo) => {
148
+ for (const fn of undo.reverse()) {
149
+ try {
150
+ fn();
151
+ }
152
+ catch {
153
+ // best-effort: one stuck undo must not block the rest
154
+ }
155
+ }
114
156
  };
115
157
  return {
116
158
  registries,
117
159
  middleware,
118
160
  events,
119
161
  async load(extensions) {
162
+ const loaded = [];
120
163
  for (const extension of extensions) {
164
+ const undo = [];
121
165
  try {
122
166
  await assertPermission(options.permission, { kind: "extension", action: "setup", target: extension.name });
123
167
  await assertExtensionLoadPolicy(options.loadPolicy, extension);
124
- await extension.setup(api);
168
+ await extension.setup(createApi((fn) => undo.push(fn)));
125
169
  }
126
170
  catch (error) {
171
+ // A failed setup must not leave partial contributions behind.
172
+ unwind(undo);
127
173
  if (errorPolicy === "throw")
128
174
  throw error;
129
175
  await events.emit(extensionError(error, extension.name, secrets));
176
+ continue;
130
177
  }
178
+ let disposed = false;
179
+ loaded.push({
180
+ name: extension.name,
181
+ dispose() {
182
+ if (disposed)
183
+ return;
184
+ disposed = true;
185
+ unwind(undo);
186
+ },
187
+ });
131
188
  }
189
+ return loaded;
132
190
  },
133
191
  };
134
192
  }