@arnilo/prism 0.3.2 → 0.5.0

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 (208) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/README.md +42 -62
  3. package/dist/agent-run-lifecycle.js +4 -0
  4. package/dist/agent-run-state.d.ts +5 -2
  5. package/dist/agent-run-state.js +18 -8
  6. package/dist/agent-session/session/assemble.d.ts +6 -0
  7. package/dist/agent-session/session/assemble.js +391 -0
  8. package/dist/agent-session/session/persist.d.ts +28 -0
  9. package/dist/agent-session/session/persist.js +166 -0
  10. package/dist/agent-session/session/provider-round.d.ts +6 -0
  11. package/dist/agent-session/session/provider-round.js +231 -0
  12. package/dist/agent-session/session/tool-round.d.ts +31 -0
  13. package/dist/agent-session/session/tool-round.js +473 -0
  14. package/dist/agent-session/session/types.d.ts +115 -0
  15. package/dist/agent-session/session/types.js +5 -0
  16. package/dist/agent-session/session.d.ts +54 -41
  17. package/dist/agent-session/session.js +23 -1132
  18. package/dist/capture.d.ts +63 -0
  19. package/dist/capture.js +67 -0
  20. package/dist/cli-dev.d.ts +29 -0
  21. package/dist/cli-dev.js +52 -0
  22. package/dist/cli-init.d.ts +34 -3
  23. package/dist/cli-init.js +192 -24
  24. package/dist/cli-runner.d.ts +6 -2
  25. package/dist/cli-runner.js +57 -10
  26. package/dist/content.d.ts +3 -3
  27. package/dist/content.js +3 -1
  28. package/dist/contracts-core/agent.d.ts +8 -0
  29. package/dist/contracts-core/batch.d.ts +97 -0
  30. package/dist/contracts-core/batch.js +65 -0
  31. package/dist/contracts-core/content.d.ts +72 -1
  32. package/dist/contracts-core/embeddings.d.ts +30 -0
  33. package/dist/contracts-core/embeddings.js +17 -0
  34. package/dist/contracts-core/images.d.ts +60 -0
  35. package/dist/contracts-core/images.js +17 -0
  36. package/dist/contracts-core/moderation.d.ts +46 -0
  37. package/dist/contracts-core/moderation.js +34 -0
  38. package/dist/contracts-core/speech.d.ts +39 -0
  39. package/dist/contracts-core/speech.js +17 -0
  40. package/dist/contracts-core/transcription.d.ts +48 -0
  41. package/dist/contracts-core/transcription.js +17 -0
  42. package/dist/contracts-core/video.d.ts +61 -0
  43. package/dist/contracts-core/video.js +17 -0
  44. package/dist/contracts-core.d.ts +7 -0
  45. package/dist/contracts-core.js +7 -0
  46. package/dist/contracts-protocol.d.ts +18 -0
  47. package/dist/contracts-run-state.d.ts +1 -2
  48. package/dist/index.d.ts +7 -3
  49. package/dist/index.js +5 -3
  50. package/dist/input.d.ts +8 -0
  51. package/dist/input.js +4 -0
  52. package/dist/node/agent-definitions.d.ts +1 -8
  53. package/dist/node/agent-definitions.js +0 -34
  54. package/dist/node/settings.d.ts +0 -1
  55. package/dist/node/settings.js +0 -5
  56. package/dist/pinned-fetch.js +29 -3
  57. package/dist/provider-events.js +3 -4
  58. package/dist/providers/media.d.ts +1 -2
  59. package/dist/providers/media.js +1 -4
  60. package/dist/rpc.d.ts +1 -1
  61. package/dist/rpc.js +4 -4
  62. package/dist/testing/persistence-schema.d.ts +1 -1
  63. package/dist/testing/persistence-schema.js +32 -28
  64. package/dist/testing/provider-conformance.d.ts +114 -5
  65. package/dist/testing/provider-conformance.js +342 -0
  66. package/dist/testing/tool-conformance.d.ts +25 -0
  67. package/dist/testing/tool-conformance.js +128 -1
  68. package/dist/testing/tool-effect-store-conformance.d.ts +0 -1
  69. package/dist/testing/tool-effect-store-conformance.js +0 -3
  70. package/dist/thinking.d.ts +48 -9
  71. package/dist/thinking.js +134 -8
  72. package/dist/tool-search.d.ts +76 -0
  73. package/dist/tool-search.js +199 -0
  74. package/docs/0.1.0-readiness.md +3 -3
  75. package/docs/a2a.md +2 -2
  76. package/docs/acp-agent.md +1 -1
  77. package/docs/acp.md +3 -3
  78. package/docs/ag-ui-adoption.md +1 -1
  79. package/docs/ag-ui.md +1 -2
  80. package/docs/agent-definitions.md +1 -1
  81. package/docs/agent-events.md +5 -5
  82. package/docs/agent-identity.md +13 -2
  83. package/docs/audit-export.md +3 -3
  84. package/docs/batch-jobs.md +120 -0
  85. package/docs/browser-automation.md +5 -5
  86. package/docs/caveman.md +2 -2
  87. package/docs/cli-rpc.md +43 -9
  88. package/docs/coding-agent-tools.md +19 -19
  89. package/docs/coding-review-and-diagnostics.md +2 -2
  90. package/docs/coding-security.md +5 -5
  91. package/docs/coding-tools.md +82 -0
  92. package/docs/coding-workspaces.md +2 -2
  93. package/docs/compaction-and-retry.md +2 -2
  94. package/docs/compaction-llm.md +4 -4
  95. package/docs/compaction-observational-memory.md +3 -3
  96. package/docs/computer-use-linux.md +13 -2
  97. package/docs/context-and-skills.md +3 -1
  98. package/docs/conversations.md +4 -4
  99. package/docs/core.md +85 -0
  100. package/docs/credential-storage.md +12 -8
  101. package/docs/credentials-and-redaction.md +1 -1
  102. package/docs/data-classification.md +1 -1
  103. package/docs/database-persistence.md +7 -3
  104. package/docs/dev-inspector.md +103 -0
  105. package/docs/device-adapters.md +2 -2
  106. package/docs/diagrams.md +247 -0
  107. package/docs/document-reader.md +6 -6
  108. package/docs/documents.md +214 -0
  109. package/docs/embeddings.md +112 -0
  110. package/docs/enterprise-postgres-state.md +7 -7
  111. package/docs/evaluations.md +41 -7
  112. package/docs/extensions.md +3 -3
  113. package/docs/forge-integration.md +3 -3
  114. package/docs/graft.md +5 -5
  115. package/docs/guardrails.md +2 -2
  116. package/docs/host-security.md +16 -15
  117. package/docs/image-generation.md +129 -0
  118. package/docs/impeccable.md +7 -5
  119. package/docs/index.md +84 -46
  120. package/docs/indexed-code-search.md +2 -2
  121. package/docs/language-intelligence.md +4 -4
  122. package/docs/live-testing.md +126 -0
  123. package/docs/mcp-tools.md +44 -13
  124. package/docs/middleware-hooks.md +1 -1
  125. package/docs/migrate-to-0.4.md +312 -0
  126. package/docs/migrate-to-0.5.md +122 -0
  127. package/docs/migration.md +51 -1
  128. package/docs/model-registry.md +38 -0
  129. package/docs/model-routing.md +6 -6
  130. package/docs/moderation.md +117 -0
  131. package/docs/multi-agent-patterns.md +177 -0
  132. package/docs/multimodal-content.md +27 -3
  133. package/docs/obscura.md +12 -12
  134. package/docs/observability.md +32 -7
  135. package/docs/openapi-tools.md +14 -4
  136. package/docs/operations.md +11 -0
  137. package/docs/performance.md +30 -10
  138. package/docs/persistence-credentials-multimodality-primitives.md +7 -7
  139. package/docs/policy-and-audit.md +18 -8
  140. package/docs/ponytail.md +3 -3
  141. package/docs/postgres-persistence.md +5 -5
  142. package/docs/process-sessions.md +2 -2
  143. package/docs/prompt-registry.md +106 -0
  144. package/docs/provider-caching.md +36 -32
  145. package/docs/provider-conformance.md +24 -2
  146. package/docs/provider-packages.md +58 -22
  147. package/docs/provider-primitives.md +5 -5
  148. package/docs/provider-request-policies.md +1 -1
  149. package/docs/providers/ai-sdk.md +18 -6
  150. package/docs/providers/alibaba.md +10 -6
  151. package/docs/providers/anthropic.md +10 -6
  152. package/docs/providers/azure.md +20 -4
  153. package/docs/providers/bedrock.md +18 -3
  154. package/docs/providers/clinepass.md +7 -3
  155. package/docs/providers/commandcode.md +253 -0
  156. package/docs/providers/deepseek.md +7 -3
  157. package/docs/providers/google.md +8 -4
  158. package/docs/providers/hyper.md +284 -0
  159. package/docs/providers/kimi.md +7 -3
  160. package/docs/providers/neuralwatt.md +12 -8
  161. package/docs/providers/ollama.md +18 -3
  162. package/docs/providers/openai-compatible.md +5 -1
  163. package/docs/providers/openai.md +9 -5
  164. package/docs/providers/opencode-go.md +8 -4
  165. package/docs/providers/openrouter.md +8 -4
  166. package/docs/providers/vertex.md +21 -5
  167. package/docs/providers/xai.md +7 -3
  168. package/docs/providers/zai.md +7 -3
  169. package/docs/rag.md +31 -9
  170. package/docs/release-and-install.md +181 -76
  171. package/docs/resource-loading.md +1 -1
  172. package/docs/runs-and-usage.md +28 -3
  173. package/docs/server.md +94 -5
  174. package/docs/settings-auth-trust-security.md +7 -5
  175. package/docs/sheets.md +229 -0
  176. package/docs/speech.md +126 -0
  177. package/docs/sqlite-persistence.md +4 -4
  178. package/docs/supervisors.md +4 -3
  179. package/docs/thinking-and-reasoning.md +93 -60
  180. package/docs/tool-conformance.md +28 -3
  181. package/docs/tool-execution-primitives.md +8 -8
  182. package/docs/tools.md +32 -5
  183. package/docs/web-tools.md +3 -3
  184. package/docs/wiki.md +7 -7
  185. package/docs/work-artifacts-and-review.md +17 -6
  186. package/docs/work-connectors.md +4 -4
  187. package/docs/work-tools.md +5 -5
  188. package/docs/workflow-orchestration-primitives.md +35 -11
  189. package/docs/workflows.md +74 -13
  190. package/docs/working-and-semantic-memory.md +53 -5
  191. package/package.json +14 -31
  192. package/templates/README.md +23 -0
  193. package/templates/deep-research/README.md.tmpl +47 -0
  194. package/templates/deep-research/env.example.tmpl +12 -0
  195. package/templates/deep-research/gitignore.tmpl +7 -0
  196. package/templates/deep-research/manifest.json +12 -0
  197. package/templates/deep-research/package.json.tmpl +23 -0
  198. package/templates/deep-research/src/agent.ts.tmpl +81 -0
  199. package/templates/deep-research/src/index.ts.tmpl +53 -0
  200. package/templates/deep-research/src/tests/research.test.ts.tmpl +114 -0
  201. package/templates/deep-research/src/tools.ts.tmpl +86 -0
  202. package/templates/deep-research/src/types.ts.tmpl +45 -0
  203. package/templates/deep-research/src/workflow.ts.tmpl +156 -0
  204. package/templates/deep-research/tsconfig.json.tmpl +15 -0
  205. package/templates/init/manifest.json +5 -0
  206. package/templates/init/package.json.tmpl +2 -1
  207. package/templates/init/providers.json +40 -24
  208. package/docs/antigravity-agent.md +0 -207
@@ -7,7 +7,8 @@
7
7
  // Error; no test runner, no network. Execution is observed via the
8
8
  // tool_execution_started / tool_execution_blocked events the runtime emits,
9
9
  // not by mutating the caller's tool.
10
- import { dispatchToolCall } from "../tools.js";
10
+ import { createActiveToolSet, createSearchToolsTool, createToolSearchState, HARD_MAX_TOOLS_INDEX, SEARCH_TOOLS_TOOL_NAME, selectDisclosedTools, } from "../tool-search.js";
11
+ import { dispatchToolCall, filterTools } from "../tools.js";
11
12
  const denyAllPermission = { check: () => ({ allowed: false, reason: "denied" }) };
12
13
  const alwaysInvalidValidator = () => "invalid";
13
14
  /**
@@ -98,7 +99,133 @@ export async function dispatchAndCollect(probe) {
98
99
  });
99
100
  return { result, events };
100
101
  }
102
+ /**
103
+ * Assert the tool-disclosure contract (plan 041) against the same narrowing the
104
+ * runtime applies: search mode only narrows (disclosed set is a subset of the
105
+ * allow/deny-filtered input, never wider, never zero, deterministic order); a
106
+ * denied tool is never described; the generated `search_tools` tool is always
107
+ * kept and its output is inert — names plus byte-truncated descriptions only,
108
+ * no JSON structure, no secret values — and activation stays disclosed beside
109
+ * the turn top-k next turn. Fails closed: an index over the hard cap discloses
110
+ * the full eligible list. Throws on the first violation.
111
+ */
112
+ export function assertToolDisclosureConforms(options) {
113
+ const eligible = filterTools(options.tools, options.filter);
114
+ if (eligible.length === 0)
115
+ throw new Error("Disclosure conformance needs at least one eligible tool");
116
+ const activated = createActiveToolSet();
117
+ const topK = options.search?.topK ?? 16;
118
+ const state = createToolSearchState({ tools: eligible, activated, search: options.search });
119
+ const searchTool = createSearchToolsTool(state);
120
+ const runTools = [...eligible, searchTool];
121
+ const input = options.input ?? "zzzqqq unmatchable zero-match probe";
122
+ // 1. Narrowing stays a subset of the eligible list and always keeps search_tools.
123
+ const disclosed = selectDisclosedTools({ tools: runTools, input, search: options.search, activated });
124
+ const eligibleNames = new Set(eligible.map((tool) => tool.name));
125
+ eligibleNames.add(SEARCH_TOOLS_TOOL_NAME);
126
+ for (const tool of disclosed) {
127
+ if (!eligibleNames.has(tool.name)) {
128
+ throw new Error(`Disclosed tool ${tool.name} is not in the eligible list; search must never widen`);
129
+ }
130
+ }
131
+ if (!disclosed.some((tool) => tool.name === SEARCH_TOOLS_TOOL_NAME)) {
132
+ throw new Error("Disclosed set dropped the generated search_tools tool");
133
+ }
134
+ if (disclosed.length === 0)
135
+ throw new Error("Disclosure disclosed zero tools; fail closed to a bounded non-empty set");
136
+ // 2. A deny-listed tool is never described to the provider.
137
+ const deniedName = eligible[0].name;
138
+ const deniedDisclosed = selectDisclosedTools({
139
+ tools: filterTools(runTools, { deny: [deniedName] }),
140
+ input,
141
+ search: options.search,
142
+ activated,
143
+ });
144
+ if (deniedDisclosed.some((tool) => tool.name === deniedName)) {
145
+ throw new Error(`Denied tool ${deniedName} was described to the provider`);
146
+ }
147
+ // 3. Deterministic order for identical turns.
148
+ const again = selectDisclosedTools({ tools: runTools, input, search: options.search, activated });
149
+ if (JSON.stringify(again.map((tool) => tool.name)) !== JSON.stringify(disclosed.map((tool) => tool.name))) {
150
+ throw new Error("Disclosure order is not deterministic for identical turns");
151
+ }
152
+ // 4. Fail closed past the index hard cap: full eligible list, never zero, never wider.
153
+ const oversized = Array.from({ length: HARD_MAX_TOOLS_INDEX + 1 }, (_, index) => ({
154
+ name: `cap_${index}`,
155
+ description: "Fixture tool beyond the frozen index cap.",
156
+ parameters: { type: "object", properties: {} },
157
+ execute: () => ({ toolCallId: "x", name: "cap" }),
158
+ }));
159
+ const overflowed = selectDisclosedTools({ tools: oversized, input, search: options.search });
160
+ if (overflowed.length !== oversized.length) {
161
+ throw new Error(`Index overflow must disclose the full list; disclosed ${overflowed.length} of ${oversized.length}`);
162
+ }
163
+ // 5. search_tools output is inert: names + byte-truncated descriptions, no JSON
164
+ // structure, no secret values; activation bounded to the eligible set.
165
+ const oversizedDescription = `${"padding ".repeat(160)}TAIL-MARKER-BEYOND-TRUNCATION`;
166
+ const probeEligible = [
167
+ ...eligible,
168
+ {
169
+ name: "oversized_desc_tool",
170
+ description: oversizedDescription,
171
+ parameters: { type: "object", properties: { untrusted: { type: "string" } } },
172
+ execute: () => ({ toolCallId: "x", name: "oversized_desc_tool" }),
173
+ },
174
+ ];
175
+ const probeState = createToolSearchState({ tools: probeEligible, activated, search: options.search });
176
+ const probeTool = createSearchToolsTool(probeState);
177
+ const probeResult = probeTool.execute({ query: "oversized_desc_tool" }, context("probe"));
178
+ if (probeResult.error)
179
+ throw new Error(`search_tools rejected a valid bounded query: ${probeResult.error.message}`);
180
+ const text = probeResult.content?.find((block) => block.type === "text");
181
+ if (text?.type !== "text" || !text.text.startsWith("- ")) {
182
+ throw new Error("search_tools returned no bounded name+description lines");
183
+ }
184
+ if (text.text.includes("TAIL-MARKER-BEYOND-TRUNCATION")) {
185
+ throw new Error("search_tools emitted an untruncated oversized description; descriptions are truncated, descriptions are never executed");
186
+ }
187
+ if (/[{}]/.test(text.text)) {
188
+ throw new Error("search_tools text carries JSON structure; output must be inert name+description lines only");
189
+ }
190
+ const activatedCount = activated.list().length;
191
+ if (activatedCount === 0 || activatedCount > topK)
192
+ throw new Error(`activation not bounded: ${activatedCount} names`);
193
+ const probeNames = new Set(probeEligible.map((tool) => tool.name));
194
+ for (const name of activated.list()) {
195
+ if (!probeNames.has(name))
196
+ throw new Error(`activated ${name} is not in the eligible set`);
197
+ }
198
+ // 5b. Secret scan: query every eligible tool by name; configured secret values
199
+ // must never surface in model-facing search output even when a host
200
+ // description carries one.
201
+ for (const tool of eligible) {
202
+ const probe = probeTool.execute({ query: tool.name }, context("probe-secret"));
203
+ const probeBlob = JSON.stringify(probe);
204
+ for (const secret of options.secrets ?? []) {
205
+ if (secret && probeBlob.includes(secret)) {
206
+ throw new Error("search output leaked a configured secret");
207
+ }
208
+ }
209
+ }
210
+ // 6. Activated tools stay disclosed on the next turn, beside the turn top-k.
211
+ // (Names outside the runtime list are inert by design — e.g. the probe's
212
+ // synthetic tool above — so only eligible names are asserted.)
213
+ const nextTurn = selectDisclosedTools({
214
+ tools: runTools,
215
+ input: "zzzqqq unmatchable next-turn probe",
216
+ search: options.search,
217
+ activated,
218
+ });
219
+ for (const name of activated.list()) {
220
+ if (eligibleNames.has(name) && !nextTurn.some((tool) => tool.name === name)) {
221
+ throw new Error(`activated tool ${name} was dropped from the next turn's disclosed set`);
222
+ }
223
+ }
224
+ }
101
225
  function pickPolicy(options) {
102
226
  return { permission: options.permission, validate: options.validate, filter: options.filter };
103
227
  }
228
+ function context(toolCallId) {
229
+ return { sessionId: "conformance", runId: "r", toolCallId };
230
+ }
104
231
  //# sourceMappingURL=tool-conformance.js.map
@@ -6,4 +6,3 @@ export interface ToolEffectStoreConformanceOptions {
6
6
  }
7
7
  /** Assert core claim/CAS, duplicate, reconciliation, and cleanup semantics without a test framework. */
8
8
  export declare function assertToolEffectStoreConforms(factory: () => ToolEffectStore | Promise<ToolEffectStore>, options?: ToolEffectStoreConformanceOptions): Promise<void>;
9
- export declare function runToolEffectStoreConformance(factory: () => ToolEffectStore | Promise<ToolEffectStore>, options?: ToolEffectStoreConformanceOptions): Promise<void>;
@@ -44,9 +44,6 @@ export async function assertToolEffectStoreConforms(factory, options = {}) {
44
44
  if (cleanup.deleted < 1)
45
45
  throw new Error("store cleanup must remove terminal effects");
46
46
  }
47
- export async function runToolEffectStoreConformance(factory, options = {}) {
48
- await assertToolEffectStoreConforms(factory, options);
49
- }
50
47
  function key(identity, ownership, value, toolCallId = "call") {
51
48
  return {
52
49
  identity,
@@ -9,8 +9,16 @@ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
9
9
  * Compat mapping families used by ≥2 packages, or explicit no-op for host-owned adapters.
10
10
  * Provider packages keep unique escape hatches (budgets, keep/all, tool_stream) local.
11
11
  */
12
- export type ThinkingCompatFamily = "openai_reasoning" | "reasoning_effort" | "thinking_type" | "noop";
12
+ export type ThinkingCompatFamily = "openai_reasoning" | "reasoning_effort" | "thinking_type" | "google" | "output_config_effort" | "noop";
13
13
  export declare function isThinkingLevel(value: unknown): value is ThinkingLevel;
14
+ /**
15
+ * Parse a host thinking-level value without guessing: known levels canonicalize to
16
+ * `ThinkingLevel`, unknown non-empty strings pass through as opaque `{ opaque }`
17
+ * (forward-compat passthrough), invalid/empty/non-string input fails closed.
18
+ */
19
+ export declare function parseThinkingLevel(value: unknown): ThinkingLevel | {
20
+ readonly opaque: string;
21
+ } | undefined;
14
22
  /**
15
23
  * Normalize a host thinkingLevel string. Known levels are lowercased; other non-empty
16
24
  * strings pass through as opaque effort values for forward-compatible provider fields.
@@ -26,17 +34,48 @@ export declare function thinkingCompatFor(family: ThinkingCompatFamily, level: T
26
34
  * Per-turn patches win over prior compat via {@link mergeProviderRequestOptions}.
27
35
  */
28
36
  export declare function applyThinkingLevel(options: ProviderRequestOptions | undefined, level: ThinkingLevel | string, family?: ThinkingCompatFamily): ProviderRequestOptions;
37
+ /**
38
+ * Declared portable thinking levels for a model, if any (ascending ladder order).
39
+ * `undefined` means the provider declares no subset — forward-compat passthrough.
40
+ */
41
+ export declare function thinkingLevelsForModel(model: Pick<ModelConfig, "provider" | "compat" | "capabilities">): readonly string[] | undefined;
42
+ /**
43
+ * Strict declared-set membership (hosts fail closed on unknown levels).
44
+ * A model that declares no levels supports any value (forward-compat passthrough).
45
+ */
46
+ export declare function isSupportedThinkingLevel(model: Pick<ModelConfig, "provider" | "compat" | "capabilities">, level: unknown): boolean;
47
+ /**
48
+ * Snap a portable level to a model's declared set (design record §2):
49
+ * in-set → unchanged; below the declared minimum → up to the minimum
50
+ * (never silently disable what cannot be disabled); otherwise nearest declared
51
+ * level by ladder distance with ties breaking up; undeclared levels and
52
+ * undeclared sets pass through. Provider-documented snap tables
53
+ * (deepseek, Z.AI GLM-5.2, clinepass slots) override this generic fallback
54
+ * inside their own resolvers.
55
+ */
56
+ export declare function snapThinkingLevel(model: Pick<ModelConfig, "provider" | "compat" | "capabilities">, level: ThinkingLevel | string): ThinkingLevel | string;
57
+ /**
58
+ * Model-aware thinking-level application (design record §5). Resolves the family
59
+ * stamp-first (`compat.thinkingFamily` → inference → `capabilities.reasoning`),
60
+ * snaps the level to the model's declared set, and merges the compat patch
61
+ * per-turn-wins. Returns options unchanged for non-reasoning models — never
62
+ * invents a field where the model declares no thinking support.
63
+ */
64
+ export declare function applyThinkingLevelForModel(options: ProviderRequestOptions | undefined, level: ThinkingLevel | string, model: Pick<ModelConfig, "provider" | "compat" | "capabilities">): ProviderRequestOptions;
29
65
  /**
30
66
  * Best-effort family inference from model metadata without a second options tree.
31
- * Prefer an explicit family in hosts/use-case workers when the provider is known.
67
+ * Prefer an explicit `compat.thinkingFamily` stamp in host/use-case workers when
68
+ * the provider is known; inference is the fallback (stamp-first).
32
69
  *
33
70
  * Heuristics (ordered):
34
- * 1. Existing `compat.thinking` object → `thinking_type`
35
- * 2. Existing `compat.reasoning` → `openai_reasoning`
36
- * 3. Existing `compat.reasoning_effort` → `reasoning_effort`
37
- * 4. Provider id starting with `openai` → `openai_reasoning`
38
- * 5. Provider id `neuralwatt` → `reasoning_effort`
39
- * 6. `capabilities.reasoning` → `reasoning_effort` (portable string field)
40
- * 7. Else `noop`
71
+ * 1. `compat.thinkingFamily` stamp → itself
72
+ * 2. Existing `compat.thinking` object → `thinking_type`
73
+ * 3. Existing `compat.thinkingConfig` object/boolean → `google`
74
+ * 4. Existing `compat.reasoning` → `openai_reasoning`
75
+ * 5. Existing `compat.reasoning_effort` → `reasoning_effort`
76
+ * 6. Provider id starting with `openai` → `openai_reasoning`
77
+ * 7. Provider id `neuralwatt` → `reasoning_effort`
78
+ * 8. `capabilities.reasoning` → `reasoning_effort` (portable string field)
79
+ * 9. Else `noop`
41
80
  */
42
81
  export declare function thinkingFamilyForModel(model: Pick<ModelConfig, "provider" | "compat" | "capabilities">): ThinkingCompatFamily;
package/dist/thinking.js CHANGED
@@ -7,6 +7,40 @@ export const THINKING_LEVELS = ["none", "minimal", "low", "medium", "high", "xhi
7
7
  export function isThinkingLevel(value) {
8
8
  return typeof value === "string" && THINKING_LEVELS.includes(value);
9
9
  }
10
+ const LEVEL_RANK = {
11
+ none: 0,
12
+ minimal: 1,
13
+ low: 2,
14
+ medium: 3,
15
+ high: 4,
16
+ xhigh: 5,
17
+ max: 6,
18
+ };
19
+ function isThinkingFamily(value) {
20
+ return (typeof value === "string" &&
21
+ (value === "openai_reasoning" ||
22
+ value === "reasoning_effort" ||
23
+ value === "thinking_type" ||
24
+ value === "google" ||
25
+ value === "output_config_effort" ||
26
+ value === "noop"));
27
+ }
28
+ function thinkingLevelRank(level) {
29
+ return isThinkingLevel(level) ? LEVEL_RANK[level] : undefined;
30
+ }
31
+ /**
32
+ * Parse a host thinking-level value without guessing: known levels canonicalize to
33
+ * `ThinkingLevel`, unknown non-empty strings pass through as opaque `{ opaque }`
34
+ * (forward-compat passthrough), invalid/empty/non-string input fails closed.
35
+ */
36
+ export function parseThinkingLevel(value) {
37
+ if (typeof value !== "string")
38
+ return undefined;
39
+ const normalized = normalizeThinkingLevel(value);
40
+ if (!normalized)
41
+ return undefined;
42
+ return isThinkingLevel(normalized) ? normalized : { opaque: normalized };
43
+ }
10
44
  /**
11
45
  * Normalize a host thinkingLevel string. Known levels are lowercased; other non-empty
12
46
  * strings pass through as opaque effort values for forward-compatible provider fields.
@@ -32,6 +66,10 @@ export function thinkingCompatFor(family, level) {
32
66
  return { reasoning_effort: normalized };
33
67
  case "thinking_type":
34
68
  return { thinking: { type: normalized === "none" ? "disabled" : "enabled" } };
69
+ case "google":
70
+ return { thinkingLevel: normalized };
71
+ case "output_config_effort":
72
+ return { output_config: { effort: normalized } };
35
73
  default: {
36
74
  const _exhaustive = family;
37
75
  return _exhaustive;
@@ -62,23 +100,111 @@ export function applyThinkingLevel(options, level, family = "reasoning_effort")
62
100
  }
63
101
  return mergeProviderRequestOptions(options, { compat: patch });
64
102
  }
103
+ /**
104
+ * Declared portable thinking levels for a model, if any (ascending ladder order).
105
+ * `undefined` means the provider declares no subset — forward-compat passthrough.
106
+ */
107
+ export function thinkingLevelsForModel(model) {
108
+ return model.capabilities?.thinkingLevels;
109
+ }
110
+ /**
111
+ * Strict declared-set membership (hosts fail closed on unknown levels).
112
+ * A model that declares no levels supports any value (forward-compat passthrough).
113
+ */
114
+ export function isSupportedThinkingLevel(model, level) {
115
+ const parsed = parseThinkingLevel(level);
116
+ if (!parsed)
117
+ return false;
118
+ const declared = thinkingLevelsForModel(model);
119
+ if (!declared || declared.length === 0)
120
+ return true;
121
+ const value = typeof parsed === "string" ? parsed : parsed.opaque;
122
+ return declared.includes(value);
123
+ }
124
+ /**
125
+ * Snap a portable level to a model's declared set (design record §2):
126
+ * in-set → unchanged; below the declared minimum → up to the minimum
127
+ * (never silently disable what cannot be disabled); otherwise nearest declared
128
+ * level by ladder distance with ties breaking up; undeclared levels and
129
+ * undeclared sets pass through. Provider-documented snap tables
130
+ * (deepseek, Z.AI GLM-5.2, clinepass slots) override this generic fallback
131
+ * inside their own resolvers.
132
+ */
133
+ export function snapThinkingLevel(model, level) {
134
+ const normalized = normalizeThinkingLevel(String(level));
135
+ if (!normalized)
136
+ return String(level);
137
+ const declared = thinkingLevelsForModel(model);
138
+ if (!declared || declared.length === 0)
139
+ return normalized;
140
+ if (declared.includes(normalized))
141
+ return normalized;
142
+ const rank = thinkingLevelRank(normalized);
143
+ const ranked = declared
144
+ .map((entry) => ({ entry, rank: thinkingLevelRank(entry) }))
145
+ .filter((entry) => entry.rank != null);
146
+ if (rank == null || ranked.length === 0)
147
+ return normalized;
148
+ const minRank = Math.min(...ranked.map(({ rank: r }) => r));
149
+ if (rank < minRank)
150
+ return ranked.find(({ rank: r }) => r === minRank).entry;
151
+ let best = ranked[0].entry;
152
+ let bestDistance = Number.POSITIVE_INFINITY;
153
+ let bestRank = Number.NEGATIVE_INFINITY;
154
+ for (const { entry, rank: candidateRank } of ranked) {
155
+ const distance = Math.abs(candidateRank - rank);
156
+ if (distance < bestDistance || (distance === bestDistance && candidateRank > bestRank)) {
157
+ best = entry;
158
+ bestDistance = distance;
159
+ bestRank = candidateRank;
160
+ }
161
+ }
162
+ return best;
163
+ }
164
+ /**
165
+ * Model-aware thinking-level application (design record §5). Resolves the family
166
+ * stamp-first (`compat.thinkingFamily` → inference → `capabilities.reasoning`),
167
+ * snaps the level to the model's declared set, and merges the compat patch
168
+ * per-turn-wins. Returns options unchanged for non-reasoning models — never
169
+ * invents a field where the model declares no thinking support.
170
+ */
171
+ export function applyThinkingLevelForModel(options, level, model) {
172
+ const normalized = normalizeThinkingLevel(String(level));
173
+ if (!normalized)
174
+ return options ?? {};
175
+ const family = model.compat?.thinkingFamily != null && isThinkingFamily(model.compat.thinkingFamily)
176
+ ? model.compat.thinkingFamily
177
+ : thinkingFamilyForModel(model);
178
+ if (family === "noop")
179
+ return options ?? {};
180
+ return applyThinkingLevel(options, snapThinkingLevel(model, normalized), family);
181
+ }
65
182
  /**
66
183
  * Best-effort family inference from model metadata without a second options tree.
67
- * Prefer an explicit family in hosts/use-case workers when the provider is known.
184
+ * Prefer an explicit `compat.thinkingFamily` stamp in host/use-case workers when
185
+ * the provider is known; inference is the fallback (stamp-first).
68
186
  *
69
187
  * Heuristics (ordered):
70
- * 1. Existing `compat.thinking` object → `thinking_type`
71
- * 2. Existing `compat.reasoning` → `openai_reasoning`
72
- * 3. Existing `compat.reasoning_effort` → `reasoning_effort`
73
- * 4. Provider id starting with `openai` → `openai_reasoning`
74
- * 5. Provider id `neuralwatt` → `reasoning_effort`
75
- * 6. `capabilities.reasoning` → `reasoning_effort` (portable string field)
76
- * 7. Else `noop`
188
+ * 1. `compat.thinkingFamily` stamp → itself
189
+ * 2. Existing `compat.thinking` object → `thinking_type`
190
+ * 3. Existing `compat.thinkingConfig` object/boolean → `google`
191
+ * 4. Existing `compat.reasoning` → `openai_reasoning`
192
+ * 5. Existing `compat.reasoning_effort` → `reasoning_effort`
193
+ * 6. Provider id starting with `openai` → `openai_reasoning`
194
+ * 7. Provider id `neuralwatt` → `reasoning_effort`
195
+ * 8. `capabilities.reasoning` → `reasoning_effort` (portable string field)
196
+ * 9. Else `noop`
77
197
  */
78
198
  export function thinkingFamilyForModel(model) {
199
+ const stamp = model.compat?.thinkingFamily;
200
+ if (isThinkingFamily(stamp))
201
+ return stamp;
79
202
  const compat = model.compat ?? {};
80
203
  if (compat.thinking != null && typeof compat.thinking === "object")
81
204
  return "thinking_type";
205
+ if (compat.thinkingConfig != null && (typeof compat.thinkingConfig === "object" || typeof compat.thinkingConfig === "boolean")) {
206
+ return "google";
207
+ }
82
208
  if (compat.reasoning != null)
83
209
  return "openai_reasoning";
84
210
  if (compat.reasoning_effort != null)
@@ -0,0 +1,76 @@
1
+ import type { Message, ToolDefinition } from "./contracts.js";
2
+ /** Same shape as `AgentInput`; kept structural here so core's assembler and this module never form a runtime cycle. */
3
+ type ToolSearchInput = string | Message | readonly Message[];
4
+ export type ToolsDisclosure = "all" | "search";
5
+ export interface ToolsSearchOptions {
6
+ /** Top-k tools disclosed per turn. Default 16; clamped to the hard cap. */
7
+ readonly topK?: number;
8
+ }
9
+ export declare const SEARCH_TOOLS_TOOL_NAME = "search_tools";
10
+ export declare const DEFAULT_TOOLS_SEARCH_TOP_K = 16;
11
+ export declare const HARD_MAX_TOOLS_SEARCH_TOP_K = 64;
12
+ /** Frozen index cap; larger registries fail closed to full disclosure. */
13
+ export declare const HARD_MAX_TOOLS_INDEX = 1024;
14
+ export declare const DEFAULT_MAX_TOOLS_SEARCH_QUERY_BYTES = 4096;
15
+ export declare const HARD_MAX_TOOLS_SEARCH_QUERY_BYTES = 65536;
16
+ export declare const TOOL_DISCLOSURE_ERROR_CODE: "tool_disclosure_exceeded";
17
+ export declare class ToolDisclosureError extends Error {
18
+ readonly code: "tool_disclosure_exceeded";
19
+ constructor(message: string);
20
+ }
21
+ export declare function isToolDisclosureError(error: unknown): error is ToolDisclosureError;
22
+ /** Run options win over agent config (mirrors resolveSkillsDisclosure); default "all". */
23
+ export declare function resolveToolsDisclosure(run?: ToolsDisclosure, agent?: ToolsDisclosure): ToolsDisclosure;
24
+ /** Activation set: same shape as LoadedSkillSet (names-only persistence, per-session instance). */
25
+ export interface ActiveToolSet {
26
+ has(name: string): boolean;
27
+ add(name: string): void;
28
+ list(): readonly string[];
29
+ clear(): void;
30
+ }
31
+ export declare function createActiveToolSet(): ActiveToolSet;
32
+ interface PostingEntry {
33
+ readonly toolIndex: number;
34
+ tf: number;
35
+ }
36
+ export interface ToolSearchIndex {
37
+ readonly tools: readonly ToolDefinition[];
38
+ readonly postings: ReadonlyMap<string, PostingEntry[]>;
39
+ }
40
+ /** ponytail: O(n·d) lexical index, rebuilt on registry change by the caller — embedder-backed
41
+ * scoring via the @arnilo/prism-memory/rag seam if accuracy fixtures fall short (plan 041). */
42
+ export declare function createToolSearchIndex(tools: readonly ToolDefinition[], skip?: (tool: ToolDefinition) => boolean): ToolSearchIndex;
43
+ export interface ToolSearchMatch {
44
+ readonly name: string;
45
+ readonly description?: string;
46
+ /** Query terms present in name/description, most significant first. */
47
+ readonly matched: readonly string[];
48
+ }
49
+ /** Bounded lexical scoring: BM25-lite (tf × IDF, registry-derived DF). Ties keep registry order. */
50
+ export declare function scoreTools(index: ToolSearchIndex, query: string, k: number, queryByteCap?: number): readonly ToolSearchMatch[];
51
+ export interface ToolSearchState {
52
+ readonly index: ToolSearchIndex;
53
+ readonly activated: ActiveToolSet;
54
+ readonly topK: number;
55
+ }
56
+ export declare function createToolSearchState(options: {
57
+ readonly tools: readonly ToolDefinition[];
58
+ readonly activated: ActiveToolSet;
59
+ readonly search?: ToolsSearchOptions;
60
+ }): ToolSearchState;
61
+ /** Provider-facing narrowing: activated tools ∪ top-k for the turn. Fails closed — any
62
+ * scoring/index error discloses the full input list (never zero, never wider than input). */
63
+ export declare function selectDisclosedTools(options: {
64
+ readonly tools: readonly ToolDefinition[];
65
+ readonly input: ToolSearchInput;
66
+ readonly queryByteCap?: number;
67
+ readonly search?: ToolsSearchOptions;
68
+ readonly activated?: {
69
+ has(name: string): boolean;
70
+ };
71
+ }): readonly ToolDefinition[];
72
+ /** Model-facing activation tool, generated only in search mode. Results are inert
73
+ * name+description lines — no schemas or bodies — and activation re-checks allow/deny
74
+ * at dispatch time (blocked-reason matrix unchanged). */
75
+ export declare function createSearchToolsTool(state: ToolSearchState, queryByteCap?: number): ToolDefinition;
76
+ export {};