@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/dist/redaction.js CHANGED
@@ -1,4 +1,7 @@
1
1
  const REDACTED = "[REDACTED]";
2
+ // Depth bound matching agent-run-state.ts; hostile deep structures yield a placeholder
3
+ // instead of a stack overflow.
4
+ const MAX_REDACT_DEPTH = 32;
2
5
  export function createSecretRedactor(secrets) {
3
6
  return { redact: (value) => redactSecrets(value, secrets) };
4
7
  }
@@ -17,6 +20,9 @@ export function redactProviderRequest(request, redactor) {
17
20
  export function redactRunLedgerRecord(record, redactor) {
18
21
  return redactor?.redact(record) ?? record;
19
22
  }
23
+ export function resolveRedactor(redactor, secrets) {
24
+ return redactor ?? (secrets?.some((secret) => Boolean(secret)) ? createSecretRedactor(secrets) : undefined);
25
+ }
20
26
  export function redactSecrets(value, secrets) {
21
27
  const needles = secrets.filter((secret) => Boolean(secret));
22
28
  if (needles.length === 0)
@@ -28,23 +34,25 @@ export function redactSecrets(value, secrets) {
28
34
  return String(key);
29
35
  };
30
36
  const assignKey = (target, key, value) => {
31
- if (!Object.prototype.hasOwnProperty.call(target, key)) {
37
+ if (!Object.hasOwn(target, key)) {
32
38
  target[key] = value;
33
39
  return;
34
40
  }
35
41
  let suffix = 2;
36
- while (Object.prototype.hasOwnProperty.call(target, `${key}__${suffix}`))
42
+ while (Object.hasOwn(target, `${key}__${suffix}`))
37
43
  suffix += 1;
38
44
  target[`${key}__${suffix}`] = value;
39
45
  };
40
46
  // ponytail: active-path WeakSet marks only ancestor cycles as [Circular]; shared
41
47
  // references (diamonds) are visited again on separate branches. Map/Set normalize
42
48
  // to JSON-shaped output; string keys are redacted like values.
43
- const redact = (input, active = new WeakSet()) => {
49
+ const redact = (input, active = new WeakSet(), depth = 0) => {
44
50
  if (typeof input === "string")
45
51
  return redactString(input);
46
52
  if (input === null || typeof input !== "object")
47
53
  return input;
54
+ if (depth >= MAX_REDACT_DEPTH)
55
+ return "[MaxDepth]";
48
56
  if (input instanceof Date || input instanceof RegExp)
49
57
  return input;
50
58
  if (ArrayBuffer.isView(input) || input instanceof ArrayBuffer)
@@ -54,18 +62,18 @@ export function redactSecrets(value, secrets) {
54
62
  active.add(input);
55
63
  try {
56
64
  if (Array.isArray(input))
57
- return input.map((item) => redact(item, active));
65
+ return input.map((item) => redact(item, active, depth + 1));
58
66
  if (input instanceof Map) {
59
67
  const out = {};
60
68
  for (const [key, item] of input)
61
- assignKey(out, redactKey(key), redact(item, active));
69
+ assignKey(out, redactKey(key), redact(item, active, depth + 1));
62
70
  return out;
63
71
  }
64
72
  if (input instanceof Set)
65
- return [...input].map((item) => redact(item, active));
73
+ return [...input].map((item) => redact(item, active, depth + 1));
66
74
  const out = {};
67
75
  for (const [key, item] of Object.entries(input))
68
- assignKey(out, redactKey(key), redact(item, active));
76
+ assignKey(out, redactKey(key), redact(item, active, depth + 1));
69
77
  return out;
70
78
  }
71
79
  finally {
@@ -76,18 +84,21 @@ export function redactSecrets(value, secrets) {
76
84
  }
77
85
  export function errorToErrorInfo(error, secrets = []) {
78
86
  const code = readErrorCode(error);
87
+ const retry = readRetryAfterMs(error);
88
+ const retryAfter = retry !== undefined ? { retryAfterMs: retry } : {};
79
89
  if (error instanceof Error) {
80
90
  return {
81
91
  name: error.name,
82
92
  message: redactSecrets(error.message, secrets),
83
93
  code,
94
+ ...retryAfter,
84
95
  cause: error.cause ? redactSecrets(String(error.cause), secrets) : undefined,
85
96
  };
86
97
  }
87
98
  if (error && typeof error === "object" && "message" in error) {
88
- return { message: redactSecrets(String(error.message), secrets), code };
99
+ return { message: redactSecrets(String(error.message), secrets), code, ...retryAfter };
89
100
  }
90
- return { message: redactSecrets(String(error), secrets), code };
101
+ return { message: redactSecrets(String(error), secrets), code, ...retryAfter };
91
102
  }
92
103
  function readErrorCode(error) {
93
104
  if (!error || typeof error !== "object" || !("code" in error))
@@ -95,4 +106,10 @@ function readErrorCode(error) {
95
106
  const code = error.code;
96
107
  return typeof code === "string" || typeof code === "number" ? code : undefined;
97
108
  }
109
+ function readRetryAfterMs(error) {
110
+ if (!error || typeof error !== "object" || !("retryAfterMs" in error))
111
+ return undefined;
112
+ const value = error.retryAfterMs;
113
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
114
+ }
98
115
  //# sourceMappingURL=redaction.js.map
@@ -1,6 +1,6 @@
1
- import { type PrismManifest } from "./manifests.js";
2
- import type { JsonObject, ResourceLoader, ResourceLoadContext } from "./contracts.js";
3
1
  import { type MediaContentBounds } from "./content.js";
2
+ import type { JsonObject, ResourceLoadContext, ResourceLoader } from "./contracts.js";
3
+ import { type PrismManifest } from "./manifests.js";
4
4
  export declare function loadTextResource(loader: ResourceLoader, uri: string, context?: ResourceLoadContext): Promise<string>;
5
5
  export declare function loadJsonResource(loader: ResourceLoader, uri: string, context?: ResourceLoadContext): Promise<JsonObject>;
6
6
  export declare function loadManifestResource(loader: ResourceLoader, uri: string, context?: ResourceLoadContext): Promise<PrismManifest>;
package/dist/resources.js CHANGED
@@ -1,6 +1,6 @@
1
- import { parsePrismManifest } from "./manifests.js";
2
1
  import { assertJsonObject } from "./config.js";
3
- import { DEFAULT_MAX_MEDIA_ITEM_BYTES, loadBoundedBinaryResource, } from "./content.js";
2
+ import { DEFAULT_MAX_MEDIA_ITEM_BYTES, loadBoundedBinaryResource } from "./content.js";
3
+ import { parsePrismManifest } from "./manifests.js";
4
4
  import { assertPermission, assertTrusted } from "./security.js";
5
5
  export async function loadTextResource(loader, uri, context) {
6
6
  await assertTrusted(context?.trust, { kind: "resource", target: uri, capability: "load", metadata: context?.metadata });
package/dist/retry.d.ts CHANGED
@@ -5,6 +5,11 @@ export interface DefaultRetryPolicyOptions {
5
5
  readonly baseDelayMs?: number;
6
6
  readonly maxDelayMs?: number;
7
7
  readonly transientCodes?: readonly (string | number)[];
8
+ /** Symmetric jitter fraction applied to computed delays (default 0.25 → ±25%).
9
+ * Prevents thundering-herd retries under a shared outage. Set 0 to disable. */
10
+ readonly jitter?: number;
11
+ /** Random source for jitter (tests); defaults to Math.random. */
12
+ readonly random?: () => number;
8
13
  }
9
14
  export declare function createDefaultRetryPolicy(options?: DefaultRetryPolicyOptions): RetryPolicy;
10
15
  export declare function isTransientErrorInfo(error: ErrorInfo, transientCodes?: ReadonlySet<string | number>): boolean;
package/dist/retry.js CHANGED
@@ -22,13 +22,20 @@ export function createDefaultRetryPolicy(options = {}) {
22
22
  const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
23
23
  const baseDelayMs = Math.max(0, options.baseDelayMs ?? 100);
24
24
  const maxDelayMs = Math.max(baseDelayMs, options.maxDelayMs ?? 1000);
25
+ const jitter = Math.min(1, Math.max(0, options.jitter ?? 0.25));
26
+ const random = options.random ?? Math.random;
25
27
  const transientCodes = new Set([...TRANSIENT_CODES, ...(options.transientCodes ?? [])]);
26
28
  return {
27
29
  name: options.name ?? "default-retry",
28
30
  decide(context) {
29
31
  if (context.signal?.aborted || context.attempt >= maxAttempts || !isTransientErrorInfo(context.error, transientCodes))
30
32
  return { retry: false };
31
- return { retry: true, delayMs: Math.min(maxDelayMs, baseDelayMs * 2 ** Math.max(0, context.attempt - 1)) };
33
+ // Provider backpressure hint (Retry-After) wins over computed backoff, always
34
+ // capped at maxDelayMs so a hostile/huge hint cannot pin a run.
35
+ const hint = context.error.retryAfterMs;
36
+ const base = Math.min(hint !== undefined && Number.isFinite(hint) && hint >= 0 ? hint : baseDelayMs * 2 ** Math.max(0, context.attempt - 1), maxDelayMs);
37
+ const delayMs = jitter > 0 ? Math.round(base * (1 - jitter + random() * 2 * jitter)) : base;
38
+ return { retry: true, delayMs };
32
39
  },
33
40
  };
34
41
  }
package/dist/rpc.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createInterface } from "node:readline";
2
- import { errorToErrorInfo } from "./redaction.js";
3
2
  import { resolveInstructionInjectors } from "./instruction-injection.js";
3
+ import { errorToErrorInfo } from "./redaction.js";
4
4
  export async function runRpcServer(options) {
5
5
  const first = options.createSession();
6
6
  const state = {
@@ -58,7 +58,11 @@ async function handleRequest(request, state, stdout, activeRuns) {
58
58
  const session = state.current;
59
59
  const existing = activeRuns.get(session.id);
60
60
  if (existing) {
61
- write(stdout, { id: request.id, ok: false, error: errorToErrorInfo(new Error(`Session ${session.id} already has an active run for request ${existing.requestId}`)) });
61
+ write(stdout, {
62
+ id: request.id,
63
+ ok: false,
64
+ error: errorToErrorInfo(new Error(`Session ${session.id} already has an active run for request ${existing.requestId}`)),
65
+ });
62
66
  return;
63
67
  }
64
68
  // ponytail: Phase 30 — resolve run options (incl. instructionInjectors names) BEFORE opening the
@@ -74,9 +78,11 @@ async function handleRequest(request, state, stdout, activeRuns) {
74
78
  await events;
75
79
  }
76
80
  write(stdout, { id: request.id, ok: true, result: { sessionId: session.id } });
77
- })().catch((error) => {
81
+ })()
82
+ .catch((error) => {
78
83
  write(stdout, { id: request.id, ok: false, error: errorToErrorInfo(error) });
79
- }).finally(() => {
84
+ })
85
+ .finally(() => {
80
86
  activeRuns.delete(session.id);
81
87
  });
82
88
  activeRuns.set(session.id, { requestId: request.id, promise });
@@ -100,16 +106,24 @@ async function handleRequest(request, state, stdout, activeRuns) {
100
106
  write(stdout, { id: request.id, ok: true, result: { sessionId: state.current.id } });
101
107
  break;
102
108
  case "state":
103
- write(stdout, { id: request.id, ok: true, result: {
109
+ write(stdout, {
110
+ id: request.id,
111
+ ok: true,
112
+ result: {
104
113
  sessionId: state.current.id,
105
114
  leafId: state.current.leafId,
106
115
  handleId: state.currentHandleId,
107
116
  // ponytail: `sessions` kept as a backward-compatible handle-id string list (== sessionId
108
117
  // for the initial session and clones); `handles` is the branch-handle detail view.
109
118
  sessions: [...state.sessions.keys()],
110
- handles: [...state.sessions.entries()].map(([handleId, session]) => ({ handleId, sessionId: session.id, leafId: session.leafId })),
119
+ handles: [...state.sessions.entries()].map(([handleId, session]) => ({
120
+ handleId,
121
+ sessionId: session.id,
122
+ leafId: session.leafId,
123
+ })),
111
124
  model: state.model,
112
- } });
125
+ },
126
+ });
113
127
  break;
114
128
  case "messages":
115
129
  write(stdout, { id: request.id, ok: true, result: { sessionId: state.current.id, entries: await state.current.entries() } });
@@ -152,7 +166,11 @@ async function handleRequest(request, state, stdout, activeRuns) {
152
166
  if (!leafId)
153
167
  throw new Error("checkout requires params.leafId");
154
168
  await state.current.checkout(leafId);
155
- write(stdout, { id: request.id, ok: true, result: { sessionId: state.current.id, leafId: state.current.leafId, handleId: state.currentHandleId } });
169
+ write(stdout, {
170
+ id: request.id,
171
+ ok: true,
172
+ result: { sessionId: state.current.id, leafId: state.current.leafId, handleId: state.currentHandleId },
173
+ });
156
174
  break;
157
175
  }
158
176
  case "command": {
@@ -245,7 +263,22 @@ function isObject(value) {
245
263
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
246
264
  }
247
265
  function isCommand(value) {
248
- return typeof value === "string" && ["prompt", "steer", "followUp", "abort", "state", "messages", "setModel", "compact", "switchSession", "forkSession", "cloneSession", "checkout", "command"].includes(value);
266
+ return (typeof value === "string" &&
267
+ [
268
+ "prompt",
269
+ "steer",
270
+ "followUp",
271
+ "abort",
272
+ "state",
273
+ "messages",
274
+ "setModel",
275
+ "compact",
276
+ "switchSession",
277
+ "forkSession",
278
+ "cloneSession",
279
+ "checkout",
280
+ "command",
281
+ ].includes(value));
249
282
  }
250
283
  function readRequestId(value) {
251
284
  return isObject(value) && (typeof value.id === "string" || typeof value.id === "number") ? value.id : null;
@@ -6,9 +6,15 @@ export declare const HARD_LEDGER_BATCH_BYTES: number;
6
6
  export declare const DEFAULT_LEDGER_BATCH_DELAY_MS = 25;
7
7
  export declare const HARD_LEDGER_BATCH_DELAY_MS = 60000;
8
8
  export interface BatchedRunLedgerOptions {
9
+ /** Flush triggers, not write grouping: a pending flush fires once the buffer reaches
10
+ * this many entries. Writes still go to the target one at a time (RunLedger has no
11
+ * batch append); these bounds shape flush *timing*, not per-write coalescing. */
9
12
  readonly maxBatchEntries?: number;
13
+ /** Flush trigger: pending flush fires once buffered bytes reach this bound. */
10
14
  readonly maxBatchBytes?: number;
15
+ /** Backpressure bound: enqueue flushes synchronously before exceeding this. */
11
16
  readonly maxBufferedEntries?: number;
17
+ /** Backpressure bound (bytes): enqueue flushes synchronously before exceeding this. */
12
18
  readonly maxBufferedBytes?: number;
13
19
  readonly maxDelayMs?: number;
14
20
  readonly durability?: RunLedgerDurability;
@@ -32,12 +32,18 @@ export function createBatchedRunLedger(target, options = {}) {
32
32
  let flushChain = Promise.resolve();
33
33
  let disposed = false;
34
34
  const status = () => ({ accepted, flushed, buffered: queue.length });
35
- const cancelTimer = () => { if (timer)
36
- clearTimeout(timer); timer = undefined; };
35
+ const cancelTimer = () => {
36
+ if (timer)
37
+ clearTimeout(timer);
38
+ timer = undefined;
39
+ };
37
40
  const schedule = () => {
38
41
  if (timer || disposed || queue.length === 0)
39
42
  return;
40
- timer = setTimeout(() => { timer = undefined; void flush().catch(() => undefined); }, maxDelayMs);
43
+ timer = setTimeout(() => {
44
+ timer = undefined;
45
+ void flush().catch(() => undefined);
46
+ }, maxDelayMs);
41
47
  timer.unref?.();
42
48
  };
43
49
  const write = (item) => {
@@ -52,20 +58,14 @@ export function createBatchedRunLedger(target, options = {}) {
52
58
  const flush = () => {
53
59
  cancelTimer();
54
60
  const operation = flushChain.then(async () => {
55
- let entries = 0;
56
- let bytes = 0;
61
+ // One write per record — RunLedger has no batch append API. maxBatchEntries/
62
+ // maxBatchBytes only decide when a flush fires (see enqueue), never how writes group.
57
63
  while (queue.length) {
58
64
  const item = queue[0];
59
- if (entries && (entries >= maxBatchEntries || bytes + item.bytes > maxBatchBytes)) {
60
- entries = 0;
61
- bytes = 0;
62
- }
63
- await write(item);
65
+ await write(item); // shifts only after success — a failed write stays buffered for retry
64
66
  queue.shift();
65
67
  bufferedBytes -= item.bytes;
66
68
  flushed += 1;
67
- entries += 1;
68
- bytes += item.bytes;
69
69
  }
70
70
  return status();
71
71
  });
@@ -83,7 +83,10 @@ export function createBatchedRunLedger(target, options = {}) {
83
83
  queue.push({ ...item, bytes });
84
84
  bufferedBytes += bytes;
85
85
  accepted += 1;
86
- if (durability === "write_through" || queue.length >= maxBatchEntries || bufferedBytes >= maxBatchBytes || (item.kind === "run" && terminal(item.record) && durability === "flush_on_terminal"))
86
+ if (durability === "write_through" ||
87
+ queue.length >= maxBatchEntries ||
88
+ bufferedBytes >= maxBatchBytes ||
89
+ (item.kind === "run" && terminal(item.record) && durability === "flush_on_terminal"))
87
90
  await flush();
88
91
  else
89
92
  schedule();
@@ -58,9 +58,23 @@ export function resolveRunLimits(agent, run) {
58
58
  resolved[name] = base ? Math.min(resolved[name], override[name]) : override[name];
59
59
  }
60
60
  const maxCost = override?.maxCost ?? base?.maxCost;
61
- return Object.freeze({ ...resolved, ...(maxCost ? { maxCost: base?.maxCost && override?.maxCost ? { amount: Math.min(base.maxCost.amount, override.maxCost.amount), currency: base.maxCost.currency === override.maxCost.currency ? base.maxCost.currency : failCurrency() } : maxCost } : {}) });
61
+ return Object.freeze({
62
+ ...resolved,
63
+ ...(maxCost
64
+ ? {
65
+ maxCost: base?.maxCost && override?.maxCost
66
+ ? {
67
+ amount: Math.min(base.maxCost.amount, override.maxCost.amount),
68
+ currency: base.maxCost.currency === override.maxCost.currency ? base.maxCost.currency : failCurrency(),
69
+ }
70
+ : maxCost,
71
+ }
72
+ : {}),
73
+ });
74
+ }
75
+ function failCurrency() {
76
+ throw new TypeError("Run limit currencies must match when narrowed");
62
77
  }
63
- function failCurrency() { throw new TypeError("Run limit currencies must match when narrowed"); }
64
78
  function validateLimits(input) {
65
79
  for (const name of LIMIT_NAMES) {
66
80
  const value = input[name];
@@ -88,7 +102,20 @@ export class RunLimitTracker {
88
102
  constructor(limits, options = {}) {
89
103
  this.options = options;
90
104
  this.limits = limits;
91
- this.counters = { turns: 0, providerAttempts: 0, toolRounds: 0, toolCalls: 0, wallTimeMs: 0, requestBytes: 0, responseBytes: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0, ...options.snapshot };
105
+ this.counters = {
106
+ turns: 0,
107
+ providerAttempts: 0,
108
+ toolRounds: 0,
109
+ toolCalls: 0,
110
+ wallTimeMs: 0,
111
+ requestBytes: 0,
112
+ responseBytes: 0,
113
+ inputTokens: 0,
114
+ outputTokens: 0,
115
+ totalTokens: 0,
116
+ cost: 0,
117
+ ...options.snapshot,
118
+ };
92
119
  for (const [key, value] of Object.entries(this.counters)) {
93
120
  if (!Number.isFinite(value) || value < 0 || (key !== "cost" && !Number.isSafeInteger(value))) {
94
121
  throw new TypeError("Run limit snapshot must contain finite non-negative counters");
@@ -104,10 +131,17 @@ export class RunLimitTracker {
104
131
  if (remaining === 0)
105
132
  this.exceed("maxWallTimeMs", limits.maxWallTimeMs);
106
133
  }
107
- get breach() { return this.exceeded; }
108
- snapshot() { return { ...this.counters, wallTimeMs: Math.min(this.limits.maxWallTimeMs, Math.ceil(performance.now() - this.startedAt)) }; }
109
- dispose() { if (this.timer)
110
- clearTimeout(this.timer); this.timer = undefined; }
134
+ get breach() {
135
+ return this.exceeded;
136
+ }
137
+ snapshot() {
138
+ return { ...this.counters, wallTimeMs: Math.min(this.limits.maxWallTimeMs, Math.ceil(performance.now() - this.startedAt)) };
139
+ }
140
+ dispose() {
141
+ if (this.timer)
142
+ clearTimeout(this.timer);
143
+ this.timer = undefined;
144
+ }
111
145
  charge(limit, delta = 1) {
112
146
  if (!Number.isSafeInteger(delta) || delta < 0)
113
147
  throw new TypeError("Run limit delta must be a non-negative safe integer");
@@ -130,7 +164,7 @@ export class RunLimitTracker {
130
164
  if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
131
165
  throw new TypeError(`Provider usage ${key} must be a non-negative safe integer`);
132
166
  }
133
- const total = usage.totalTokens ?? ((usage.inputTokens ?? 0) + (usage.outputTokens ?? 0));
167
+ const total = usage.totalTokens ?? (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
134
168
  if (!Number.isSafeInteger(total))
135
169
  throw new TypeError("Provider usage totalTokens is invalid");
136
170
  this.charge("maxInputTokens", usage.inputTokens ?? 0);
@@ -149,8 +183,13 @@ export class RunLimitTracker {
149
183
  }
150
184
  exceed(limit, observed) {
151
185
  if (!this.exceeded) {
152
- const maximum = limit === "maxCost" ? this.limits.maxCost?.amount ?? 0 : this.limits[limit];
153
- this.exceeded = { limit, maximum, observed, ...(limit === "maxCost" && this.limits.maxCost ? { currency: this.limits.maxCost.currency } : {}) };
186
+ const maximum = limit === "maxCost" ? (this.limits.maxCost?.amount ?? 0) : this.limits[limit];
187
+ this.exceeded = {
188
+ limit,
189
+ maximum,
190
+ observed,
191
+ ...(limit === "maxCost" && this.limits.maxCost ? { currency: this.limits.maxCost.currency } : {}),
192
+ };
154
193
  this.options.onExceeded?.(this.exceeded);
155
194
  }
156
195
  if (limit !== "maxWallTimeMs")
@@ -1,5 +1,5 @@
1
- import { createAgent } from "./agents.js";
2
1
  import { validateRunStateOptions } from "./agent-run-state.js";
2
+ import { createAgent } from "./agents.js";
3
3
  import { assertIdentityActive, assertIdentityMatchesOwnership } from "./identity.js";
4
4
  import { resolveRunLimits } from "./run-limits.js";
5
5
  import { createToolParameterValidator, createToolRegistry } from "./tools.js";
package/dist/security.js CHANGED
@@ -50,11 +50,16 @@ export function createStaticPermissionPolicy(options) {
50
50
  const key = `${request.kind}:${request.target}:${request.action}`;
51
51
  if (deny.has(key))
52
52
  return { allowed: false, reason: `Permission denied: ${key}` };
53
- return { allowed: allow.size === 0 || allow.has(key), reason: allow.size && !allow.has(key) ? `Permission not allowed: ${key}` : undefined };
53
+ return {
54
+ allowed: allow.size === 0 || allow.has(key),
55
+ reason: allow.size && !allow.has(key) ? `Permission not allowed: ${key}` : undefined,
56
+ };
54
57
  },
55
58
  };
56
59
  }
57
60
  export function denialToErrorInfo(error) {
58
- return error instanceof Error ? { name: error.name, message: error.message, code: error.code } : { message: String(error) };
61
+ return error instanceof Error
62
+ ? { name: error.name, message: error.message, code: error.code }
63
+ : { message: String(error) };
59
64
  }
60
65
  //# sourceMappingURL=security.js.map
@@ -1,4 +1,4 @@
1
- import { type Message, type SessionBranchRead, type BranchReader, type SessionEntry, type SessionStore } from "./contracts.js";
1
+ import { type BranchReader, type Message, type SessionBranchRead, type SessionEntry, type SessionStore } from "./contracts.js";
2
2
  export interface CreateSessionEntryOptions extends Omit<SessionEntry, "id" | "timestamp"> {
3
3
  readonly id?: string;
4
4
  readonly timestamp?: string;
@@ -1,4 +1,4 @@
1
- import { DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, SESSION_APPEND_CONFLICT_CODE, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SessionAppendConflictError, SessionSearchUnsupportedError, resolveSessionSearchQuery, } from "./contracts.js";
1
+ import { DEFAULT_MAX_SESSION_SEARCH_LINEAR_BYTES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_ENTRIES, DEFAULT_MAX_SESSION_SEARCH_LINEAR_SESSIONS, DEFAULT_MAX_SESSION_SEARCH_SNIPPET_BYTES, resolveSessionSearchQuery, SESSION_APPEND_CONFLICT_CODE, SESSION_SEARCH_WORKSPACE_METADATA_KEY, SessionAppendConflictError, SessionSearchUnsupportedError, } from "./contracts.js";
2
2
  import { createId } from "./ids.js";
3
3
  export function createSessionEntry(options) {
4
4
  const { createId, now, ...entry } = options;
@@ -13,7 +13,9 @@ export function createSessionEntry(options) {
13
13
  // this if a legitimate branch exceeds it.
14
14
  const MAX_BRANCH_PAGES = 64;
15
15
  export function getSessionBranchEntries(input, options = {}) {
16
- return typeof input === "function" ? readBranchFromReader(input, options) : getSessionBranchEntriesCore(input, options);
16
+ return typeof input === "function"
17
+ ? readBranchFromReader(input, options)
18
+ : getSessionBranchEntriesCore(input, options);
17
19
  }
18
20
  async function readBranchFromReader(reader, query) {
19
21
  const items = [];
@@ -29,8 +31,7 @@ async function readBranchFromReader(reader, query) {
29
31
  // indexEntries + the parentId walk order it and still reject missing parents / dupes.
30
32
  return getSessionBranchEntriesCore(items, { leafId: query.leafId });
31
33
  }
32
- function getSessionBranchEntriesCore(entries, options = {}) {
33
- const index = indexEntries(entries);
34
+ function getSessionBranchEntriesCore(entries, options = {}, index = indexEntries(entries)) {
34
35
  const leafId = options.leafId ?? entries.at(-1)?.id;
35
36
  if (!leafId)
36
37
  return [];
@@ -50,7 +51,7 @@ export function listSessionBranches(entries) {
50
51
  const index = indexEntries(entries);
51
52
  return entries
52
53
  .filter((entry) => !index.parentIds.has(entry.id))
53
- .map((entry) => ({ leafId: entry.id, entries: getSessionBranchEntries(entries, { leafId: entry.id }) }));
54
+ .map((entry) => ({ leafId: entry.id, entries: getSessionBranchEntriesCore(entries, { leafId: entry.id }, index) }));
54
55
  }
55
56
  export function rebuildSessionContext(input, options = {}) {
56
57
  if (typeof input === "function") {
@@ -66,13 +67,15 @@ async function rebuildSessionContextFromReader(reader, query) {
66
67
  }
67
68
  function rebuildSessionContextCore(entries, options = {}) {
68
69
  const branch = getSessionBranchEntriesCore(entries, options);
69
- const compaction = [...branch].reverse().find((entry) => entry.kind === "compaction" && entry.summary && isCompactionEntryData(entry.data));
70
+ const compaction = [...branch]
71
+ .reverse()
72
+ .find((entry) => entry.kind === "compaction" && entry.summary && isCompactionEntryData(entry.data));
70
73
  if (!compaction || !isCompactionEntryData(compaction.data)) {
71
74
  return {
72
75
  leafId: branch.at(-1)?.id,
73
76
  entries: branch,
74
- messages: branch.flatMap((entry) => entry.kind === "message" && entry.message ? [entry.message] : []),
75
- summaries: branch.flatMap((entry) => entry.kind === "summary" && entry.summary ? [entry.summary] : []),
77
+ messages: branch.flatMap((entry) => (entry.kind === "message" && entry.message ? [entry.message] : [])),
78
+ summaries: branch.flatMap((entry) => (entry.kind === "summary" && entry.summary ? [entry.summary] : [])),
76
79
  };
77
80
  }
78
81
  const keepIds = new Set(compaction.data.keepEntryIds ?? []);
@@ -129,17 +132,22 @@ export function createMemorySessionStore(initialEntries = [], options = {}) {
129
132
  if (dedupKey !== undefined && idempotencySeen.has(dedupKey)) {
130
133
  throw new SessionAppendConflictError({ code: SESSION_APPEND_CONFLICT_CODE, idempotencyDuplicate: true });
131
134
  }
132
- // expectedParentId is existence validation (the parent must already be in the
133
- // store or be undefined for a root). Tip-CAS is intentionally NOT used: prism
135
+ // expectedParentId is same-session existence validation (the parent must already
136
+ // be in the store for THIS session, or be undefined for a root). A parent from
137
+ // another session is rejected: the write would be unreadable, since every branch
138
+ // walk is per-session. Tip-CAS is intentionally NOT used: prism
134
139
  // allows branching from any existing leaf (checkout + append), so a stale-but-
135
140
  // existing parent is a valid branch, not a conflict. DB adapters may layer
136
141
  // stricter tip-CAS via unique constraints for linear-only sessions.
137
- if (options?.expectedParentId !== undefined && !byId.has(options.expectedParentId)) {
138
- throw new SessionAppendConflictError({
139
- code: SESSION_APPEND_CONFLICT_CODE,
140
- expectedParentId: options.expectedParentId,
141
- currentLeafId: leafBySession.get(entry.sessionId),
142
- });
142
+ if (options?.expectedParentId !== undefined) {
143
+ const parent = byId.get(options.expectedParentId);
144
+ if (!parent || parent.sessionId !== entry.sessionId) {
145
+ throw new SessionAppendConflictError({
146
+ code: SESSION_APPEND_CONFLICT_CODE,
147
+ expectedParentId: options.expectedParentId,
148
+ currentLeafId: leafBySession.get(entry.sessionId),
149
+ });
150
+ }
143
151
  }
144
152
  if (byId.has(entry.id))
145
153
  throw new Error(`Duplicate session entry id: ${entry.id}`);
@@ -257,16 +265,12 @@ function searchMemorySessionsLinear(bySession, leafBySession, query) {
257
265
  }
258
266
  matches.sort((a, b) => compareSearchHit(a, b, q.order));
259
267
  const afterCursor = q.cursor ? decodeSearchCursor(q.cursor) : undefined;
260
- const filtered = afterCursor
261
- ? matches.filter((hit) => isAfterSearchCursor(hit, afterCursor, q.order))
262
- : matches;
268
+ const filtered = afterCursor ? matches.filter((hit) => isAfterSearchCursor(hit, afterCursor, q.order)) : matches;
263
269
  const page = filtered.slice(0, q.limit);
264
270
  const last = page.at(-1);
265
271
  return {
266
272
  items: page,
267
- nextCursor: filtered.length > q.limit && last?.updatedAt
268
- ? encodeSearchCursor(last.updatedAt, last.sessionId)
269
- : undefined,
273
+ nextCursor: filtered.length > q.limit && last?.updatedAt ? encodeSearchCursor(last.updatedAt, last.sessionId) : undefined,
270
274
  };
271
275
  }
272
276
  function entrySearchText(entry) {
@@ -323,8 +327,8 @@ function isCompactionEntryData(value) {
323
327
  if (!value || typeof value !== "object" || Array.isArray(value))
324
328
  return false;
325
329
  const data = value;
326
- return (data.throughEntryId === undefined || typeof data.throughEntryId === "string")
327
- && (data.keepEntryIds === undefined || (Array.isArray(data.keepEntryIds) && data.keepEntryIds.every((item) => typeof item === "string")));
330
+ return ((data.throughEntryId === undefined || typeof data.throughEntryId === "string") &&
331
+ (data.keepEntryIds === undefined || (Array.isArray(data.keepEntryIds) && data.keepEntryIds.every((item) => typeof item === "string"))));
328
332
  }
329
333
  function indexEntries(entries) {
330
334
  const byId = new Map();
@@ -38,8 +38,8 @@ export function assertStructuredOutputRequestSupported(model, options) {
38
38
  return;
39
39
  validateStructuredOutputOptions(options.structuredOutput);
40
40
  if (!modelSupportsStructuredOutput(model.capabilities)) {
41
- throw new StructuredOutputError("unsupported_model", `Model ${model.provider}/${model.model} does not declare structuredOutput capability; `
42
- + `set loop.structuredOutputMode to "artifact-loop" or choose a capable model`);
41
+ throw new StructuredOutputError("unsupported_model", `Model ${model.provider}/${model.model} does not declare structuredOutput capability; ` +
42
+ `set loop.structuredOutputMode to "artifact-loop" or choose a capable model`);
43
43
  }
44
44
  }
45
45
  function isGenerateValidateReviseLoopOptions(loop) {
@@ -1,7 +1,12 @@
1
1
  // ponytail: Phase 31 — `source: "user"` is the global base layer; `source: "app"` sits above package.
2
2
  // Behavioral change from Phase 14: `source: "user"` is now the global base (rank 0), not a high-priority caller override.
3
3
  // Unknown custom sources sort after package but before app/run so they cannot override host/run layers.
4
- const sourceRank = new Map([["user", 0], ["package", 1], ["app", 2], ["run", 3]]);
4
+ const sourceRank = new Map([
5
+ ["user", 0],
6
+ ["package", 1],
7
+ ["app", 2],
8
+ ["run", 3],
9
+ ]);
5
10
  const unknownSourceRank = 1.5;
6
11
  export function composeSystemPrompt(contributions = [], options = {}) {
7
12
  const parts = baseParts(options.base);
@@ -40,7 +45,7 @@ function isContributionArray(value) {
40
45
  return Array.isArray(value);
41
46
  }
42
47
  function baseParts(base) {
43
- return (typeof base === "string" ? [base] : base ?? []).filter((text) => text.length > 0);
48
+ return (typeof base === "string" ? [base] : (base ?? [])).filter((text) => text.length > 0);
44
49
  }
45
50
  function rank(layer) {
46
51
  return sourceRank.get(layer.source ?? "") ?? unknownSourceRank;
@@ -16,7 +16,11 @@ import { createSessionEntry } from "../session-stores.js";
16
16
  export async function assertCompactionStrategyConforms(strategy, options = {}) {
17
17
  const secrets = options.secrets ?? [];
18
18
  const entries = [
19
- createSessionEntry({ sessionId: "s", kind: "message", message: { role: "user", content: [{ type: "text", text: `old ${secrets[0] ?? "history"}` }] } }),
19
+ createSessionEntry({
20
+ sessionId: "s",
21
+ kind: "message",
22
+ message: { role: "user", content: [{ type: "text", text: `old ${secrets[0] ?? "history"}` }] },
23
+ }),
20
24
  createSessionEntry({ sessionId: "s", kind: "message", message: { role: "assistant", content: [{ type: "text", text: "recent" }] } }),
21
25
  ];
22
26
  const context = {