@arnilo/prism 0.3.0 → 0.3.1

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 (69) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +3 -1
  3. package/dist/agent-loops.js +45 -8
  4. package/dist/agent-session/helpers.js +2 -2
  5. package/dist/cache-helpers.d.ts +11 -0
  6. package/dist/cache-helpers.js +29 -5
  7. package/dist/cli-provider-add.js +2 -1
  8. package/dist/context-budget.js +9 -6
  9. package/dist/contracts-core/agent.d.ts +2 -0
  10. package/dist/contracts-core/provider.d.ts +2 -0
  11. package/dist/event-multiplexer.js +0 -4
  12. package/dist/index.d.ts +6 -4
  13. package/dist/index.js +5 -3
  14. package/dist/input.js +19 -11
  15. package/dist/node/session-store-jsonl.js +7 -3
  16. package/dist/providers/openai-compatible.js +2 -1
  17. package/dist/providers/openai-primitives.js +2 -1
  18. package/dist/providers/schema.d.ts +7 -0
  19. package/dist/providers/schema.js +25 -0
  20. package/dist/testing/provider-conformance.d.ts +10 -0
  21. package/dist/testing/provider-conformance.js +37 -0
  22. package/dist/trim-trailing-slashes.d.ts +8 -0
  23. package/dist/trim-trailing-slashes.js +14 -0
  24. package/docs/0.1.0-readiness.md +1 -1
  25. package/docs/acp.md +1 -0
  26. package/docs/ag-ui.md +1 -0
  27. package/docs/agent-loops.md +3 -0
  28. package/docs/agent-session-runtime.md +1 -0
  29. package/docs/browser-automation.md +1 -0
  30. package/docs/database-persistence.md +1 -1
  31. package/docs/graft.md +125 -0
  32. package/docs/host-security.md +3 -1
  33. package/docs/index.md +11 -9
  34. package/docs/input-and-prompt-assembly.md +11 -6
  35. package/docs/instruction-injection.md +1 -1
  36. package/docs/mcp-tools.md +1 -0
  37. package/docs/migration.md +10 -0
  38. package/docs/node-jsonl-session-store.md +1 -1
  39. package/docs/obscura.md +175 -0
  40. package/docs/observability.md +21 -1
  41. package/docs/performance.md +58 -4
  42. package/docs/ponytail.md +1 -1
  43. package/docs/provider-caching.md +13 -11
  44. package/docs/provider-conformance.md +6 -0
  45. package/docs/provider-packages.md +1 -1
  46. package/docs/provider-primitives.md +15 -2
  47. package/docs/providers/ai-sdk.md +1 -1
  48. package/docs/providers/anthropic.md +1 -1
  49. package/docs/providers/azure.md +1 -0
  50. package/docs/providers/bedrock.md +1 -0
  51. package/docs/providers/kimi.md +2 -1
  52. package/docs/providers/openai.md +19 -7
  53. package/docs/providers/opencode-go.md +3 -1
  54. package/docs/providers/openrouter.md +4 -3
  55. package/docs/providers/vertex.md +1 -0
  56. package/docs/public-contracts.md +2 -1
  57. package/docs/rag.md +55 -8
  58. package/docs/release-and-install.md +45 -7
  59. package/docs/server.md +1 -0
  60. package/docs/supervisors.md +3 -2
  61. package/docs/system-prompts.md +1 -1
  62. package/docs/tools.md +1 -1
  63. package/docs/web-tools.md +2 -0
  64. package/docs/wiki.md +140 -0
  65. package/docs/workflows.md +4 -3
  66. package/docs/working-and-semantic-memory.md +20 -0
  67. package/package.json +12 -5
  68. package/docs/api-page-template.md +0 -32
  69. package/docs/release-0.2.7-evidence.md +0 -514
package/CHANGELOG.md CHANGED
@@ -1,3 +1,30 @@
1
+ ## [0.3.1] - 2026-08-29
2
+
3
+ > Root + 28 changed packages move to 0.3.1 in the plan 039 changed-package cut; `@arnilo/prism-rag` moves 0.3.1 → 0.3.2; `@arnilo/prism-obscura` publishes new at 0.3.0. Distinct from the 2026-08-26 rag/memory/otel 0.3.1 patch below (independent Decision B tags).
4
+
5
+ ### Added
6
+ - `@arnilo/prism-obscura` (new workspace package, 0.3.0): optional Obscura headless-browser engine over a **host-installed** binary — fail-closed `spawnObscuraProcess` lifecycle (absolute shell-free command, Docker argv, bounded readiness, group close), `createObscuraMcpTools` bridging the complete advertised MCP surface (reads effect-free; mutations and unknown future tools exclusive/serialized; `obscura_` prefix; loopback-default HTTP), `connectObscuraCdp` managed/external CDP + Playwright `connectOverCDP` composition, and `createObscuraWebTools` (standard `web_search`/`web_fetch` through one replaceable HTML search profile plus native `obscura_fetch`/`obscura_scrape`; public-HTTP(S)-only URL validation, byte/count/timeout caps, `allowEval`-gated custom expressions, untrusted-content labeling). Deliberately omitted from `prism-all`/`prism-base`/`prism-sdk` umbrella profiles — installation does not supply the binary/image; consumers install it explicitly.
7
+ - Cross-host conformance (`scripts/obscura-host-conformance.test.mjs`, wired into `npm test`): the same Obscura `ToolDefinition[]` executes through core agent sessions, the Prism MCP server, the server handler, AG-UI, ACP, workflow nodes, supervisor children, and Antigravity delegated exposure — no host branch; host authorization/selection deny before execution; abort kills owned children.
8
+ - New required peers: `@arnilo/prism-obscura` peers `@arnilo/prism` and `@arnilo/prism-web-tools` (reused citation/normalized shapes; `WebProvider` widened with `"obscura"`); `@arnilo/prism-obscura` also peers `@arnilo/prism-mcp`, `@arnilo/prism-browser`, and optional `playwright-core@1.61.0`.
9
+
10
+ ## [0.3.1] - 2026-08-26
11
+
12
+ ### Added
13
+ - **Release 0.3.1 (plan 034)** is the first Decision B independent patch: only `@arnilo/prism-rag`, `@arnilo/prism-memory`, and `@arnilo/prism-observability-opentelemetry` move `0.3.0 → 0.3.1`. Internal `^0.3.0` ranges stay valid; root and every other workspace package stay at their pre-plan versions. Production RAG engine (P1–P8):
14
+ - **P1 transactional pgvector store** — `createPostgresVectorStore` (standalone) plus the existing `createPostgresMemoryStores().vectorStore` now implement `getBySource` + `transaction`. Additive DDL: `embedder_id` / `content_hash` / `generation`, `text_tsv` + GIN, HNSW when the embedding dimension is pinned, per-scope generation pointer table. Host names schema/table and owns the knowledge database.
15
+ - **P2 hybrid retrieve** — `retrieveContext({ lexical: "fts" | "bm25" | "off" })` fuses vector + lexical legs with RRF (`fusion: "rrf"`, `rrfK` 60 / cap 1,000). Provenance `retrieval` is `vector` | `lexical` | `hybrid`. Stores advertise `lexicalModes`; unsupported `bm25` fails closed. `fuseReciprocalRank` / `tokenizeLexical` exported.
16
+ - **P3 embedder identity** — `Embedder.id` is required and stamped as `embedderId`. Retrieve fails closed with `ERR_PRISM_RAG_EMBEDDER_MISMATCH` on id/dimension drift or legacy rows without an id (re-index path named). **TypeScript break for Embedder implementers** — add `readonly id: string`; see `docs/migration.md` `0.3.0 → 0.3.1`.
17
+ - **P4 content-hash skip** — `replaceSource({ contentHash })` skips unchanged sources (`{ indexed: 0, skipped: true }`) and reuses embeddings when chunk text matches (`reuseEmbeddings`, `isValidContentHash`).
18
+ - **P5 heading metadata** — `chunkMarkdown` stamps `metadata.heading` (parent-first ATX stack) unless the caller supplied one; parser metadata (`page`, `section`, …) copies onto chunks.
19
+ - **P6 generation visibility** — `replaceSource` bumps a scope-level generation; retrieval filters to the current generation (legacy generation-less rows stay visible). `getCurrentGeneration` / `setCurrentGeneration` support rollback.
20
+ - **P7 RAG telemetry** — dependency-free `RagTelemetry` seam; `createRagTelemetry()` in the OpenTelemetry package maps the `rag_request` / `rag_index` span tree. Span-name + `rag.*` allow-list drops raw chunk text.
21
+ - **P8 TEI reranker** — `createTeiReranker({ baseUrl })` posts `{query, texts, raw_scores:false}` to host TEI `/rerank`, permutation-only, fail-closed parse, 65,536-byte ceiling, `pinnedFetch` default. SSRF/URL policy stays host-side.
22
+ - **multi-scope retrieve** — `retrieveContext({ scopes: [org, user, session] })` embeds once, runs per-scope vector/lexical legs, fuses with one RRF, reranks once, and cuts `topK`. `scope` still valid; empty `scopes` returns zero hits with no embed/search/rerank (host “no allowed corpora” path). `HARD_RETRIEVE_SCOPE_CAP` is 8. Provenance carries `tenantId`/`resourceId`/`corpusId`. Telemetry: `rag.scope_count` + chunk tenant/corpus.
23
+ - Compat baselines regenerated (`--update-baseline`). Scanner sees additive exports only; the Embedder.id implementer break is documented, not a name-level removal. No `--allow-break`. Publication remains the operator handoff (`docs/release-and-install.md` `0.3.1 independent RAG engine patch` — package tags `@arnilo/prism-memory@0.3.1`, `@arnilo/prism-rag@0.3.1`, `@arnilo/prism-observability-opentelemetry@0.3.1`).
24
+
25
+ ### Changed
26
+ - Store compatibility with 0.3.0: **compatible, additive**. New columns/indexes/tables are `IF NOT EXISTS`; 0.3.0 rows remain readable. Rollback = restore the 0.3.0 package versions (generation-less retrieve still works; heading/hash/telemetry/TEI simply disappear). No root/core persisted-shape change.
27
+
1
28
  ## [0.3.0] - 2026-08-20
2
29
 
3
30
  ### Added
package/README.md CHANGED
@@ -171,8 +171,10 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
171
171
  | `@arnilo/prism-provider-vertex` | Google Vertex provider |
172
172
  | `@arnilo/prism-provider-ai-sdk` | AI SDK interoperability adapter |
173
173
  | `@arnilo/prism-browser` | optional host-wired Playwright browser automation (not core; not auto-activated) |
174
+ | `@arnilo/prism-obscura` | optional Obscura headless-browser engine over a host-installed binary: MCP surface, CDP + Playwright, bounded CLI web tools (not in umbrellas; install explicitly) |
174
175
  | `@arnilo/prism-computer-use-linux` | optional Linux desktop-control tools over a host-owned `computer-use-linux` MCP binary |
175
176
  | `@arnilo/prism-antigravity-agent` | optional Antigravity CLI delegated agent adapter with per-run Prism MCP capability exposure |
177
+ | `@arnilo/prism-wiki` | optional Karpathy LLM Wiki knowledge compiler with local `qmd` hybrid search and Context7 line navigation |
176
178
  | `@arnilo/prism-compaction-llm` | provider-backed compaction strategy |
177
179
  | `@arnilo/prism-compaction-observational-memory` | source-backed memory + recall tool |
178
180
  | `@arnilo/prism-coding-agent` | bounded shell/read/write/edit tools |
@@ -191,7 +193,7 @@ printf '{"id":"1","command":"prompt","params":{"input":"Hi"}}\n' \
191
193
  | `@arnilo/prism-base` | profile: core + compaction + JSON Schema validation |
192
194
  | `@arnilo/prism-code` | profile: base + coding tools/security + MCP |
193
195
  | `@arnilo/prism-sdk` | profile: base + workflows + MCP + credentials + OpenTelemetry |
194
- | `@arnilo/prism-all` | broad umbrella: 21 first-party packages (47 transitive) across a 47-package workspace closure — omits document-reader, OpenAPI tools, NATS, Caveman, Ponytail, Impeccable, computer-use-linux, and antigravity-agent |
196
+ | `@arnilo/prism-all` | broad umbrella: 21 first-party packages (47 transitive) across a 47-package workspace closure — omits document-reader, OpenAPI tools, NATS, Caveman, Ponytail, Impeccable, computer-use-linux, antigravity-agent, Graft, Obscura, and wiki |
195
197
 
196
198
  ## Scripts
197
199
 
@@ -80,12 +80,29 @@ export function generateValidateReviseLoop(opts) {
80
80
  const max = opts.maxRevisions ?? 3;
81
81
  const repairer = opts.repairer ?? defaultRepairer();
82
82
  const finalOnly = opts.structuredOutputTiming === "final-turn-only" && opts.toolCalls === "bounded";
83
- // ponytail: per-run state hoisted to factory scope so snapshot/restore can capture it;
84
- // resolveLoop invokes this factory once per run, so there is no cross-run leakage.
83
+ // ponytail: keep only current-run state in this strategy so snapshot/restore can capture it;
84
+ // supplied strategy instances may be reused, so a new non-restored run resets these locals.
85
85
  let attempts = 0;
86
86
  let artifactPhase = !finalOnly;
87
87
  let savedSchema;
88
88
  let pendingHistory = [];
89
+ let stateOwner;
90
+ let restoredStatePending = false;
91
+ const resetState = () => {
92
+ attempts = 0;
93
+ artifactPhase = !finalOnly;
94
+ savedSchema = undefined;
95
+ pendingHistory = [];
96
+ };
97
+ const prepareRunState = (ctx) => {
98
+ const sameRun = stateOwner?.sessionId === ctx.sessionId && stateOwner.runId === ctx.runId;
99
+ if (!sameRun) {
100
+ if (!restoredStatePending)
101
+ resetState();
102
+ stateOwner = { sessionId: ctx.sessionId, runId: ctx.runId };
103
+ }
104
+ restoredStatePending = false;
105
+ };
89
106
  return {
90
107
  name: "generate-validate-revise",
91
108
  revision: "1",
@@ -108,8 +125,10 @@ export function generateValidateReviseLoop(opts) {
108
125
  // Repair messages were appended to the session before suspension, so the rebuilt
109
126
  // history already carries them; re-applying pendingHistory would duplicate them.
110
127
  pendingHistory = [];
128
+ restoredStatePending = true;
111
129
  },
112
130
  async run(ctx) {
131
+ prepareRunState(ctx);
113
132
  let usage;
114
133
  let nextInput = ctx.input;
115
134
  let toolRounds = 0;
@@ -260,17 +279,35 @@ export async function dispatchToolCallsInOrder(calls, ctx) {
260
279
  }
261
280
  const results = new Array(calls.length);
262
281
  let nextIndex = 0;
282
+ let stopped = false;
283
+ let firstFailure;
284
+ const recordFailure = (error) => {
285
+ if (stopped)
286
+ return;
287
+ stopped = true;
288
+ firstFailure = error;
289
+ };
263
290
  const workers = Array.from({ length: concurrency }, async () => {
264
291
  for (;;) {
265
- throwIfAborted(ctx.signal);
266
- const index = nextIndex;
267
- nextIndex += 1;
268
- if (index >= calls.length)
292
+ if (stopped)
269
293
  return;
270
- results[index] = await ctx.dispatchToolCall(calls[index]);
294
+ try {
295
+ throwIfAborted(ctx.signal);
296
+ const index = nextIndex;
297
+ nextIndex += 1;
298
+ if (index >= calls.length)
299
+ return;
300
+ results[index] = await ctx.dispatchToolCall(calls[index]);
301
+ }
302
+ catch (error) {
303
+ recordFailure(error);
304
+ return;
305
+ }
271
306
  }
272
307
  });
273
- await Promise.all(workers);
308
+ await Promise.allSettled(workers);
309
+ if (stopped)
310
+ throw firstFailure;
274
311
  for (const result of results) {
275
312
  throwIfAborted(ctx.signal);
276
313
  if (!result)
@@ -142,10 +142,10 @@ export function throwIfAbortedSignal(signal) {
142
142
  if (signal?.aborted)
143
143
  throw signal.reason instanceof Error ? signal.reason : new Error("Agent run aborted");
144
144
  }
145
- const jsonTextEncoder = new TextEncoder();
146
145
  export function jsonBytes(value) {
147
146
  try {
148
- return jsonTextEncoder.encode(JSON.stringify(value)).byteLength;
147
+ const serialized = JSON.stringify(value);
148
+ return serialized === undefined ? 0 : Buffer.byteLength(serialized, "utf8");
149
149
  }
150
150
  catch {
151
151
  throw new TypeError("Provider request or event must be JSON-serializable for run limits");
@@ -23,6 +23,17 @@ export interface CacheUsageReport {
23
23
  export declare function sanitizeCacheKey(value: string | undefined, maxLength: number): string | undefined;
24
24
  export declare function mapCacheRetention(retention: CacheRetention | undefined, model: ModelConfig): "short" | "long" | undefined;
25
25
  export declare function applyCacheControl(messages: readonly Message[], breakpoints: readonly PromptCacheBreakpoint[], options?: ApplyCacheControlOptions): readonly CacheControlledMessage[];
26
+ export interface SystemCacheControlTextBlock {
27
+ readonly type: "text";
28
+ readonly text: string;
29
+ readonly cache_control?: CacheControlValue;
30
+ }
31
+ /**
32
+ * Provider-style `system` field: plain joined string by default; native text
33
+ * blocks only when cache-control markers must survive (system_prompt breakpoints).
34
+ */
35
+ export declare function systemCacheControlField(messages: readonly CacheControlledMessage[], toText: (message: Message) => string): string | readonly SystemCacheControlTextBlock[] | undefined;
26
36
  export declare function cacheHitRate(usage: Usage | undefined): number | undefined;
27
37
  export declare function cacheSavings(usage: Usage | undefined, model: ModelConfig): number | undefined;
28
38
  export declare function cacheUsageReport(usage: Usage | undefined, model?: ModelConfig): CacheUsageReport | undefined;
39
+ export declare function resolveBreakpoint(messages: readonly Message[], breakpoint: PromptCacheBreakpoint): number;
@@ -1,8 +1,15 @@
1
1
  export function sanitizeCacheKey(value, maxLength) {
2
- const key = value
3
- ?.replace(/[^A-Za-z0-9_.:-]+/g, "-")
4
- .replace(/^-+|-+$/g, "")
5
- .slice(0, Math.max(0, maxLength));
2
+ const replaced = value?.replace(/[^A-Za-z0-9_.:-]+/g, "-");
3
+ if (!replaced)
4
+ return undefined;
5
+ // Index/slice edge trim instead of the `/^-+|-+$/g` alternation (CodeQL js/polynomial-redos, alert 22).
6
+ let start = 0;
7
+ let end = replaced.length;
8
+ while (start < end && replaced.charCodeAt(start) === 45)
9
+ start += 1;
10
+ while (end > start && replaced.charCodeAt(end - 1) === 45)
11
+ end -= 1;
12
+ const key = replaced.slice(start, end).slice(0, Math.max(0, maxLength));
6
13
  return key || undefined;
7
14
  }
8
15
  export function mapCacheRetention(retention, model) {
@@ -29,6 +36,23 @@ export function applyCacheControl(messages, breakpoints, options = {}) {
29
36
  return { ...message, content };
30
37
  });
31
38
  }
39
+ /**
40
+ * Provider-style `system` field: plain joined string by default; native text
41
+ * blocks only when cache-control markers must survive (system_prompt breakpoints).
42
+ */
43
+ export function systemCacheControlField(messages, toText) {
44
+ const system = messages.filter((message) => message.role === "system");
45
+ if (!system.length)
46
+ return undefined;
47
+ if (!system.some((message) => message.content.some((block) => block.cache_control))) {
48
+ return system.map(toText).join("\n\n") || undefined;
49
+ }
50
+ return system.map((message) => {
51
+ const cache_control = message.content.find((block) => block.cache_control)?.cache_control;
52
+ const block = { type: "text", text: toText(message) };
53
+ return cache_control ? { ...block, cache_control } : block;
54
+ });
55
+ }
32
56
  export function cacheHitRate(usage) {
33
57
  const read = usage?.cacheReadTokens;
34
58
  const input = usage?.inputTokens;
@@ -54,7 +78,7 @@ export function cacheUsageReport(usage, model) {
54
78
  currency: estimatedSavings === undefined ? usage.currency : (model?.cost?.currency ?? usage.currency),
55
79
  };
56
80
  }
57
- function resolveBreakpoint(messages, breakpoint) {
81
+ export function resolveBreakpoint(messages, breakpoint) {
58
82
  switch (breakpoint.location) {
59
83
  case "system_prompt":
60
84
  return messages.findIndex((message) => message.role === "system");
@@ -2,6 +2,7 @@ import { accessSync, constants as fsConstants } from "node:fs";
2
2
  import { access, mkdir, readdir, readFile, realpath, writeFile } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { trimTrailingSlashes } from "./trim-trailing-slashes.js";
5
6
  export class ProviderAddUsageError extends Error {
6
7
  }
7
8
  const NPM_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
@@ -208,7 +209,7 @@ function buildTokens(input) {
208
209
  __PROVIDER_PASCAL__: pascal,
209
210
  __PACKAGE_NAME__: input.name,
210
211
  __PRISM_VERSION__: input.version,
211
- __BASE_URL__: input.baseUrl.replace(/\/+$/, ""),
212
+ __BASE_URL__: trimTrailingSlashes(input.baseUrl),
212
213
  __ENV_KEY__: input.envKey,
213
214
  __MODEL_ID__: input.model,
214
215
  };
@@ -20,7 +20,7 @@ export function estimateTextTokens(text) {
20
20
  return Math.ceil(text.length / 4);
21
21
  }
22
22
  export function estimateTextBytes(text) {
23
- return new TextEncoder().encode(text).byteLength;
23
+ return Buffer.byteLength(text, "utf8");
24
24
  }
25
25
  export function estimateMessageTokens(message) {
26
26
  return estimateTextTokens(messageText(message));
@@ -64,11 +64,12 @@ export function applyContextBudget(options) {
64
64
  const tools = options.tools ? [...options.tools] : undefined;
65
65
  const omitted = [];
66
66
  const demotedBodies = new Set();
67
+ const historyCursor = { index: 0 };
67
68
  // Measure once, then subtract each dropped item's own estimate (dropNext computes it
68
69
  // with the same estimators) — avoids an O(n²) re-scan of the full keep-set per drop.
69
70
  const kept = measureAll(groups, context, skills, tools, skillContext, demotedBodies);
70
71
  while (overBudget(kept, budget)) {
71
- const drop = dropNext(groups, context, skills, layout, skillContext, demotedBodies);
72
+ const drop = dropNext(groups, context, skills, layout, skillContext, demotedBodies, historyCursor);
72
73
  if (!drop) {
73
74
  throw new ContextBudgetError();
74
75
  }
@@ -77,6 +78,8 @@ export function applyContextBudget(options) {
77
78
  if (omitted.length < HARD_MAX_CONTEXT_BUDGET_OMISSIONS)
78
79
  omitted.push(drop);
79
80
  }
81
+ if (historyCursor.index > 0)
82
+ groups.history = groups.history.slice(historyCursor.index);
80
83
  const reportOmissions = omitted.slice(0, DEFAULT_MAX_CONTEXT_BUDGET_OMISSIONS);
81
84
  return {
82
85
  groups,
@@ -94,8 +97,8 @@ export function applyContextBudget(options) {
94
97
  },
95
98
  };
96
99
  }
97
- function dropNext(groups, context, skills, layout, skillContext, demotedBodies) {
98
- // ponytail: drop droppable groups in layout order; within history, drop oldest first (shift).
100
+ function dropNext(groups, context, skills, layout, skillContext, demotedBodies, historyCursor) {
101
+ // ponytail: drop droppable groups in layout order; within history, advance a cursor and slice once.
99
102
  // cache_aware keeps attachments longer so stable prefix stays intact while budget still allows it.
100
103
  const order = layout === "cache_aware"
101
104
  ? ["tool_results", "history", "summaries", "context", "skills", "attachments"]
@@ -105,8 +108,8 @@ function dropNext(groups, context, skills, layout, skillContext, demotedBodies)
105
108
  const message = groups.toolResults.pop();
106
109
  return omission("tool_results", message.id ?? toolResultId(message), message);
107
110
  }
108
- if (kind === "history" && groups.history.length > 0) {
109
- const message = groups.history.shift();
111
+ if (kind === "history" && historyCursor.index < groups.history.length) {
112
+ const message = groups.history[historyCursor.index++];
110
113
  return omission("history", message.id, message);
111
114
  }
112
115
  if (kind === "summaries" && groups.summaries.length > 0) {
@@ -225,6 +225,8 @@ export interface PromptBuilder {
225
225
  }
226
226
  export interface PromptBuildRequest {
227
227
  readonly messages: readonly Message[];
228
+ /** Input layout selected by the host; the default builder uses cache-aware ordering unless legacy is explicit. */
229
+ readonly inputLayout?: InputAssemblyLayout;
228
230
  readonly context?: readonly ContextBlock[];
229
231
  readonly skills?: readonly Skill[];
230
232
  readonly skillsDisclosure?: import("../skill-disclosure.js").SkillsDisclosure;
@@ -11,6 +11,8 @@ export interface ModelCacheCapabilities {
11
11
  readonly maxBreakpoints?: number;
12
12
  readonly minCacheableTokens?: number;
13
13
  readonly longRetention?: boolean;
14
+ /** Model accepts explicit per-block cache breakpoints (e.g. OpenAI GPT-5.6+ `prompt_cache_breakpoint`). */
15
+ readonly explicitBreakpoints?: boolean;
14
16
  }
15
17
  export type PromptCacheMode = "auto" | "on" | "off";
16
18
  export type PromptCacheBreakpointLocation = "system_prompt" | "tools" | "stable_context" | "last_stable_message" | "last_user_message" | "message_id";
@@ -119,10 +119,6 @@ export function createEventMultiplexer(options = {}) {
119
119
  isClosed = true;
120
120
  options.signal?.removeEventListener("abort", abort);
121
121
  finishSources();
122
- queue.length = 0;
123
- // Free the single-consumer slot: a consumer suspended at a yield may never pull
124
- // again, and after close a fresh subscriber terminates immediately anyway.
125
- activeConsumer = false;
126
122
  if (waiter) {
127
123
  const resolve = waiter;
128
124
  waiter = undefined;
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export { createAgent, createAgentSession, resumeAgentRun, resumeAgentRunStream }
10
10
  export type { ArtifactApproval, ArtifactApprovalState, ArtifactBodyErrorCode, ArtifactBodyPresignOptions, ArtifactBodyRef, ArtifactBodyStore, ArtifactBodyTransferOptions, ArtifactCitation, ArtifactDecisionState, ArtifactDeliveryToken, ArtifactRecord, ArtifactRevision, } from "./artifacts.js";
11
11
  export { ARTIFACT_BODY_ERROR_CODES, ARTIFACT_CHECKPOINT_NAMESPACE, ArtifactBodyStoreError, ArtifactError, artifactApprovalState, artifactCheckpointKey, } from "./artifacts.js";
12
12
  export type { ApplyCacheControlOptions, CacheControlledContentBlock, CacheControlledMessage, CacheControlValue, CacheUsageReport, } from "./cache-helpers.js";
13
- export { applyCacheControl, cacheHitRate, cacheSavings, cacheUsageReport, mapCacheRetention, sanitizeCacheKey } from "./cache-helpers.js";
13
+ export { applyCacheControl, cacheHitRate, cacheSavings, cacheUsageReport, mapCacheRetention, resolveBreakpoint, sanitizeCacheKey, systemCacheControlField, } from "./cache-helpers.js";
14
14
  export type { CacheTelemetry, CacheTelemetryOptions, CacheTelemetryReport, CacheTelemetrySample, } from "./cache-telemetry.js";
15
15
  export { CACHE_TELEMETRY_OVERFLOW_KEY, CacheTelemetryError, createCacheTelemetry, DEFAULT_CACHE_TELEMETRY_CAP, } from "./cache-telemetry.js";
16
16
  export type { MemoryCheckpointStoreOptions } from "./checkpoints.js";
@@ -33,12 +33,12 @@ export type { ConversationBranchRef, ConversationReplayCursor, ConversationThrea
33
33
  export { CONVERSATION_METADATA_KEY, ConversationError, conversationMarkerMetadata, conversationThreadFromRecord, DEFAULT_MAX_CONVERSATION_CURSOR_BYTES, decodeConversationReplayCursor, encodeConversationReplayCursor, HARD_MAX_CONVERSATION_CURSOR_BYTES, } from "./conversations.js";
34
34
  export type { CredentialRecord, CredentialValueSource, MemoryCredentialStore, RevocableOAuthCredentialStore } from "./credentials.js";
35
35
  export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, resolveCredentialValue, revokeOAuthCredential, } from "./credentials.js";
36
+ export type { DelegatedAgentStepInput } from "./delegated-agent-step.js";
37
+ export { createDelegatedAgentStep, DelegatedAgentStepError, MAX_DELEGATED_AGENT_DURATION_MS, MAX_DELEGATED_AGENT_EVENT_BYTES, MAX_DELEGATED_AGENT_ID_BYTES, MAX_DELEGATED_AGENT_NAME_BYTES, MAX_DELEGATED_AGENT_STEP_INDEX, MAX_DELEGATED_AGENT_TOKEN_COUNT, } from "./delegated-agent-step.js";
36
38
  export type { DeviceAdapter, DeviceAdmitRequest, DeviceChunkResult, DeviceConformanceResult, DeviceKind, DevicePolicyErrorCode, DevicePolicyOptions, DeviceStreamLimits, ResolvedDevicePolicy, } from "./devices.js";
37
39
  export { acceptDeviceChunk, assertDeviceAdmit, DEFAULT_DEVICE_MAX_CHUNK_BYTES, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, DevicePolicyError, HARD_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CONCURRENT_SESSIONS, redactDeviceTelemetry, resolveDevicePolicy, runDevicePolicyConformance, } from "./devices.js";
38
40
  export type { EventMultiplexer, EventMultiplexerOptions, EventOverflowInfo, EventOverflowPolicy } from "./event-multiplexer.js";
39
41
  export { createEventMultiplexer, EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE, EventMultiplexerError } from "./event-multiplexer.js";
40
- export { createDelegatedAgentStep, DelegatedAgentStepError, MAX_DELEGATED_AGENT_DURATION_MS, MAX_DELEGATED_AGENT_EVENT_BYTES, MAX_DELEGATED_AGENT_ID_BYTES, MAX_DELEGATED_AGENT_NAME_BYTES, MAX_DELEGATED_AGENT_STEP_INDEX, MAX_DELEGATED_AGENT_TOKEN_COUNT, } from "./delegated-agent-step.js";
41
- export type { DelegatedAgentStepInput } from "./delegated-agent-step.js";
42
42
  export type { ExecutionAction, ExecutionDecision, ExecutionPolicy, ExecutionRisk } from "./execution-policy.js";
43
43
  export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
44
44
  export type { ExtensionErrorPolicy, ExtensionEventBus, ExtensionEventHandler, ExtensionKernel, ExtensionKernelOptions, ExtensionLoadPolicy, LoadedExtension, } from "./extensions.js";
@@ -73,6 +73,7 @@ export { providerContentDelta, providerContinuationRequired, providerDone, provi
73
73
  export { authMethodKey, defineProviderPackage, systemPromptContributionKey } from "./provider-packages.js";
74
74
  export type { SessionCachePolicyOptions } from "./provider-request-policy.js";
75
75
  export { createProviderRequestPolicyChain, createSessionCachePolicy, mergeProviderRequestOptions } from "./provider-request-policy.js";
76
+ export { canonicalizeJsonSchema } from "./providers/schema.js";
76
77
  export type { ProviderRegistry, ProviderRegistryOptions } from "./providers.js";
77
78
  export { createProviderRegistry, createProviderResolver } from "./providers.js";
78
79
  export type { SecretRedactor } from "./redaction.js";
@@ -110,8 +111,9 @@ export type { FoldToolResultsContext, ResolvedToolResultFoldOptions, ToolResultF
110
111
  export { DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES, DEFAULT_TOOL_RESULT_FOLD_MIN_AGE_TURNS, DEFAULT_TOOL_RESULT_FOLD_MIN_BYTES, foldedToolResultHeader, foldToolResultHistory, foldToolResults, formatFoldedToolResult, HARD_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES, resolveToolResultFold, TOOL_RESULT_FOLD_TURN_METADATA_KEY, } from "./tool-result-fold.js";
111
112
  export type { DispatchToolCallOptions, ToolArgumentValidationError, ToolArgumentValidationResult, ToolArgumentValidator, ToolFilter, ToolFilterInput, ToolParameterValidatorOptions, ToolRegistryOptions, ToolValidator, } from "./tools.js";
112
113
  export { createToolParameterValidator, createToolRegistry, dispatchToolCall, filterTools } from "./tools.js";
114
+ export { trimTrailingSlashes } from "./trim-trailing-slashes.js";
113
115
  export type { ResolvedUseCaseModel, ResolveUseCaseModelInput, UseCaseModelBinding, } from "./use-case-model.js";
114
116
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
115
117
  export declare const name = "prism";
116
- export declare const version = "0.3.0";
118
+ export declare const version = "0.3.1";
117
119
  export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { createAgentRunLifecycle } from "./agent-run-lifecycle.js";
5
5
  export { AGENT_RUN_STATE_NAMESPACE, AGENT_RUN_STATE_SCHEMA_VERSION, agentFingerprint, DEFAULT_MAX_AGENT_RUN_STATE_BYTES, HARD_MAX_AGENT_RUN_STATE_BYTES, loadAgentRunState, } from "./agent-run-state.js";
6
6
  export { createAgent, createAgentSession, resumeAgentRun, resumeAgentRunStream } from "./agents.js";
7
7
  export { ARTIFACT_BODY_ERROR_CODES, ARTIFACT_CHECKPOINT_NAMESPACE, ArtifactBodyStoreError, ArtifactError, artifactApprovalState, artifactCheckpointKey, } from "./artifacts.js";
8
- export { applyCacheControl, cacheHitRate, cacheSavings, cacheUsageReport, mapCacheRetention, sanitizeCacheKey } from "./cache-helpers.js";
8
+ export { applyCacheControl, cacheHitRate, cacheSavings, cacheUsageReport, mapCacheRetention, resolveBreakpoint, sanitizeCacheKey, systemCacheControlField, } from "./cache-helpers.js";
9
9
  export { CACHE_TELEMETRY_OVERFLOW_KEY, CacheTelemetryError, createCacheTelemetry, DEFAULT_CACHE_TELEMETRY_CAP, } from "./cache-telemetry.js";
10
10
  export { CHECKPOINT_CONFLICT_CODE, CheckpointConflictError, createMemoryCheckpointStore } from "./checkpoints.js";
11
11
  export { createDefaultCompactionStrategy, isCompactionEntryData } from "./compaction.js";
@@ -17,9 +17,9 @@ export { parseAgentFile, parseSkillFile } from "./contribution-parsing.js";
17
17
  export { createContributionRegistries, createContributionRegistry, registerDiscoveredContributions } from "./contributions.js";
18
18
  export { CONVERSATION_METADATA_KEY, ConversationError, conversationMarkerMetadata, conversationThreadFromRecord, DEFAULT_MAX_CONVERSATION_CURSOR_BYTES, decodeConversationReplayCursor, encodeConversationReplayCursor, HARD_MAX_CONVERSATION_CURSOR_BYTES, } from "./conversations.js";
19
19
  export { createChainedCredentialResolver, createEnvCredentialResolver, createExplicitCredentialResolver, createMemoryCredentialStore, refreshOAuthCredential, resolveCredentialValue, revokeOAuthCredential, } from "./credentials.js";
20
+ export { createDelegatedAgentStep, DelegatedAgentStepError, MAX_DELEGATED_AGENT_DURATION_MS, MAX_DELEGATED_AGENT_EVENT_BYTES, MAX_DELEGATED_AGENT_ID_BYTES, MAX_DELEGATED_AGENT_NAME_BYTES, MAX_DELEGATED_AGENT_STEP_INDEX, MAX_DELEGATED_AGENT_TOKEN_COUNT, } from "./delegated-agent-step.js";
20
21
  export { acceptDeviceChunk, assertDeviceAdmit, DEFAULT_DEVICE_MAX_CHUNK_BYTES, DEFAULT_DEVICE_MAX_CONCURRENT_SESSIONS, DevicePolicyError, HARD_DEVICE_MAX_CHUNK_BYTES, HARD_DEVICE_MAX_CONCURRENT_SESSIONS, redactDeviceTelemetry, resolveDevicePolicy, runDevicePolicyConformance, } from "./devices.js";
21
22
  export { createEventMultiplexer, EVENT_MULTIPLEXER_SINGLE_CONSUMER_CODE, EventMultiplexerError } from "./event-multiplexer.js";
22
- export { createDelegatedAgentStep, DelegatedAgentStepError, MAX_DELEGATED_AGENT_DURATION_MS, MAX_DELEGATED_AGENT_EVENT_BYTES, MAX_DELEGATED_AGENT_ID_BYTES, MAX_DELEGATED_AGENT_NAME_BYTES, MAX_DELEGATED_AGENT_STEP_INDEX, MAX_DELEGATED_AGENT_TOKEN_COUNT, } from "./delegated-agent-step.js";
23
23
  export { applyExecutionDecision, assertExecutionAllowed, checkExecution, ExecutionDeniedError } from "./execution-policy.js";
24
24
  export { createExtensionEventBus, createExtensionKernel } from "./extensions.js";
25
25
  export { createMemoryRunFeedbackStore, prepareRunFeedback, RunFeedbackError, requireRunFeedbackOwnership, runFeedbackPageLimit, } from "./feedback.js";
@@ -40,6 +40,7 @@ export { boundResponse, defaultResolver, isLoopbackAddress, isLoopbackHostname,
40
40
  export { providerContentDelta, providerContinuationRequired, providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallContent, toolCallFromArgumentsText, } from "./provider-events.js";
41
41
  export { authMethodKey, defineProviderPackage, systemPromptContributionKey } from "./provider-packages.js";
42
42
  export { createProviderRequestPolicyChain, createSessionCachePolicy, mergeProviderRequestOptions } from "./provider-request-policy.js";
43
+ export { canonicalizeJsonSchema } from "./providers/schema.js";
43
44
  export { createProviderRegistry, createProviderResolver } from "./providers.js";
44
45
  export { createSecretRedactor, errorToErrorInfo, redactAgentEvent, redactMessage, redactProviderRequest, redactRunLedgerRecord, redactSecrets, redactSessionEntry, resolveRedactor, } from "./redaction.js";
45
46
  export { loadBinaryResource, loadJsonResource, loadManifestResource, loadTextResource } from "./resources.js";
@@ -60,8 +61,9 @@ export { applyThinkingLevel, isThinkingLevel, normalizeThinkingLevel, THINKING_L
60
61
  export { createMemoryToolEffectStore, ToolEffectError } from "./tool-effects.js";
61
62
  export { DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES, DEFAULT_TOOL_RESULT_FOLD_MIN_AGE_TURNS, DEFAULT_TOOL_RESULT_FOLD_MIN_BYTES, foldedToolResultHeader, foldToolResultHistory, foldToolResults, formatFoldedToolResult, HARD_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES, resolveToolResultFold, TOOL_RESULT_FOLD_TURN_METADATA_KEY, } from "./tool-result-fold.js";
62
63
  export { createToolParameterValidator, createToolRegistry, dispatchToolCall, filterTools } from "./tools.js";
64
+ export { trimTrailingSlashes } from "./trim-trailing-slashes.js";
63
65
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
64
66
  export const name = "prism";
65
- export const version = "0.3.0";
67
+ export const version = "0.3.1";
66
68
  export const description = "Agent harness for AI providers, agents, sessions, and tools.";
67
69
  //# sourceMappingURL=index.js.map
package/dist/input.js CHANGED
@@ -41,16 +41,22 @@ export function createDefaultPromptBuilder() {
41
41
  // Tool-capable models receive schemas via request.tools; the text list only serves
42
42
  // text-only (or unknown-capability) models — duplicating it doubles tool tokens per turn.
43
43
  const tools = request.model?.capabilities?.tools === true ? undefined : request.tools;
44
- return [
45
- ...contextMessages(request.context),
46
- ...buildSkillMessages(request.skills, {
47
- disclosure: request.skillsDisclosure,
48
- loaded: request.loadedSkills,
49
- demotedBodies: request.demotedSkillBodies?.length ? new Set(request.demotedSkillBodies) : undefined,
50
- }),
51
- ...toolMessages(tools),
52
- ...request.messages,
53
- ];
44
+ const context = contextMessages(request.context);
45
+ const skills = buildSkillMessages(request.skills, {
46
+ disclosure: request.skillsDisclosure,
47
+ loaded: request.loadedSkills,
48
+ demotedBodies: request.demotedSkillBodies?.length ? new Set(request.demotedSkillBodies) : undefined,
49
+ });
50
+ const declarations = toolMessages(tools);
51
+ if ((request.inputLayout ?? "cache_aware") === "legacy") {
52
+ return [...context, ...skills, ...declarations, ...request.messages];
53
+ }
54
+ // Default input assembly keeps stable instruction messages at the front. Keep that
55
+ // boundary ahead of dynamic context/skills; legacy retains the old whole-prompt order.
56
+ const firstNonSystem = request.messages.findIndex((message) => message.role !== "system");
57
+ const stable = firstNonSystem < 0 ? request.messages : request.messages.slice(0, firstNonSystem);
58
+ const dynamic = firstNonSystem < 0 ? [] : request.messages.slice(firstNonSystem);
59
+ return [...stable, ...context, ...skills, ...declarations, ...dynamic];
54
60
  },
55
61
  };
56
62
  }
@@ -165,6 +171,7 @@ export async function assembleProviderInput(options) {
165
171
  const promptRequest = options.middleware
166
172
  ? await options.middleware.run("prompt_build", {
167
173
  messages,
174
+ inputLayout: layout,
168
175
  context,
169
176
  skills,
170
177
  skillsDisclosure: options.skillsDisclosure,
@@ -176,6 +183,7 @@ export async function assembleProviderInput(options) {
176
183
  })
177
184
  : {
178
185
  messages,
186
+ inputLayout: layout,
179
187
  context,
180
188
  skills,
181
189
  skillsDisclosure: options.skillsDisclosure,
@@ -185,7 +193,7 @@ export async function assembleProviderInput(options) {
185
193
  metadata: options.metadata,
186
194
  signal: options.signal,
187
195
  };
188
- const providerMessages = await promptBuilder.build({ ...promptRequest, tools, model: options.model });
196
+ const providerMessages = await promptBuilder.build({ ...promptRequest, inputLayout: layout, tools, model: options.model });
189
197
  assertMessagesSupportModelCapabilities(options.model, providerMessages);
190
198
  const metadata = budgetReport ? { ...options.metadata, [CONTEXT_BUDGET_REPORT_METADATA_KEY]: budgetReport } : options.metadata;
191
199
  return {
@@ -14,7 +14,9 @@ export function createJsonlSessionStore(pathOrOptions) {
14
14
  const idempotencySeen = new Set();
15
15
  return {
16
16
  append(entry, appendOptions) {
17
- appendChain = appendChain.then(async () => {
17
+ const operation = appendChain
18
+ .catch(() => undefined)
19
+ .then(async () => {
18
20
  if (options.createDirectory !== false)
19
21
  await mkdir(dirname(path), { recursive: true });
20
22
  const readResult = await readJsonlSessionEntries(path);
@@ -29,7 +31,8 @@ export function createJsonlSessionStore(pathOrOptions) {
29
31
  if (dedupKey !== undefined && idempotencySeen.has(dedupKey)) {
30
32
  throw new SessionAppendConflictError({ code: SESSION_APPEND_CONFLICT_CODE, idempotencyDuplicate: true });
31
33
  }
32
- if (appendOptions?.expectedParentId !== undefined && !entries.some((existing) => existing.id === appendOptions.expectedParentId)) {
34
+ if (appendOptions?.expectedParentId !== undefined &&
35
+ !entries.some((existing) => existing.id === appendOptions.expectedParentId)) {
33
36
  throw new SessionAppendConflictError({ code: SESSION_APPEND_CONFLICT_CODE, expectedParentId: appendOptions.expectedParentId });
34
37
  }
35
38
  if (entries.some((existing) => existing.id === entry.id))
@@ -38,7 +41,8 @@ export function createJsonlSessionStore(pathOrOptions) {
38
41
  idempotencySeen.add(dedupKey);
39
42
  await appendFile(path, `${JSON.stringify(entry)}\n`, "utf8");
40
43
  });
41
- return appendChain;
44
+ appendChain = operation.catch(() => undefined);
45
+ return operation;
42
46
  },
43
47
  async list(sessionId) {
44
48
  return (await readEntries(path)).filter((entry) => entry.sessionId === sessionId);
@@ -1,6 +1,7 @@
1
1
  import { resolveCredentialValue } from "../credentials.js";
2
2
  import { providerDone, providerError, providerTextDelta, providerThinkingDelta, providerToolCall, providerToolCallDelta, providerUsage, toolCallFromArgumentsText, } from "../provider-events.js";
3
3
  import { assertStructuredOutputRequestSupported } from "../structured-output.js";
4
+ import { trimTrailingSlashes } from "../trim-trailing-slashes.js";
4
5
  import { applyOpenAIChatStructuredOutput, assertOpenAIChatMessage, mapOpenAIChatUsage, serializeOpenAIChatMessage, serializeOpenAITool, } from "./openai-primitives.js";
5
6
  import { httpStatusError, ProviderTransportError, readBoundedResponseText, readSseEvents } from "./transport.js";
6
7
  /**
@@ -100,7 +101,7 @@ export function createOpenAICompatibleProvider(options) {
100
101
  try {
101
102
  const url = typeof options.chatCompletionsUrl === "function"
102
103
  ? options.chatCompletionsUrl(request)
103
- : (options.chatCompletionsUrl ?? `${options.baseUrl.replace(/\/+$/, "")}/chat/completions`);
104
+ : (options.chatCompletionsUrl ?? `${trimTrailingSlashes(options.baseUrl)}/chat/completions`);
104
105
  const authStyle = options.authStyle ?? "bearer";
105
106
  const headers = {
106
107
  ...Object.fromEntries(Object.entries(request.options?.headers ?? {}).filter((entry) => typeof entry[1] === "string")),
@@ -1,3 +1,4 @@
1
+ import { canonicalizeJsonSchema } from "./schema.js";
1
2
  export function assertOpenAIChatMessage(message, path) {
2
3
  if (!message || typeof message !== "object") {
3
4
  throw new Error(`Invalid provider message at ${path}: expected object`);
@@ -46,7 +47,7 @@ export function serializeOpenAITool(tool) {
46
47
  function: {
47
48
  name: tool.name,
48
49
  description: tool.description,
49
- parameters: tool.parameters ?? { type: "object" },
50
+ parameters: canonicalizeJsonSchema(tool.parameters ?? { type: "object" }),
50
51
  },
51
52
  };
52
53
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Deterministic JSON Schema clone for tool/function parameters.
3
+ * Sorts object keys and unordered `required` names. Leaves semantic arrays
4
+ * (`prefixItems`, `examples`, `enum`, tuple `items`) in caller order.
5
+ * Does not resolve `$ref`, mutate input, or enforce schema bounds.
6
+ */
7
+ export declare function canonicalizeJsonSchema(value: unknown): unknown;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Deterministic JSON Schema clone for tool/function parameters.
3
+ * Sorts object keys and unordered `required` names. Leaves semantic arrays
4
+ * (`prefixItems`, `examples`, `enum`, tuple `items`) in caller order.
5
+ * Does not resolve `$ref`, mutate input, or enforce schema bounds.
6
+ */
7
+ export function canonicalizeJsonSchema(value) {
8
+ if (Array.isArray(value))
9
+ return value.map(canonicalizeJsonSchema);
10
+ if (!value || typeof value !== "object")
11
+ return value;
12
+ return Object.fromEntries(Object.entries(value)
13
+ .sort(([left], [right]) => compareKey(left, right))
14
+ .map(([key, item]) => [key, key === "required" ? canonicalizeRequired(item) : canonicalizeJsonSchema(item)]));
15
+ }
16
+ function canonicalizeRequired(item) {
17
+ if (Array.isArray(item) && item.every((entry) => typeof entry === "string")) {
18
+ return [...item].sort(compareKey);
19
+ }
20
+ return canonicalizeJsonSchema(item);
21
+ }
22
+ function compareKey(left, right) {
23
+ return left < right ? -1 : left > right ? 1 : 0;
24
+ }
25
+ //# sourceMappingURL=schema.js.map
@@ -38,6 +38,16 @@ export declare function assertProviderStreamConforms(options: ProviderStreamConf
38
38
  export declare function assertAbortIsObserved(options: ProviderAbortConformanceOptions): Promise<void>;
39
39
  export declare function assertToolCallDeltasReconstruct(events: readonly ProviderEvent[], expected: readonly ToolCallDeltaExpectation[]): readonly ToolCallContent[];
40
40
  export declare function assertSerializedRequestCoversContent(request: ProviderRequest, body: unknown, options?: SerializedContentCoverageOptions): void;
41
+ export declare function assertCanonicalToolParameters(serialized: unknown, original: unknown): void;
41
42
  export declare function assertProviderOwnedHeadersWin(captured: Headers, options: ProviderHeaderOwnershipConformanceOptions): void;
42
43
  export declare function assertNoSecretLeak(events: readonly ProviderEvent[], secrets: readonly string[]): void;
44
+ /**
45
+ * Implicit/none-cache providers must serialize no foreign cache fields: implicit
46
+ * caching works by byte-stable prefix reuse, not request payloads. `allowed` names
47
+ * fields the provider documents for that route (e.g. `cachedContent` via the host
48
+ * `extra.cachedContent` escape hatch on Gemini).
49
+ */
50
+ export declare function assertNoForeignCacheFields(body: unknown, allowed?: readonly string[]): void;
51
+ /** Provider construction and setup must perform zero network calls; discovery and streams are caller-gated. */
52
+ export declare function assertNoFetches(calls: readonly unknown[]): void;
43
53
  export declare function assertUsageAccounting(events: readonly ProviderEvent[], expected: Usage): Usage;