@librechat/agents 4.0.1 → 4.0.3

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 (245) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +3 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/decisions/deadline.cjs +52 -0
  4. package/dist/cjs/decisions/deadline.cjs.map +1 -0
  5. package/dist/cjs/decisions/dialect.cjs +88 -0
  6. package/dist/cjs/decisions/dialect.cjs.map +1 -0
  7. package/dist/cjs/decisions/http.cjs +77 -0
  8. package/dist/cjs/decisions/http.cjs.map +1 -0
  9. package/dist/cjs/decisions/index.cjs +7 -0
  10. package/dist/cjs/decisions/presets.cjs +82 -0
  11. package/dist/cjs/decisions/presets.cjs.map +1 -0
  12. package/dist/cjs/decisions/questions.cjs +70 -0
  13. package/dist/cjs/decisions/questions.cjs.map +1 -0
  14. package/dist/cjs/decisions/structuredChat.cjs +172 -0
  15. package/dist/cjs/decisions/structuredChat.cjs.map +1 -0
  16. package/dist/cjs/decisions/structuredOutput.cjs +18 -0
  17. package/dist/cjs/decisions/structuredOutput.cjs.map +1 -0
  18. package/dist/cjs/decisions/traceMarker.cjs +6 -0
  19. package/dist/cjs/decisions/traceMarker.cjs.map +1 -0
  20. package/dist/cjs/decisions/transport.cjs +166 -0
  21. package/dist/cjs/decisions/transport.cjs.map +1 -0
  22. package/dist/cjs/decisions/types.cjs +44 -0
  23. package/dist/cjs/decisions/types.cjs.map +1 -0
  24. package/dist/cjs/graphs/Graph.cjs +28 -9
  25. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  26. package/dist/cjs/langfuseToolOutputTracing.cjs +6 -1
  27. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  28. package/dist/cjs/llm/google/index.cjs +1 -1
  29. package/dist/cjs/llm/invoke.cjs +34 -12
  30. package/dist/cjs/llm/invoke.cjs.map +1 -1
  31. package/dist/cjs/llm/openai/index.cjs +7 -0
  32. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  33. package/dist/cjs/llm/providerTextBoundary.cjs +355 -0
  34. package/dist/cjs/llm/providerTextBoundary.cjs.map +1 -0
  35. package/dist/cjs/llm/providerTextChunk.cjs +14 -0
  36. package/dist/cjs/llm/providerTextChunk.cjs.map +1 -0
  37. package/dist/cjs/llm/providerTextControls.cjs +236 -0
  38. package/dist/cjs/llm/providerTextControls.cjs.map +1 -0
  39. package/dist/cjs/llm/stream/smoother.cjs +2 -1
  40. package/dist/cjs/llm/stream/smoother.cjs.map +1 -1
  41. package/dist/cjs/main.cjs +49 -1
  42. package/dist/cjs/protection/providerText.cjs +224 -0
  43. package/dist/cjs/protection/providerText.cjs.map +1 -0
  44. package/dist/cjs/protection/providerTextInput.cjs +25 -0
  45. package/dist/cjs/protection/providerTextInput.cjs.map +1 -0
  46. package/dist/cjs/rerank/index.cjs +2 -0
  47. package/dist/cjs/rerank/search.cjs +45 -0
  48. package/dist/cjs/rerank/search.cjs.map +1 -0
  49. package/dist/cjs/rerank/systemone.cjs +70 -0
  50. package/dist/cjs/rerank/systemone.cjs.map +1 -0
  51. package/dist/cjs/run.cjs +11 -1
  52. package/dist/cjs/run.cjs.map +1 -1
  53. package/dist/cjs/stream.cjs +42 -14
  54. package/dist/cjs/stream.cjs.map +1 -1
  55. package/dist/cjs/summarization/node.cjs +26 -8
  56. package/dist/cjs/summarization/node.cjs.map +1 -1
  57. package/dist/cjs/tools/SubagentTool.cjs +11 -3
  58. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  59. package/dist/cjs/tools/ToolNode.cjs +6 -5
  60. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  61. package/dist/cjs/tools/search/rerankers.cjs +1 -0
  62. package/dist/cjs/tools/search/search.cjs +3 -3
  63. package/dist/cjs/tools/search/search.cjs.map +1 -1
  64. package/dist/cjs/tools/search/tool.cjs +4 -3
  65. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  66. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +13 -2
  67. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -1
  68. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +149 -26
  69. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  70. package/dist/cjs/tools/subagent/SubagentReplay.cjs +4 -2
  71. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  72. package/dist/cjs/tools/subagent/childGraphConfig.cjs +3 -1
  73. package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -1
  74. package/dist/cjs/tools/subagent/diagnostics.cjs +88 -2
  75. package/dist/cjs/tools/subagent/diagnostics.cjs.map +1 -1
  76. package/dist/cjs/tools/subagent/hostArgs.cjs +214 -0
  77. package/dist/cjs/tools/subagent/hostArgs.cjs.map +1 -0
  78. package/dist/cjs/tools/subagent/index.cjs +2 -0
  79. package/dist/cjs/tools/toolErrorContent.cjs +1 -1
  80. package/dist/esm/agents/AgentContext.mjs +3 -2
  81. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  82. package/dist/esm/decisions/deadline.mjs +52 -0
  83. package/dist/esm/decisions/deadline.mjs.map +1 -0
  84. package/dist/esm/decisions/dialect.mjs +87 -0
  85. package/dist/esm/decisions/dialect.mjs.map +1 -0
  86. package/dist/esm/decisions/http.mjs +75 -0
  87. package/dist/esm/decisions/http.mjs.map +1 -0
  88. package/dist/esm/decisions/index.mjs +8 -0
  89. package/dist/esm/decisions/presets.mjs +77 -0
  90. package/dist/esm/decisions/presets.mjs.map +1 -0
  91. package/dist/esm/decisions/questions.mjs +67 -0
  92. package/dist/esm/decisions/questions.mjs.map +1 -0
  93. package/dist/esm/decisions/structuredChat.mjs +172 -0
  94. package/dist/esm/decisions/structuredChat.mjs.map +1 -0
  95. package/dist/esm/decisions/structuredOutput.mjs +18 -0
  96. package/dist/esm/decisions/structuredOutput.mjs.map +1 -0
  97. package/dist/esm/decisions/traceMarker.mjs +6 -0
  98. package/dist/esm/decisions/traceMarker.mjs.map +1 -0
  99. package/dist/esm/decisions/transport.mjs +164 -0
  100. package/dist/esm/decisions/transport.mjs.map +1 -0
  101. package/dist/esm/decisions/types.mjs +39 -0
  102. package/dist/esm/decisions/types.mjs.map +1 -0
  103. package/dist/esm/graphs/Graph.mjs +28 -9
  104. package/dist/esm/graphs/Graph.mjs.map +1 -1
  105. package/dist/esm/langfuseToolOutputTracing.mjs +6 -1
  106. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  107. package/dist/esm/llm/google/index.mjs +1 -1
  108. package/dist/esm/llm/invoke.mjs +34 -12
  109. package/dist/esm/llm/invoke.mjs.map +1 -1
  110. package/dist/esm/llm/openai/index.mjs +7 -0
  111. package/dist/esm/llm/openai/index.mjs.map +1 -1
  112. package/dist/esm/llm/providerTextBoundary.mjs +355 -0
  113. package/dist/esm/llm/providerTextBoundary.mjs.map +1 -0
  114. package/dist/esm/llm/providerTextChunk.mjs +13 -0
  115. package/dist/esm/llm/providerTextChunk.mjs.map +1 -0
  116. package/dist/esm/llm/providerTextControls.mjs +230 -0
  117. package/dist/esm/llm/providerTextControls.mjs.map +1 -0
  118. package/dist/esm/llm/stream/smoother.mjs +2 -1
  119. package/dist/esm/llm/stream/smoother.mjs.map +1 -1
  120. package/dist/esm/main.mjs +16 -2
  121. package/dist/esm/protection/providerText.mjs +218 -0
  122. package/dist/esm/protection/providerText.mjs.map +1 -0
  123. package/dist/esm/protection/providerTextInput.mjs +24 -0
  124. package/dist/esm/protection/providerTextInput.mjs.map +1 -0
  125. package/dist/esm/rerank/index.mjs +3 -0
  126. package/dist/esm/rerank/search.mjs +45 -0
  127. package/dist/esm/rerank/search.mjs.map +1 -0
  128. package/dist/esm/rerank/systemone.mjs +70 -0
  129. package/dist/esm/rerank/systemone.mjs.map +1 -0
  130. package/dist/esm/run.mjs +11 -1
  131. package/dist/esm/run.mjs.map +1 -1
  132. package/dist/esm/stream.mjs +42 -14
  133. package/dist/esm/stream.mjs.map +1 -1
  134. package/dist/esm/summarization/node.mjs +26 -8
  135. package/dist/esm/summarization/node.mjs.map +1 -1
  136. package/dist/esm/tools/SubagentTool.mjs +11 -3
  137. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  138. package/dist/esm/tools/ToolNode.mjs +6 -5
  139. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  140. package/dist/esm/tools/search/rerankers.mjs +1 -1
  141. package/dist/esm/tools/search/search.mjs +3 -3
  142. package/dist/esm/tools/search/search.mjs.map +1 -1
  143. package/dist/esm/tools/search/tool.mjs +4 -3
  144. package/dist/esm/tools/search/tool.mjs.map +1 -1
  145. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +13 -2
  146. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -1
  147. package/dist/esm/tools/subagent/SubagentExecutor.mjs +149 -26
  148. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  149. package/dist/esm/tools/subagent/SubagentReplay.mjs +4 -2
  150. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  151. package/dist/esm/tools/subagent/childGraphConfig.mjs +3 -1
  152. package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -1
  153. package/dist/esm/tools/subagent/diagnostics.mjs +84 -3
  154. package/dist/esm/tools/subagent/diagnostics.mjs.map +1 -1
  155. package/dist/esm/tools/subagent/hostArgs.mjs +207 -0
  156. package/dist/esm/tools/subagent/hostArgs.mjs.map +1 -0
  157. package/dist/esm/tools/subagent/index.mjs +2 -0
  158. package/dist/esm/tools/toolErrorContent.mjs +1 -1
  159. package/dist/types/decisions/deadline.d.ts +3 -0
  160. package/dist/types/decisions/dialect.d.ts +11 -0
  161. package/dist/types/decisions/http.d.ts +22 -0
  162. package/dist/types/decisions/index.d.ts +10 -0
  163. package/dist/types/decisions/presets.d.ts +17 -0
  164. package/dist/types/decisions/questions.d.ts +8 -0
  165. package/dist/types/decisions/structuredChat.d.ts +17 -0
  166. package/dist/types/decisions/structuredOutput.d.ts +4 -0
  167. package/dist/types/decisions/traceMarker.d.ts +2 -0
  168. package/dist/types/decisions/transport.d.ts +29 -0
  169. package/dist/types/decisions/types.d.ts +119 -0
  170. package/dist/types/graphs/Graph.d.ts +7 -3
  171. package/dist/types/index.d.ts +4 -0
  172. package/dist/types/llm/invoke.d.ts +4 -1
  173. package/dist/types/llm/providerTextBoundary.d.ts +4 -0
  174. package/dist/types/llm/providerTextChunk.d.ts +7 -0
  175. package/dist/types/llm/providerTextControls.d.ts +18 -0
  176. package/dist/types/protection/providerText.d.ts +77 -0
  177. package/dist/types/protection/providerTextInput.d.ts +3 -0
  178. package/dist/types/rerank/index.d.ts +4 -0
  179. package/dist/types/rerank/search.d.ts +7 -0
  180. package/dist/types/rerank/systemone.d.ts +13 -0
  181. package/dist/types/rerank/types.d.ts +24 -0
  182. package/dist/types/run.d.ts +2 -0
  183. package/dist/types/tools/BashExecutor.d.ts +4 -0
  184. package/dist/types/tools/CodeExecutor.d.ts +4 -0
  185. package/dist/types/tools/ReadFile.d.ts +4 -0
  186. package/dist/types/tools/SkillTool.d.ts +4 -0
  187. package/dist/types/tools/SubagentTool.d.ts +2 -0
  188. package/dist/types/tools/ToolSearch.d.ts +4 -0
  189. package/dist/types/tools/search/schema.d.ts +4 -0
  190. package/dist/types/tools/search/types.d.ts +3 -1
  191. package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +6 -0
  192. package/dist/types/tools/subagent/SubagentExecutor.d.ts +22 -1
  193. package/dist/types/tools/subagent/SubagentReplay.d.ts +3 -1
  194. package/dist/types/tools/subagent/diagnostics.d.ts +56 -8
  195. package/dist/types/tools/subagent/hostArgs.d.ts +57 -0
  196. package/dist/types/tools/subagent/index.d.ts +3 -0
  197. package/dist/types/types/graph.d.ts +39 -0
  198. package/dist/types/types/run.d.ts +6 -2
  199. package/dist/types/types/tools.d.ts +2 -0
  200. package/package.json +4 -4
  201. package/src/agents/AgentContext.ts +3 -2
  202. package/src/decisions/deadline.ts +92 -0
  203. package/src/decisions/dialect.ts +200 -0
  204. package/src/decisions/http.ts +175 -0
  205. package/src/decisions/index.ts +14 -0
  206. package/src/decisions/presets.ts +111 -0
  207. package/src/decisions/questions.ts +128 -0
  208. package/src/decisions/structuredChat.ts +358 -0
  209. package/src/decisions/structuredOutput.ts +25 -0
  210. package/src/decisions/traceMarker.ts +2 -0
  211. package/src/decisions/transport.ts +335 -0
  212. package/src/decisions/types.ts +193 -0
  213. package/src/graphs/Graph.ts +57 -9
  214. package/src/index.ts +9 -0
  215. package/src/langfuseToolOutputTracing.ts +8 -1
  216. package/src/llm/invoke.ts +48 -7
  217. package/src/llm/openai/index.ts +8 -0
  218. package/src/llm/providerTextBoundary.ts +446 -0
  219. package/src/llm/providerTextChunk.ts +15 -0
  220. package/src/llm/providerTextControls.ts +173 -0
  221. package/src/llm/stream/smoother.ts +4 -2
  222. package/src/protection/providerText.ts +325 -0
  223. package/src/protection/providerTextInput.ts +21 -0
  224. package/src/rerank/index.ts +4 -0
  225. package/src/rerank/search.ts +85 -0
  226. package/src/rerank/systemone.ts +140 -0
  227. package/src/rerank/types.ts +28 -0
  228. package/src/run.ts +13 -0
  229. package/src/stream.ts +33 -7
  230. package/src/summarization/node.ts +32 -14
  231. package/src/tools/SubagentTool.ts +14 -3
  232. package/src/tools/ToolNode.ts +7 -3
  233. package/src/tools/search/search.ts +4 -4
  234. package/src/tools/search/tool.ts +27 -25
  235. package/src/tools/search/types.ts +4 -1
  236. package/src/tools/subagent/SubagentExecutionRegistry.ts +27 -2
  237. package/src/tools/subagent/SubagentExecutor.ts +323 -28
  238. package/src/tools/subagent/SubagentReplay.ts +10 -3
  239. package/src/tools/subagent/childGraphConfig.ts +12 -1
  240. package/src/tools/subagent/diagnostics.ts +180 -9
  241. package/src/tools/subagent/hostArgs.ts +413 -0
  242. package/src/tools/subagent/index.ts +20 -0
  243. package/src/types/graph.ts +44 -0
  244. package/src/types/run.ts +6 -2
  245. package/src/types/tools.ts +6 -1
@@ -24,8 +24,9 @@ export type LegacyGraphConfig = BaseGraphConfig & {
24
24
  } & Omit<g.StandardGraphInput,
25
25
  /** `streamLimits` is excluded because legacy graphs receive limits only
26
26
  * via the top-level `RunConfig.streamLimits`; accepting the field here
27
- * would type-check but be silently ignored by `createLegacyGraph`. */
28
- 'provider' | 'clientOptions' | 'agents' | 'streamLimits'> & Omit<g.AgentInputs, 'provider' | 'clientOptions' | 'agentId'>;
27
+ * would type-check but be silently ignored by `createLegacyGraph`.
28
+ * Required provider-text protection also belongs only to RunConfig. */
29
+ 'provider' | 'clientOptions' | 'agents' | 'streamLimits' | 'providerTextProtection'> & Omit<g.AgentInputs, 'provider' | 'clientOptions' | 'agentId'>;
29
30
  export type SupervisedGraphConfig = BaseGraphConfig & {
30
31
  type: 'supervised';
31
32
  /** Enable supervised router; when false, fall back to standard loop */
@@ -281,6 +282,8 @@ export type RunConfig = {
281
282
  subagentTasks?: SubagentTaskConfig;
282
283
  /** Authorizes and supplies host-owned context to isolated child runs. */
283
284
  subagentContext?: g.SubagentContextAdapter;
285
+ /** Host diagnostic sink for foreground, detached, and nested startup failures. */
286
+ onSubagentResolutionFailure?: g.StandardGraphInput['onSubagentResolutionFailure'];
284
287
  /**
285
288
  * Pre-constructed hook registry for this run. Hooks fire at lifecycle
286
289
  * points in `processStream` (RunStart, UserPromptSubmit, Stop,
@@ -315,6 +318,7 @@ export type RunConfig = {
315
318
  * tool-call argument byte cap is ON by default, the per-turn event cap is
316
319
  * opt-in. See {@link StreamLimits}.
317
320
  */
321
+ providerTextProtection?: g.StandardGraphInput['providerTextProtection'];
318
322
  streamLimits?: StreamLimits;
319
323
  returnContent?: boolean;
320
324
  tokenCounter?: TokenCounter;
@@ -502,6 +502,8 @@ export type JsonSchemaType = {
502
502
  properties?: Record<string, JsonSchemaType>;
503
503
  required?: string[];
504
504
  description?: string;
505
+ pattern?: string;
506
+ maxLength?: number;
505
507
  additionalProperties?: boolean | JsonSchemaType;
506
508
  };
507
509
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "4.0.1",
3
+ "version": "4.0.3",
4
4
  "reova": {
5
5
  "enabled": true,
6
6
  "endpoint": "https://telemetry.reo.dev/data"
@@ -329,7 +329,7 @@
329
329
  "uuid": "^11.1.1"
330
330
  },
331
331
  "peerDependencies": {
332
- "@anthropic-ai/sandbox-runtime": "^0.0.67"
332
+ "@anthropic-ai/sandbox-runtime": "^0.0.78"
333
333
  },
334
334
  "peerDependenciesMeta": {
335
335
  "@anthropic-ai/sandbox-runtime": {
@@ -337,7 +337,7 @@
337
337
  }
338
338
  },
339
339
  "devDependencies": {
340
- "@anthropic-ai/sandbox-runtime": "^0.0.67",
340
+ "@anthropic-ai/sandbox-runtime": "0.0.78",
341
341
  "@anthropic-ai/vertex-sdk": "^0.12.0",
342
342
  "@eslint/js": "^10.0.1",
343
343
  "@langchain/langgraph-checkpoint-mongodb": "^1.4.1",
@@ -360,7 +360,7 @@
360
360
  "mongodb-memory-server-core": "^11.2.0",
361
361
  "prettier": "^3.6.2",
362
362
  "ts-jest": "^29.4.12",
363
- "tsc-alias": "^1.8.10",
363
+ "tsc-alias": "^1.9.7",
364
364
  "tsdown": "^0.22.2",
365
365
  "tslib": "^2.6.3",
366
366
  "tsx": "^4.23.12",
@@ -58,6 +58,7 @@ import {
58
58
  import { isTokenCounterCacheCompatible } from '@/llm/tokenCounterCacheCompatibility';
59
59
  import { snapshotCompactionSemanticIndex } from '@/summarization/semanticIndex';
60
60
  import { createExactTokenCountCache } from '@/llm/contextPressureMeter';
61
+ import { markProviderTextInput } from '@/protection/providerTextInput';
61
62
  import { buildSummaryCarrierText } from '@/summarization/shared';
62
63
  import { createSchemaOnlyTools } from '@/tools/schema';
63
64
  import { apportionTokenCounts } from '@/utils/tokens';
@@ -960,7 +961,7 @@ export class AgentContext {
960
961
  : 0;
961
962
  }
962
963
 
963
- return RunnableLambda.from((messages: BaseMessage[]) => {
964
+ return markProviderTextInput(RunnableLambda.from((messages: BaseMessage[]) => {
964
965
  const prefix: BaseMessage[] = systemMessage ? [systemMessage] : [];
965
966
 
966
967
  // Build the non-system portion (summary + conversation), then apply
@@ -998,7 +999,7 @@ export class AgentContext {
998
999
  );
999
1000
  }
1000
1001
  return [...prefix, ...body];
1001
- }).withConfig({ runName: 'prompt' });
1002
+ })).withConfig({ runName: 'prompt' });
1002
1003
  }
1003
1004
 
1004
1005
  private buildSummaryHumanMessage(
@@ -0,0 +1,92 @@
1
+ import { DecisionError } from './types';
2
+
3
+ export type AwaitWithinDeadline = <T>(task: Promise<T>) => Promise<T>;
4
+
5
+ /** Bounds even non-cooperative credential minters, fetches, body reads, and model calls. */
6
+ export async function withDecisionDeadline<T>(
7
+ provider: string,
8
+ timeoutMs: number,
9
+ callerSignal: AbortSignal | undefined,
10
+ operation: (signal: AbortSignal, waitFor: AwaitWithinDeadline) => Promise<T>
11
+ ): Promise<T> {
12
+ if (
13
+ !Number.isSafeInteger(timeoutMs) ||
14
+ timeoutMs <= 0 ||
15
+ timeoutMs > 3_600_000
16
+ ) {
17
+ throw new DecisionError('bad_request', 'invalid decision model timeout', {
18
+ provider,
19
+ });
20
+ }
21
+ if (callerSignal?.aborted === true) {
22
+ throw new DecisionError('aborted', 'caller aborted the request', {
23
+ provider,
24
+ });
25
+ }
26
+
27
+ const expiresAt = performance.now() + timeoutMs;
28
+ const deadline = new AbortController();
29
+ const signal = callerSignal
30
+ ? AbortSignal.any([callerSignal, deadline.signal])
31
+ : deadline.signal;
32
+ const timer = setTimeout(() => deadline.abort(), timeoutMs);
33
+ const expired = (): boolean => {
34
+ if (performance.now() >= expiresAt) {
35
+ deadline.abort();
36
+ }
37
+ return signal.aborted;
38
+ };
39
+ const abortError = (): DecisionError => {
40
+ if (callerSignal?.aborted === true) {
41
+ return new DecisionError('aborted', 'caller aborted the request', {
42
+ provider,
43
+ });
44
+ }
45
+ return new DecisionError('timeout', 'decision model deadline exceeded', {
46
+ provider,
47
+ });
48
+ };
49
+
50
+ const waitFor: AwaitWithinDeadline = <U>(task: Promise<U>): Promise<U> =>
51
+ new Promise<U>((resolve, reject) => {
52
+ if (expired()) {
53
+ void task.catch(() => {});
54
+ reject(abortError());
55
+ return;
56
+ }
57
+ const onAbort = (): void => {
58
+ signal.removeEventListener('abort', onAbort);
59
+ reject(abortError());
60
+ };
61
+ signal.addEventListener('abort', onAbort, { once: true });
62
+ if (expired()) {
63
+ onAbort();
64
+ }
65
+ task.then(
66
+ (value) => {
67
+ signal.removeEventListener('abort', onAbort);
68
+ if (expired()) {
69
+ reject(abortError());
70
+ } else {
71
+ resolve(value);
72
+ }
73
+ },
74
+ (error: unknown) => {
75
+ signal.removeEventListener('abort', onAbort);
76
+ reject(expired() ? abortError() : error);
77
+ }
78
+ );
79
+ });
80
+
81
+ try {
82
+ return await waitFor(operation(signal, waitFor));
83
+ } catch (error) {
84
+ if (expired()) {
85
+ throw abortError();
86
+ }
87
+ throw error;
88
+ } finally {
89
+ expired();
90
+ clearTimeout(timer);
91
+ }
92
+ }
@@ -0,0 +1,200 @@
1
+ import type {
2
+ DecisionAnswer,
3
+ DecisionDialect,
4
+ DecisionQuestion,
5
+ } from './types';
6
+ import { isDecisionObject } from './types';
7
+
8
+ interface WireQuestion {
9
+ type: string;
10
+ instructions: DecisionQuestion['instructions'];
11
+ criteria?: DecisionQuestion['criteria'];
12
+ }
13
+
14
+ interface WireAnswer {
15
+ type?: unknown;
16
+ probability?: unknown;
17
+ noul?: unknown;
18
+ choice?: unknown;
19
+ score?: unknown;
20
+ confidence?: unknown;
21
+ probabilities?: unknown;
22
+ }
23
+
24
+ /** System One calls a yes/no question a `noul`; both dialects use a `{true, false}` criterion. */
25
+ export function toWireQuestion(
26
+ question: DecisionQuestion,
27
+ dialect: DecisionDialect
28
+ ): WireQuestion {
29
+ const type =
30
+ dialect === 'systemone' && question.type === 'boolean'
31
+ ? 'noul'
32
+ : question.type;
33
+ if (question.criteria == null) {
34
+ return { type, instructions: question.instructions };
35
+ }
36
+ const criteria =
37
+ question.type === 'boolean' && typeof question.criteria === 'string'
38
+ ? { true: question.criteria }
39
+ : question.criteria;
40
+ return { type, instructions: question.instructions, criteria };
41
+ }
42
+
43
+ function isProbability(value: unknown): value is number {
44
+ return (
45
+ typeof value === 'number' &&
46
+ Number.isFinite(value) &&
47
+ value >= 0 &&
48
+ value <= 1
49
+ );
50
+ }
51
+
52
+ /** `undefined` means malformed; `null` means no distribution was reported. */
53
+ function probabilitiesOf(
54
+ raw: unknown,
55
+ answerType: 'choice' | 'score',
56
+ question?: DecisionQuestion,
57
+ selectedChoice?: string
58
+ ): Record<string, number> | null | undefined {
59
+ if (raw == null) {
60
+ return null;
61
+ }
62
+ if (!isDecisionObject(raw)) {
63
+ return undefined;
64
+ }
65
+ const entries = Object.entries(raw);
66
+ if (entries.length === 0) {
67
+ return null;
68
+ }
69
+ if (
70
+ (question?.type === 'choice' &&
71
+ entries.length !== Object.keys(question.criteria).length) ||
72
+ (question?.type === 'score' && entries.length !== question.criteria.length)
73
+ ) {
74
+ return undefined;
75
+ }
76
+ const probabilities: Record<string, number> = Object.create(null);
77
+ let total = 0;
78
+ let maximum = 0;
79
+ for (const [key, value] of entries) {
80
+ if (!isProbability(value)) {
81
+ return undefined;
82
+ }
83
+ if (question?.type === 'choice' && !Object.hasOwn(question.criteria, key)) {
84
+ return undefined;
85
+ }
86
+ if (
87
+ answerType === 'score' &&
88
+ (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= entries.length)
89
+ ) {
90
+ return undefined;
91
+ }
92
+ probabilities[key] = value;
93
+ total += value;
94
+ maximum = Math.max(maximum, value);
95
+ }
96
+ if (
97
+ selectedChoice !== undefined &&
98
+ (!Object.hasOwn(probabilities, selectedChoice) ||
99
+ probabilities[selectedChoice] < maximum)
100
+ ) {
101
+ return undefined;
102
+ }
103
+ const roundingTolerance = Math.min(
104
+ 0.02,
105
+ Math.max(0.01, entries.length * 0.00005 + 0.001)
106
+ );
107
+ return Math.abs(total - 1) <= roundingTolerance ? probabilities : undefined;
108
+ }
109
+
110
+ /** A validated answer, or `null` for one that cannot safely be read. */
111
+ export function readAnswer(
112
+ answer: unknown,
113
+ dialect: DecisionDialect,
114
+ question?: DecisionQuestion
115
+ ): DecisionAnswer | null {
116
+ if (!isDecisionObject(answer)) {
117
+ return null;
118
+ }
119
+ const record: WireAnswer = answer;
120
+ const booleanType = dialect === 'systemone' ? 'noul' : 'boolean';
121
+ const probability =
122
+ dialect === 'systemone' ? record.noul : record.probability;
123
+ if (record.type === booleanType) {
124
+ if (
125
+ !isProbability(probability) ||
126
+ (question != null && question.type !== 'boolean')
127
+ ) {
128
+ return null;
129
+ }
130
+ return { type: 'boolean', probability };
131
+ }
132
+ if (record.type !== 'choice' && record.type !== 'score') {
133
+ return null;
134
+ }
135
+ const confidence = record.confidence;
136
+ if (confidence != null && !isProbability(confidence)) {
137
+ return null;
138
+ }
139
+ const probabilities = probabilitiesOf(
140
+ record.probabilities,
141
+ record.type,
142
+ question,
143
+ record.type === 'choice' && typeof record.choice === 'string'
144
+ ? record.choice
145
+ : undefined
146
+ );
147
+ if (probabilities === undefined) {
148
+ return null;
149
+ }
150
+ if (record.type === 'choice') {
151
+ if (
152
+ typeof record.choice !== 'string' ||
153
+ (question != null &&
154
+ (question.type !== 'choice' ||
155
+ !Object.hasOwn(question.criteria, record.choice)))
156
+ ) {
157
+ return null;
158
+ }
159
+ return {
160
+ type: 'choice',
161
+ choice: record.choice,
162
+ confidence: confidence ?? null,
163
+ probabilities,
164
+ };
165
+ }
166
+ if (
167
+ typeof record.score !== 'number' ||
168
+ !Number.isFinite(record.score) ||
169
+ record.score < 0 ||
170
+ (question != null &&
171
+ (question.type !== 'score' ||
172
+ record.score > question.criteria.length - 1))
173
+ ) {
174
+ return null;
175
+ }
176
+ if (probabilities !== null) {
177
+ let weightedScore = 0;
178
+ let levels = 0;
179
+ for (const [level, probability] of Object.entries(probabilities)) {
180
+ weightedScore += Number(level) * probability;
181
+ levels += 1;
182
+ }
183
+ const roundingTolerance = Math.min(
184
+ 0.02,
185
+ 0.00005 * (1 + (levels * (levels - 1)) / 2) + 0.001
186
+ );
187
+ if (
188
+ !Number.isFinite(weightedScore) ||
189
+ Math.abs(record.score - weightedScore) > roundingTolerance
190
+ ) {
191
+ return null;
192
+ }
193
+ }
194
+ return {
195
+ type: 'score',
196
+ score: record.score,
197
+ confidence: confidence ?? null,
198
+ probabilities,
199
+ };
200
+ }
@@ -0,0 +1,175 @@
1
+ import type {
2
+ DecisionModel,
3
+ DecisionAnswer,
4
+ DecisionResult,
5
+ DecisionRequest,
6
+ DecisionDialect,
7
+ DecisionCredential,
8
+ DecisionQuestion,
9
+ } from './types';
10
+ import type { DecisionFetch } from './transport';
11
+ import { DecisionError, isDecisionObject, readDecisionUsage } from './types';
12
+ import { validateDecisionQuestions } from './questions';
13
+ import { toWireQuestion, readAnswer } from './dialect';
14
+ import { createTransport } from './transport';
15
+
16
+ export const HTTP_PROVIDER_ID = 'http';
17
+
18
+ export interface HttpDecisionModelOptions {
19
+ providerId?: string;
20
+ apiKey?: DecisionCredential;
21
+ requiresAuth?: boolean;
22
+ /** Full URL, not a base path. */
23
+ endpoint: string;
24
+ model?: string;
25
+ dialect?: DecisionDialect;
26
+ requestKey?: string;
27
+ responseKey?: string;
28
+ timeoutMs?: number;
29
+ maxRetries?: number;
30
+ fetch?: DecisionFetch;
31
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
32
+ onAnswered?: (label: string, ms: number) => void;
33
+ }
34
+
35
+ export function parseEnvelope(
36
+ body: string,
37
+ providerId: string,
38
+ readOne: (
39
+ answer: unknown,
40
+ question?: DecisionQuestion
41
+ ) => DecisionAnswer | null,
42
+ responseKey?: string,
43
+ expected?: Record<string, DecisionQuestion>
44
+ ): DecisionResult {
45
+ let parsed: unknown;
46
+ try {
47
+ parsed = JSON.parse(body);
48
+ } catch {
49
+ throw new DecisionError('malformed_response', 'response was not JSON', {
50
+ provider: providerId,
51
+ });
52
+ }
53
+ if (!isDecisionObject(parsed)) {
54
+ throw new DecisionError(
55
+ 'malformed_response',
56
+ 'response was not an object',
57
+ {
58
+ provider: providerId,
59
+ }
60
+ );
61
+ }
62
+ const unwrapped =
63
+ responseKey != null && responseKey !== '' ? parsed[responseKey] : parsed;
64
+ if (
65
+ !isDecisionObject(unwrapped) ||
66
+ !isDecisionObject(unwrapped.answers) ||
67
+ Object.keys(unwrapped.answers).length === 0
68
+ ) {
69
+ throw new DecisionError(
70
+ 'malformed_response',
71
+ 'response carried no answers',
72
+ {
73
+ provider: providerId,
74
+ }
75
+ );
76
+ }
77
+
78
+ const answers: DecisionResult['answers'] = Object.create(null);
79
+ for (const [id, answer] of Object.entries(unwrapped.answers)) {
80
+ if (expected && !Object.hasOwn(expected, id)) {
81
+ throw new DecisionError(
82
+ 'malformed_response',
83
+ 'response carried an unknown answer',
84
+ {
85
+ provider: providerId,
86
+ }
87
+ );
88
+ }
89
+ const mapped = readOne(answer, expected?.[id]);
90
+ if (!mapped) {
91
+ throw new DecisionError(
92
+ 'malformed_response',
93
+ 'response carried an invalid answer',
94
+ {
95
+ provider: providerId,
96
+ }
97
+ );
98
+ }
99
+ answers[id] = mapped;
100
+ }
101
+
102
+ return {
103
+ model: typeof unwrapped.model === 'string' ? unwrapped.model : 'unknown',
104
+ answers,
105
+ usage: readDecisionUsage(unwrapped.usage),
106
+ };
107
+ }
108
+
109
+ /** Jev and Laya use the same System One HTTP dialect, but may report different confidence metrics. */
110
+ export function createHttpDecisionModel(
111
+ options: HttpDecisionModelOptions
112
+ ): DecisionModel {
113
+ const providerId = options.providerId ?? HTTP_PROVIDER_ID;
114
+ const send = createTransport({ ...options, providerId });
115
+ const model = options.model ?? '';
116
+ const dialect: DecisionDialect = options.dialect ?? 'port';
117
+ const { requestKey, responseKey } = options;
118
+
119
+ return {
120
+ id: providerId,
121
+ model,
122
+ async decide(request: DecisionRequest): Promise<DecisionResult> {
123
+ return send(
124
+ () => {
125
+ const entries = validateDecisionQuestions(
126
+ request.questions,
127
+ providerId
128
+ );
129
+ const questions: Record<
130
+ string,
131
+ ReturnType<typeof toWireQuestion>
132
+ > = Object.create(null);
133
+ const expected: Record<string, DecisionQuestion> =
134
+ Object.create(null);
135
+ for (const [id, question] of entries) {
136
+ questions[id] = toWireQuestion(question, dialect);
137
+ expected[id] = question;
138
+ }
139
+ const inner = { state: request.state, questions };
140
+ let payload: string;
141
+ try {
142
+ payload = JSON.stringify({
143
+ ...(model !== '' ? { model } : {}),
144
+ ...(requestKey != null && requestKey !== ''
145
+ ? { [requestKey]: inner }
146
+ : inner),
147
+ });
148
+ } catch {
149
+ throw new DecisionError(
150
+ 'bad_request',
151
+ 'invalid decision model request',
152
+ {
153
+ provider: providerId,
154
+ }
155
+ );
156
+ }
157
+ return {
158
+ payload,
159
+ parse: (body: string) =>
160
+ parseEnvelope(
161
+ body,
162
+ providerId,
163
+ (answer, question) => readAnswer(answer, dialect, question),
164
+ responseKey,
165
+ expected
166
+ ),
167
+ };
168
+ },
169
+ request.signal,
170
+ request.label ?? 'decide',
171
+ request.timeoutMs
172
+ );
173
+ },
174
+ };
175
+ }
@@ -0,0 +1,14 @@
1
+ export * from './types';
2
+ export * from './questions';
3
+ export * from './presets';
4
+ export {
5
+ createHttpDecisionModel,
6
+ parseEnvelope,
7
+ HTTP_PROVIDER_ID,
8
+ } from './http';
9
+ export { createStructuredChatDecisionModel } from './structuredChat';
10
+ export type { HttpDecisionModelOptions } from './http';
11
+ export type { StructuredChatDecisionModelOptions } from './structuredChat';
12
+ export { toWireQuestion, readAnswer } from './dialect';
13
+ export { createTransport } from './transport';
14
+ export type { DecisionFetch, Transport, TransportOptions } from './transport';
@@ -0,0 +1,111 @@
1
+ import type {
2
+ DecisionModel,
3
+ DecisionCredential,
4
+ DecisionProviderSettings,
5
+ } from './types';
6
+ import type { DecisionFetch } from './transport';
7
+ import { createHttpDecisionModel } from './http';
8
+
9
+ export const DEFAULT_API_KEY_ENV = 'DECISION_API_KEY';
10
+
11
+ /**
12
+ * Known HTTP hosts as settings, not new decision model implementations. Jev and Laya speak the
13
+ * same wire protocol, but their confidence measures and checkpoint calibration differ.
14
+ */
15
+ export const DECISION_PRESETS: Readonly<
16
+ Record<string, Readonly<DecisionProviderSettings>>
17
+ > = Object.freeze({
18
+ http: Object.freeze({
19
+ dialect: 'port',
20
+ requiresAuth: true,
21
+ apiKeyEnv: DEFAULT_API_KEY_ENV,
22
+ }),
23
+ typesafe: Object.freeze({
24
+ baseURL: 'https://api.typesafe.ai/v1/systemone',
25
+ model: 'jev-latest',
26
+ dialect: 'systemone',
27
+ requiresAuth: true,
28
+ apiKeyEnv: 'TYPESAFE_API_KEY',
29
+ }),
30
+ laya: Object.freeze({
31
+ /** Supply the server's full /v1/systemone URL; no model lets Laya route by language. */
32
+ dialect: 'systemone',
33
+ requiresAuth: false,
34
+ }),
35
+ clickhouse: Object.freeze({
36
+ /** ClickHouse's inference gateway; the token is what `dataplanectl chai auth token` prints, hourly. */
37
+ baseURL: 'https://inference-internal.clickhouse.cloud/v1/systemone',
38
+ model: 'jev-latest',
39
+ dialect: 'systemone',
40
+ requiresAuth: true,
41
+ apiKeyEnv: 'CHAI_AUTH_TOKEN',
42
+ }),
43
+ openrouter: Object.freeze({
44
+ baseURL: 'https://openrouter.ai/api/alpha/decisions',
45
+ model: '~typesafe/jev-latest',
46
+ dialect: 'systemone',
47
+ requiresAuth: true,
48
+ apiKeyEnv: 'OPENROUTER_KEY',
49
+ }),
50
+ cloudflare: Object.freeze({
51
+ /** No default URL: the account id is part of it. */
52
+ model: 'typesafe/jev',
53
+ dialect: 'systemone',
54
+ requestKey: 'input',
55
+ responseKey: 'result',
56
+ requiresAuth: true,
57
+ apiKeyEnv: 'CLOUDFLARE_API_TOKEN',
58
+ }),
59
+ });
60
+
61
+ export function decisionPreset(
62
+ name: string | undefined
63
+ ): DecisionProviderSettings | null {
64
+ if (name == null || name === '') {
65
+ return null;
66
+ }
67
+ return Object.hasOwn(DECISION_PRESETS, name)
68
+ ? { ...DECISION_PRESETS[name] }
69
+ : null;
70
+ }
71
+
72
+ /** Caller settings win except that required preset authentication cannot be weakened. */
73
+ export function mergeDecisionSettings(
74
+ preset: DecisionProviderSettings | null,
75
+ configured: DecisionProviderSettings | undefined
76
+ ): DecisionProviderSettings {
77
+ return {
78
+ ...(preset ?? {}),
79
+ ...(configured ?? {}),
80
+ ...(preset?.requiresAuth === true ? { requiresAuth: true } : {}),
81
+ };
82
+ }
83
+
84
+ export function decisionProviderNames(): string[] {
85
+ return Object.keys(DECISION_PRESETS);
86
+ }
87
+
88
+ export function createDecisionModel(
89
+ settings: DecisionProviderSettings,
90
+ apiKey?: DecisionCredential,
91
+ options?: {
92
+ fetch?: DecisionFetch;
93
+ providerId?: string;
94
+ onAnswered?: (label: string, ms: number) => void;
95
+ }
96
+ ): DecisionModel {
97
+ return createHttpDecisionModel({
98
+ providerId: options?.providerId,
99
+ apiKey,
100
+ requiresAuth: settings.requiresAuth,
101
+ endpoint: settings.baseURL ?? '',
102
+ model: settings.model,
103
+ dialect: settings.dialect,
104
+ requestKey: settings.requestKey,
105
+ responseKey: settings.responseKey,
106
+ timeoutMs: settings.timeoutMs,
107
+ maxRetries: settings.maxRetries,
108
+ fetch: options?.fetch,
109
+ onAnswered: options?.onAnswered,
110
+ });
111
+ }