@inflexa-ai/harness 0.17.0 → 0.19.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 (249) hide show
  1. package/dist/agents/report-builder.d.ts +2 -0
  2. package/dist/agents/report-builder.js +2 -0
  3. package/dist/agents/report-session-agent.d.ts +56 -0
  4. package/dist/agents/report-session-agent.js +72 -0
  5. package/dist/app/report-session-runtime.d.ts +72 -0
  6. package/dist/app/report-session-runtime.js +157 -0
  7. package/dist/app/spawn-report-session.d.ts +85 -0
  8. package/dist/app/spawn-report-session.js +83 -0
  9. package/dist/contracts/chat-events.d.ts +9 -0
  10. package/dist/contracts/index.d.ts +6 -2
  11. package/dist/contracts/index.js +3 -1
  12. package/dist/contracts/organ-system.d.ts +49 -0
  13. package/dist/contracts/organ-system.js +67 -0
  14. package/dist/contracts/report-blocks.d.ts +564 -0
  15. package/dist/contracts/report-blocks.js +117 -0
  16. package/dist/contracts/report-reference.d.ts +459 -0
  17. package/dist/contracts/report-reference.js +256 -0
  18. package/dist/contracts/schemas/chat-events.d.ts +2 -0
  19. package/dist/contracts/schemas/chat-events.js +2 -0
  20. package/dist/contracts/severity.d.ts +15 -0
  21. package/dist/contracts/severity.js +10 -0
  22. package/dist/contracts/target-dossier.d.ts +8675 -12972
  23. package/dist/contracts/target-dossier.js +534 -591
  24. package/dist/data/safety-panel-schema.d.ts +18 -5
  25. package/dist/data/safety-panel-schema.js +4 -4
  26. package/dist/execution/artifact-metadata.js +1 -0
  27. package/dist/execution/report-runner.js +1 -3
  28. package/dist/execution/run-synthesis.js +2 -0
  29. package/dist/index.d.ts +6 -0
  30. package/dist/index.js +19 -0
  31. package/dist/lib/async-utils.d.ts +16 -4
  32. package/dist/lib/async-utils.js +20 -15
  33. package/dist/lib/host-concurrency.js +1 -0
  34. package/dist/loop/__fixtures__/scripted-provider.js +23 -2
  35. package/dist/loop/run-agent.js +58 -10
  36. package/dist/loop/tool-detail.d.ts +40 -0
  37. package/dist/loop/tool-detail.js +35 -3
  38. package/dist/loop/types.d.ts +13 -0
  39. package/dist/memory/thread-history.d.ts +10 -0
  40. package/dist/memory/thread-history.js +12 -1
  41. package/dist/memory/thread-store.d.ts +4 -4
  42. package/dist/memory/thread-store.js +18 -9
  43. package/dist/prompts/report-session.d.ts +1 -0
  44. package/dist/prompts/report-session.js +89 -0
  45. package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
  46. package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
  47. package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
  48. package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
  49. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
  50. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
  51. package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
  52. package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
  53. package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
  54. package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
  55. package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
  56. package/dist/prompts/target-assessment/investigation/index.js +3 -0
  57. package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
  58. package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
  59. package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
  60. package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
  61. package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
  62. package/dist/providers/__fixtures__/session.d.ts +5 -4
  63. package/dist/providers/__fixtures__/session.js +4 -4
  64. package/dist/providers/ai-sdk.d.ts +77 -0
  65. package/dist/providers/ai-sdk.js +149 -19
  66. package/dist/providers/errors.d.ts +14 -0
  67. package/dist/providers/errors.js +103 -18
  68. package/dist/providers/types.d.ts +16 -0
  69. package/dist/providers/types.js +10 -1
  70. package/dist/report-model/block-walk.d.ts +56 -0
  71. package/dist/report-model/block-walk.js +89 -0
  72. package/dist/report-model/draft-finish.d.ts +60 -0
  73. package/dist/report-model/draft-finish.js +56 -0
  74. package/dist/report-model/draft-operations.d.ts +125 -0
  75. package/dist/report-model/draft-operations.js +341 -0
  76. package/dist/report-model/draft-read.d.ts +59 -0
  77. package/dist/report-model/draft-read.js +109 -0
  78. package/dist/report-model/draft.d.ts +51 -0
  79. package/dist/report-model/draft.js +32 -0
  80. package/dist/report-model/fixture-resolver.d.ts +8 -0
  81. package/dist/report-model/fixture-resolver.js +252 -0
  82. package/dist/report-model/pin-snapshot.d.ts +30 -0
  83. package/dist/report-model/pin-snapshot.js +40 -0
  84. package/dist/report-model/reference-resolver.d.ts +113 -0
  85. package/dist/report-model/reference-resolver.js +60 -0
  86. package/dist/report-model/structural-validation.d.ts +22 -0
  87. package/dist/report-model/structural-validation.js +68 -0
  88. package/dist/report-model/validate.d.ts +72 -0
  89. package/dist/report-model/validate.js +95 -0
  90. package/dist/report-render/chart.d.ts +33 -0
  91. package/dist/report-render/chart.js +538 -0
  92. package/dist/report-render/page.d.ts +127 -0
  93. package/dist/report-render/page.js +307 -0
  94. package/dist/report-render/references.d.ts +32 -0
  95. package/dist/report-render/references.js +44 -0
  96. package/dist/report-render/render.d.ts +25 -0
  97. package/dist/report-render/render.js +125 -0
  98. package/dist/report-render/script-json.d.ts +13 -0
  99. package/dist/report-render/script-json.js +15 -0
  100. package/dist/report-render/types.d.ts +39 -0
  101. package/dist/report-render/types.js +10 -0
  102. package/dist/report-render/value-bridge.d.ts +62 -0
  103. package/dist/report-render/value-bridge.js +82 -0
  104. package/dist/report-render/views/chart-view.d.ts +11 -0
  105. package/dist/report-render/views/chart-view.js +23 -0
  106. package/dist/report-render/views/page-view.d.ts +20 -0
  107. package/dist/report-render/views/page-view.js +49 -0
  108. package/dist/report-render/views/prose.d.ts +28 -0
  109. package/dist/report-render/views/prose.js +75 -0
  110. package/dist/report-render/views/references-view.d.ts +20 -0
  111. package/dist/report-render/views/references-view.js +64 -0
  112. package/dist/report-render/views/values.d.ts +42 -0
  113. package/dist/report-render/views/values.js +43 -0
  114. package/dist/runtime/assemble.d.ts +15 -0
  115. package/dist/runtime/assemble.js +28 -6
  116. package/dist/runtime/boot.d.ts +3 -3
  117. package/dist/runtime/boot.js +15 -4
  118. package/dist/runtime/dbos.d.ts +4 -4
  119. package/dist/state/artifacts.d.ts +16 -0
  120. package/dist/state/artifacts.js +18 -0
  121. package/dist/state/init.js +54 -0
  122. package/dist/state/purge-analysis.js +2 -0
  123. package/dist/state/report-session-state.d.ts +112 -0
  124. package/dist/state/report-session-state.js +150 -0
  125. package/dist/state/report-versions.d.ts +129 -0
  126. package/dist/state/report-versions.js +167 -0
  127. package/dist/state/snapshot-parse.d.ts +34 -0
  128. package/dist/state/snapshot-parse.js +82 -0
  129. package/dist/tasks/data-profile.js +1 -0
  130. package/dist/tools/ad-hoc-router.d.ts +7 -2
  131. package/dist/tools/ad-hoc-router.js +2 -1
  132. package/dist/tools/bio/chembl.js +1 -0
  133. package/dist/tools/bio/comptox.js +1 -0
  134. package/dist/tools/bio/drug-gene-interactions.js +1 -0
  135. package/dist/tools/bio/gene-disease-evidence.js +1 -0
  136. package/dist/tools/bio/gene-preclinical-profile.js +1 -0
  137. package/dist/tools/bio/keys.d.ts +3 -2
  138. package/dist/tools/bio/keys.js +3 -2
  139. package/dist/tools/bio/lookup-annotation.js +1 -0
  140. package/dist/tools/bio/opentargets.d.ts +3 -3
  141. package/dist/tools/bio/opentargets.js +4 -3
  142. package/dist/tools/bio/pubchem.js +1 -0
  143. package/dist/tools/bio/search-clinical-trials.js +1 -0
  144. package/dist/tools/bio/search-faers.js +1 -0
  145. package/dist/tools/bio/search-geo-datasets.js +1 -0
  146. package/dist/tools/bio/search-interactions.js +1 -0
  147. package/dist/tools/bio/target-safety.d.ts +4 -6
  148. package/dist/tools/bio/target-safety.js +7 -6
  149. package/dist/tools/define-tool.d.ts +19 -12
  150. package/dist/tools/define-tool.js +4 -2
  151. package/dist/tools/display/show-user.js +1 -0
  152. package/dist/tools/iterate-report.js +2 -0
  153. package/dist/tools/lib/chembl-client.d.ts +2 -0
  154. package/dist/tools/lib/chembl-client.js +1 -0
  155. package/dist/tools/lib/monarch-client.d.ts +42 -0
  156. package/dist/tools/lib/monarch-client.js +100 -0
  157. package/dist/tools/report/build-report.js +1 -0
  158. package/dist/tools/report/mint-preview-url.js +1 -0
  159. package/dist/tools/report/preview-snapshot.js +1 -0
  160. package/dist/tools/report/submit-report.js +1 -0
  161. package/dist/tools/report/version-fs.js +4 -0
  162. package/dist/tools/report-authoring/authoring-tools.d.ts +241 -0
  163. package/dist/tools/report-authoring/authoring-tools.js +460 -0
  164. package/dist/tools/report-session/index.d.ts +8 -0
  165. package/dist/tools/report-session/index.js +8 -0
  166. package/dist/tools/report-session/preview-report.d.ts +90 -0
  167. package/dist/tools/report-session/preview-report.js +245 -0
  168. package/dist/tools/research/context7-docs.js +2 -0
  169. package/dist/tools/research/generate-analogy-report.js +1 -0
  170. package/dist/tools/research/generate-plan.js +7 -2
  171. package/dist/tools/research/inspect-data-profile.js +4 -0
  172. package/dist/tools/research/literature-reviewer.js +1 -0
  173. package/dist/tools/research/search-arxiv.js +1 -0
  174. package/dist/tools/research/search-github-repos.js +1 -0
  175. package/dist/tools/research/search-semantic-scholar.js +1 -0
  176. package/dist/tools/sandbox/list-available-packages.js +23 -0
  177. package/dist/tools/sandbox/list-available-refs.js +36 -0
  178. package/dist/tools/sandbox/report-blocker.js +1 -0
  179. package/dist/tools/sandbox/skills.js +2 -0
  180. package/dist/tools/workspace/file-stat.js +1 -0
  181. package/dist/tools/workspace/show-file.js +2 -0
  182. package/dist/tools/workspace/show-plan.js +1 -0
  183. package/dist/workflows/execute-analysis.d.ts +5 -1
  184. package/dist/workflows/execute-analysis.js +5 -5
  185. package/dist/workflows/execute-target-assessment.d.ts +13 -2
  186. package/dist/workflows/execute-target-assessment.js +78 -16
  187. package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
  188. package/dist/workflows/target-assessment/assemblers/index.js +1 -1
  189. package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
  190. package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
  191. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
  192. package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
  193. package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
  194. package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
  195. package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
  196. package/dist/workflows/target-assessment/collectors/index.js +48 -5
  197. package/dist/workflows/target-assessment/coverage.d.ts +43 -27
  198. package/dist/workflows/target-assessment/coverage.js +33 -29
  199. package/dist/workflows/target-assessment/fanout/index.js +1 -0
  200. package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
  201. package/dist/workflows/target-assessment/investigation/index.js +419 -0
  202. package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
  203. package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
  204. package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
  205. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
  206. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
  207. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
  208. package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
  209. package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
  210. package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
  211. package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
  212. package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
  213. package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
  214. package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
  215. package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
  216. package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
  217. package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
  218. package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
  219. package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
  220. package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
  221. package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
  222. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
  223. package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
  224. package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
  225. package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
  226. package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
  227. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
  228. package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
  229. package/dist/workflows/target-assessment/metrics.d.ts +1 -6
  230. package/dist/workflows/target-assessment/metrics.js +1 -13
  231. package/dist/workflows/target-assessment/phase4-assemble.d.ts +2147 -807
  232. package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
  233. package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
  234. package/dist/workflows/target-assessment/phase5-persist.js +56 -173
  235. package/dist/workflows/target-assessment/schemas.d.ts +46 -1
  236. package/dist/workflows/target-assessment/schemas.js +19 -0
  237. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
  238. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
  239. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +26 -62
  240. package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
  241. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +27 -154
  242. package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
  243. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
  244. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
  245. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
  246. package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
  247. package/dist/workflows/target-assessment/synthesis/index.js +4 -1
  248. package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
  249. package/package.json +4 -1
@@ -32,6 +32,16 @@ export interface AiSdkProviderDeps {
32
32
  readonly logger?: Logger;
33
33
  /** Output-token ceiling per request. Defaults to {@link DEFAULT_MAX_OUTPUT_TOKENS}. */
34
34
  readonly maxOutputTokens?: number;
35
+ /** The retry limit of the harness envelope. Defaults to {@link RETRY_MAX_RETRIES}. */
36
+ readonly maxRetries?: number;
37
+ /**
38
+ * The bound for each silent interval of a request attempt, in milliseconds.
39
+ * The provider hands the value to the SDK as the bound for each gap between
40
+ * two content chunks of a stream. The value also names itself in the error of
41
+ * a stream that the bound stopped. When the field is absent, the provider
42
+ * hands the SDK no bound.
43
+ */
44
+ readonly requestTimeoutMs?: number;
35
45
  }
36
46
  /**
37
47
  * Connection + model configuration for one `ChatProvider`, discriminated by
@@ -52,6 +62,27 @@ export type AiSdkProviderConfig = {
52
62
  readonly capabilities?: Partial<ProviderCapabilities>;
53
63
  /** Output-token ceiling per request. Defaults to {@link DEFAULT_MAX_OUTPUT_TOKENS}. */
54
64
  readonly maxOutputTokens?: number;
65
+ /**
66
+ * The bound for each silent interval of a request attempt, in
67
+ * milliseconds. The value bounds the wait until the response starts, and
68
+ * each gap between two content chunks of a stream, per attempt. A chunk
69
+ * that carries no content, for example a keep-alive comment, does not
70
+ * reset the gap bound. When the field is absent, the provider installs no
71
+ * guard, and its behavior does not change.
72
+ *
73
+ * The harness lifts the transport floor of Bun: the guard adds
74
+ * `timeout: false` to the fetch init, thus the idle cut of 300 seconds
75
+ * does not apply. Under Node the floor of undici stays. Thus a Node
76
+ * embedder that sets a value above 300 seconds supplies a `fetch` whose
77
+ * dispatcher raises that limit.
78
+ */
79
+ readonly requestTimeoutMs?: number;
80
+ /**
81
+ * The retry limit of the harness envelope. The envelope uses this value
82
+ * in place of the default limit. When the field is absent, the envelope
83
+ * keeps the default of 10.
84
+ */
85
+ readonly maxRetries?: number;
55
86
  } | {
56
87
  readonly kind: "openai-compatible";
57
88
  readonly name: string;
@@ -62,6 +93,27 @@ export type AiSdkProviderConfig = {
62
93
  readonly capabilities?: Partial<ProviderCapabilities>;
63
94
  /** Output-token ceiling per request. Defaults to {@link DEFAULT_MAX_OUTPUT_TOKENS}. */
64
95
  readonly maxOutputTokens?: number;
96
+ /**
97
+ * The bound for each silent interval of a request attempt, in
98
+ * milliseconds. The value bounds the wait until the response starts, and
99
+ * each gap between two content chunks of a stream, per attempt. A chunk
100
+ * that carries no content, for example a keep-alive comment, does not
101
+ * reset the gap bound. When the field is absent, the provider installs no
102
+ * guard, and its behavior does not change.
103
+ *
104
+ * The harness lifts the transport floor of Bun: the guard adds
105
+ * `timeout: false` to the fetch init, thus the idle cut of 300 seconds
106
+ * does not apply. Under Node the floor of undici stays. Thus a Node
107
+ * embedder that sets a value above 300 seconds supplies a `fetch` whose
108
+ * dispatcher raises that limit.
109
+ */
110
+ readonly requestTimeoutMs?: number;
111
+ /**
112
+ * The retry limit of the harness envelope. The envelope uses this value
113
+ * in place of the default limit. When the field is absent, the envelope
114
+ * keeps the default of 10.
115
+ */
116
+ readonly maxRetries?: number;
65
117
  };
66
118
  export interface ConfiguredAiSdkProviderDeps {
67
119
  readonly config: AiSdkProviderConfig;
@@ -78,11 +130,36 @@ export interface ConfiguredAiSdkProviderDeps {
78
130
  */
79
131
  export declare function computeRetryDelayMs(error: unknown, exponentialBackoffDelay: number, random?: () => number): number;
80
132
  export declare function createAiSdkProvider(deps: AiSdkProviderDeps): ChatProvider;
133
+ /**
134
+ * Wrap a fetch so that the wait until the response starts does not exceed
135
+ * `requestTimeoutMs`. The wrapper arms one timer for each fetch call. The timer
136
+ * runs from the send until the fetch promise settles, and the headers of the
137
+ * response settle it. On expiry the wrapper aborts a dedicated controller with a
138
+ * `RequestTimeoutError` reason, and it composes that controller with the caller
139
+ * signal through `AbortSignal.any`. Thus the caller signal keeps its normal
140
+ * effect, and a caller abort stays a cancellation.
141
+ *
142
+ * The guard bounds the response start only. The SDK bounds each later gap of a
143
+ * streamed body with its own `chunkMs` setting, thus no timer of the wrapper
144
+ * survives the headers. Thus a stream with steady content does not trip the
145
+ * guard, whatever its total length. One fetch call is one attempt, because the
146
+ * SDK retries are off, thus the window is per attempt.
147
+ *
148
+ * When the field is absent, `createConfiguredAiSdkProvider` installs no wrapper,
149
+ * and the behavior stays byte-identical.
150
+ */
151
+ export declare function wrapFetchWithRequestTimeout(fetchImpl: FetchLike, requestTimeoutMs: number): FetchLike;
81
152
  /**
82
153
  * Realize an `AiSdkProviderConfig` into a `ChatProvider` bound to that config's
83
154
  * connection and model. The model is closed into the returned provider here (it
84
155
  * is never passed per `ChatRequest`), so N models require N provider
85
156
  * instances over one shared connection configuration — one instance serves the
86
157
  * single model it was built with.
158
+ *
159
+ * When the config sets `requestTimeoutMs`, the provider wraps the effective fetch
160
+ * (`config.fetch ?? globalThis.fetch`) with the response-start guard, it hands
161
+ * the value to the stream call as the SDK chunk bound, and it advertises the
162
+ * value on the returned instance. When the field is absent, it installs no
163
+ * wrapper, it hands the SDK no bound, and it advertises no value.
87
164
  */
88
165
  export declare function createConfiguredAiSdkProvider(deps: ConfiguredAiSdkProviderDeps): ChatProvider;
@@ -1,4 +1,4 @@
1
- import { generateText, streamText, wrapLanguageModel } from "ai";
1
+ import { generateText, streamText, wrapLanguageModel, } from "ai";
2
2
  import { createAnthropic } from "@ai-sdk/anthropic";
3
3
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
4
4
  import { APICallError } from "@ai-sdk/provider";
@@ -6,7 +6,7 @@ import { retryWithExponentialBackoff } from "@ai-sdk/provider-utils";
6
6
  import { ResultAsync, err, ok } from "neverthrow";
7
7
  import { scopeWorkloadId } from "../auth/types.js";
8
8
  import { createNoopLogger } from "../lib/console-logger.js";
9
- import { classifyProviderError, toProviderError } from "./errors.js";
9
+ import { classifyProviderError, RequestTimeoutError, toProviderError } from "./errors.js";
10
10
  /**
11
11
  * The harness-owned retry envelope. The AI SDK's built-in retry exposes only a
12
12
  * count: its 2s initial delay and ×2 factor are fixed, it caps neither the
@@ -189,15 +189,20 @@ function unwrapForClassification(e) {
189
189
  * that, once retries are exhausted, `toProviderError`'s status walk reaches the
190
190
  * true HTTP status instead of stopping at a synthetic wrapper. The attempt
191
191
  * counter is per-instance because each call builds its own retry.
192
+ *
193
+ * An aborted caller signal also stops the retry. A wall-clock guard of a caller
194
+ * aborts with a `TimeoutError` reason, which the taxonomy marks as a retryable
195
+ * timeout. Without this guard the envelope would loop an expiry that the caller
196
+ * itself declared.
192
197
  */
193
- function createRetry(signal, logger) {
198
+ function createRetry(signal, logger, maxRetries) {
194
199
  let retryCount = 0;
195
200
  return retryWithExponentialBackoff({
196
- maxRetries: RETRY_MAX_RETRIES,
201
+ maxRetries,
197
202
  initialDelayInMs: RETRY_INITIAL_DELAY_MS,
198
203
  backoffFactor: RETRY_BACKOFF_FACTOR,
199
204
  abortSignal: signal,
200
- shouldRetry: (e) => !(e instanceof BillingSeamFailure) && classifyProviderError(e).retryable,
205
+ shouldRetry: (e) => !(e instanceof BillingSeamFailure) && signal?.aborted !== true && classifyProviderError(e).retryable,
201
206
  getDelayInMs: ({ error, exponentialBackoffDelay }) => {
202
207
  const delayMs = computeRetryDelayMs(error, exponentialBackoffDelay);
203
208
  retryCount += 1;
@@ -329,15 +334,52 @@ function sanitizeMessages(messages) {
329
334
  }
330
335
  return out;
331
336
  }
337
+ /**
338
+ * Advance the SDK stream to the next text delta.
339
+ *
340
+ * The full stream carries one part for each event of a turn, and only a text
341
+ * part reaches the consumer of the harness. Thus the pull skips each other part.
342
+ * The `abort` part stops the pull, because the SDK closes the stream directly
343
+ * after it. Thus a timeout can never end the stream in silence with the text
344
+ * that arrived before it.
345
+ */
346
+ async function pullNextDelta(iterator) {
347
+ for (let next = await iterator.next(); next.done !== true; next = await iterator.next()) {
348
+ const part = next.value;
349
+ if (part.type === "text-delta")
350
+ return { kind: "delta", text: part.text };
351
+ if (part.type === "abort")
352
+ return { kind: "aborted", reason: part.reason };
353
+ }
354
+ return { kind: "end" };
355
+ }
356
+ /**
357
+ * The failure that an `abort` part of the SDK stream stands for.
358
+ *
359
+ * The part carries a message only, thus the caller signal is the one thing that
360
+ * separates a cancellation from a timeout. When the caller signal is aborted,
361
+ * the reason of that signal rides out, and the cancellation path applies
362
+ * unchanged. Otherwise the SDK chunk bound stopped the stream, and the sentinel
363
+ * names the configured value for the classifier.
364
+ */
365
+ function abortedStreamFailure(signal, reason, requestTimeoutMs) {
366
+ if (signal?.aborted === true)
367
+ return signal.reason;
368
+ if (requestTimeoutMs !== undefined)
369
+ return new RequestTimeoutError(requestTimeoutMs);
370
+ return new Error(reason ?? "The provider aborted the stream.");
371
+ }
332
372
  export function createAiSdkProvider(deps) {
333
373
  const capabilities = { toolCalling: deps.capabilities?.toolCalling ?? true };
334
374
  const logger = (deps.logger ?? createNoopLogger()).named("providers.ai-sdk");
335
375
  const maxOutputTokens = deps.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
376
+ const maxRetries = deps.maxRetries ?? RETRY_MAX_RETRIES;
377
+ const requestTimeoutMs = deps.requestTimeoutMs;
336
378
  const requestedModelId = requestedModelIdOf(deps.model);
337
379
  routeSdkWarningsTo(logger);
338
380
  function chat(req, session, signal) {
339
381
  const run = async () => {
340
- const retry = createRetry(signal, logger);
382
+ const retry = createRetry(signal, logger, maxRetries);
341
383
  const capture = captureServedModelId(deps.model);
342
384
  try {
343
385
  const result = await retry(async () => {
@@ -357,6 +399,10 @@ export function createAiSdkProvider(deps) {
357
399
  maxRetries: 0,
358
400
  headers,
359
401
  abortSignal: signal,
402
+ // No SDK timeout rides here. `chunkMs` bounds a stream only,
403
+ // thus it is inert on this call. The fetch guard is the full
404
+ // bound, because the headers of a non-streaming response arrive
405
+ // when the model completes.
360
406
  providerOptions: req.providerOptions,
361
407
  });
362
408
  });
@@ -371,7 +417,7 @@ export function createAiSdkProvider(deps) {
371
417
  return new ResultAsync(run());
372
418
  }
373
419
  async function* chatStream(req, session, signal) {
374
- const retry = createRetry(signal, logger);
420
+ const retry = createRetry(signal, logger, maxRetries);
375
421
  const capture = captureServedModelId(deps.model);
376
422
  try {
377
423
  // Retry covers only stream establishment: streamText defers wire errors
@@ -395,11 +441,22 @@ export function createAiSdkProvider(deps) {
395
441
  maxRetries: 0,
396
442
  headers,
397
443
  abortSignal: signal,
444
+ // The SDK bounds each gap between two content chunks of the body.
445
+ // Its timer arms at the first part of the provider stream, thus the
446
+ // wait until the response starts stays with the fetch guard. A
447
+ // chunk that carries no content, for example a keep-alive comment,
448
+ // produces no part, thus it does not reset this bound.
449
+ ...(requestTimeoutMs !== undefined ? { timeout: { chunkMs: requestTimeoutMs } } : {}),
398
450
  providerOptions: req.providerOptions,
399
451
  });
400
- const iterator = result.textStream[Symbol.asyncIterator]();
401
- const first = await iterator.next();
402
- if (first.done) {
452
+ const iterator = result.fullStream[Symbol.asyncIterator]();
453
+ const first = await pullNextDelta(iterator);
454
+ // The chunk bound can stop the stream before any content arrives. The
455
+ // throw carries the sentinel, thus the envelope gives the attempt a
456
+ // fresh window under the retry policy of a connection error.
457
+ if (first.kind === "aborted")
458
+ throw abortedStreamFailure(signal, first.reason, requestTimeoutMs);
459
+ if (first.kind === "end") {
403
460
  // A doStream promise rejection — the shape a real connection
404
461
  // failure takes — does NOT surface at this first pull: streamText
405
462
  // resolves it `{ done: true }` and defers the rejection to the
@@ -408,8 +465,8 @@ export function createAiSdkProvider(deps) {
408
465
  // retried; a genuine text-less turn resolves them, and the terminal
409
466
  // event is built from these already-resolved values without a
410
467
  // second await. (These promises only settle once the stream is
411
- // fully drained, so they can be awaited here only because the first
412
- // pull already reported the stream done.)
468
+ // fully drained, so they can be awaited here only because the pull
469
+ // already reported the stream done.)
413
470
  return {
414
471
  kind: "completed",
415
472
  messages: await result.responseMessages,
@@ -418,7 +475,7 @@ export function createAiSdkProvider(deps) {
418
475
  usage: await result.usage,
419
476
  };
420
477
  }
421
- return { kind: "streaming", result, iterator, firstDelta: first.value };
478
+ return { kind: "streaming", result, iterator, firstDelta: first.text };
422
479
  });
423
480
  if (opened.kind === "completed") {
424
481
  const response = responseFromMessages({
@@ -438,9 +495,14 @@ export function createAiSdkProvider(deps) {
438
495
  yield { type: "text-delta", text: firstDelta };
439
496
  // Drain the same iterator: past the first delta, errors propagate
440
497
  // unchanged (no retry, no re-emit).
441
- for (let next = await iterator.next(); !next.done; next = await iterator.next()) {
442
- text += next.value;
443
- yield { type: "text-delta", text: next.value };
498
+ for (let pull = await pullNextDelta(iterator); pull.kind !== "end"; pull = await pullNextDelta(iterator)) {
499
+ // A mid-stream abort ends the turn with an error, never with the
500
+ // partial text that arrived before it. The catch below builds the
501
+ // terminal `ProviderError` from the throwable.
502
+ if (pull.kind === "aborted")
503
+ throw abortedStreamFailure(signal, pull.reason, requestTimeoutMs);
504
+ text += pull.text;
505
+ yield { type: "text-delta", text: pull.text };
444
506
  }
445
507
  const response = responseFromMessages({
446
508
  messages: await result.responseMessages,
@@ -461,7 +523,63 @@ export function createAiSdkProvider(deps) {
461
523
  throw toProviderError(unwrapForClassification(e), workloadOf(session));
462
524
  }
463
525
  }
464
- return { capabilities, chat, chatStream };
526
+ // The provider advertises the guard limit on its own instance, thus a
527
+ // consumer that scales a deadline reads it from the provider in its deps.
528
+ return { capabilities, chat, chatStream, ...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}) };
529
+ }
530
+ /**
531
+ * Wrap a fetch so that the wait until the response starts does not exceed
532
+ * `requestTimeoutMs`. The wrapper arms one timer for each fetch call. The timer
533
+ * runs from the send until the fetch promise settles, and the headers of the
534
+ * response settle it. On expiry the wrapper aborts a dedicated controller with a
535
+ * `RequestTimeoutError` reason, and it composes that controller with the caller
536
+ * signal through `AbortSignal.any`. Thus the caller signal keeps its normal
537
+ * effect, and a caller abort stays a cancellation.
538
+ *
539
+ * The guard bounds the response start only. The SDK bounds each later gap of a
540
+ * streamed body with its own `chunkMs` setting, thus no timer of the wrapper
541
+ * survives the headers. Thus a stream with steady content does not trip the
542
+ * guard, whatever its total length. One fetch call is one attempt, because the
543
+ * SDK retries are off, thus the window is per attempt.
544
+ *
545
+ * When the field is absent, `createConfiguredAiSdkProvider` installs no wrapper,
546
+ * and the behavior stays byte-identical.
547
+ */
548
+ export function wrapFetchWithRequestTimeout(fetchImpl, requestTimeoutMs) {
549
+ return async (input, init) => {
550
+ const controller = new AbortController();
551
+ const callerSignal = init?.signal ?? undefined;
552
+ const timer = setTimeout(() => controller.abort(new RequestTimeoutError(requestTimeoutMs)), requestTimeoutMs);
553
+ // The harness runs on Node, where a timer has `unref`. Unref the guard
554
+ // timer, thus an armed guard does not keep a short-lived process alive.
555
+ // The DOM type of `setTimeout` declares no `unref`, thus guard the call.
556
+ timer.unref?.();
557
+ const clear = () => {
558
+ clearTimeout(timer);
559
+ // The harness runs on Node, where a caller signal can outlive one fetch
560
+ // attempt. Remove the abort listener when the attempt settles, thus a
561
+ // long-lived signal does not retain this closure.
562
+ callerSignal?.removeEventListener("abort", clear);
563
+ };
564
+ // Compose the guard controller with the caller signal, thus either one
565
+ // can abort the wire, and the abort reason names its own source. A caller
566
+ // abort also clears the guard timer at once. Thus an armed timer does not
567
+ // reach a pointless expiry after the caller cancels.
568
+ const signal = callerSignal ? AbortSignal.any([callerSignal, controller.signal]) : controller.signal;
569
+ callerSignal?.addEventListener("abort", clear, { once: true });
570
+ // `timeout: false` is an extension of the fetch init of Bun. Bun cuts a
571
+ // request that stays idle for 300 seconds, and this key lifts that cut. Node
572
+ // ignores an unknown member of the init, thus the key is inert there. A
573
+ // probe on Bun 1.3.14 proved both results. The `RequestInit` type declares
574
+ // no such member, thus the cast.
575
+ const forwarded = { ...init, signal, timeout: false };
576
+ try {
577
+ return await fetchImpl(input, forwarded);
578
+ }
579
+ finally {
580
+ clear();
581
+ }
582
+ };
465
583
  }
466
584
  /**
467
585
  * Realize an `AiSdkProviderConfig` into a `ChatProvider` bound to that config's
@@ -469,14 +587,22 @@ export function createAiSdkProvider(deps) {
469
587
  * is never passed per `ChatRequest`), so N models require N provider
470
588
  * instances over one shared connection configuration — one instance serves the
471
589
  * single model it was built with.
590
+ *
591
+ * When the config sets `requestTimeoutMs`, the provider wraps the effective fetch
592
+ * (`config.fetch ?? globalThis.fetch`) with the response-start guard, it hands
593
+ * the value to the stream call as the SDK chunk bound, and it advertises the
594
+ * value on the returned instance. When the field is absent, it installs no
595
+ * wrapper, it hands the SDK no bound, and it advertises no value.
472
596
  */
473
597
  export function createConfiguredAiSdkProvider(deps) {
474
598
  const config = deps.config;
599
+ const requestTimeoutMs = config.requestTimeoutMs;
600
+ const effectiveFetch = requestTimeoutMs === undefined ? config.fetch : wrapFetchWithRequestTimeout(config.fetch ?? globalThis.fetch, requestTimeoutMs);
475
601
  if (config.kind === "anthropic") {
476
602
  const provider = createAnthropic({
477
603
  baseURL: config.baseURL,
478
604
  apiKey: config.apiKey,
479
- fetch: config.fetch,
605
+ fetch: effectiveFetch,
480
606
  });
481
607
  return createAiSdkProvider({
482
608
  model: provider.chat(config.model),
@@ -484,13 +610,15 @@ export function createConfiguredAiSdkProvider(deps) {
484
610
  capabilities: config.capabilities,
485
611
  logger: deps.logger,
486
612
  ...(config.maxOutputTokens !== undefined ? { maxOutputTokens: config.maxOutputTokens } : {}),
613
+ ...(config.maxRetries !== undefined ? { maxRetries: config.maxRetries } : {}),
614
+ ...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}),
487
615
  });
488
616
  }
489
617
  const provider = createOpenAICompatible({
490
618
  name: config.name,
491
619
  baseURL: config.baseURL,
492
620
  apiKey: config.apiKey,
493
- fetch: config.fetch,
621
+ fetch: effectiveFetch,
494
622
  });
495
623
  return createAiSdkProvider({
496
624
  model: provider.chatModel(config.model),
@@ -498,5 +626,7 @@ export function createConfiguredAiSdkProvider(deps) {
498
626
  capabilities: config.capabilities,
499
627
  logger: deps.logger,
500
628
  ...(config.maxOutputTokens !== undefined ? { maxOutputTokens: config.maxOutputTokens } : {}),
629
+ ...(config.maxRetries !== undefined ? { maxRetries: config.maxRetries } : {}),
630
+ ...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}),
501
631
  });
502
632
  }
@@ -15,6 +15,20 @@
15
15
  * re-authenticating fixes it — which is a different remedy from every other
16
16
  * non-retryable 4xx, so it is a different kind.
17
17
  */
18
+ /**
19
+ * The sentinel that the request-timeout guard uses as an abort reason. The guard
20
+ * arms a timer for each fetch attempt. On expiry the guard aborts the attempt
21
+ * with this value. The value rides the abort chain, thus the classifier finds it
22
+ * and marks the attempt as a retryable provider timeout.
23
+ *
24
+ * The name is not `AbortError`, thus the abort-detection paths do not swallow a
25
+ * guard abort as a caller cancellation. The message names the configured value.
26
+ */
27
+ export declare class RequestTimeoutError extends Error {
28
+ /** The configured bound for one silent interval, in milliseconds. */
29
+ readonly requestTimeoutMs: number;
30
+ constructor(requestTimeoutMs: number);
31
+ }
18
32
  export type ProviderErrorKind = "auth" | "budget" | "tenant-blocked" | "provider";
19
33
  /**
20
34
  * The provider error value channel — a `DomainError`-conforming
@@ -16,6 +16,24 @@
16
16
  * non-retryable 4xx, so it is a different kind.
17
17
  */
18
18
  import { redactSecrets } from "../input-sanitization.js";
19
+ /**
20
+ * The sentinel that the request-timeout guard uses as an abort reason. The guard
21
+ * arms a timer for each fetch attempt. On expiry the guard aborts the attempt
22
+ * with this value. The value rides the abort chain, thus the classifier finds it
23
+ * and marks the attempt as a retryable provider timeout.
24
+ *
25
+ * The name is not `AbortError`, thus the abort-detection paths do not swallow a
26
+ * guard abort as a caller cancellation. The message names the configured value.
27
+ */
28
+ export class RequestTimeoutError extends Error {
29
+ /** The configured bound for one silent interval, in milliseconds. */
30
+ requestTimeoutMs;
31
+ constructor(requestTimeoutMs) {
32
+ super(`The provider sent no data within the request timeout of ${requestTimeoutMs} ms.`);
33
+ this.name = "RequestTimeoutError";
34
+ this.requestTimeoutMs = requestTimeoutMs;
35
+ }
36
+ }
19
37
  /**
20
38
  * Structural guard for an already-constructed `ProviderError` value. A
21
39
  * `ProviderError` is a plain object, not an `Error`, so it cannot be
@@ -46,6 +64,20 @@ export function isProviderError(value) {
46
64
  export function toProviderError(e, workload) {
47
65
  if (isProviderError(e))
48
66
  return e;
67
+ // A guard abort names the configured value in its message. Compose a message
68
+ // that also names the workload, thus a reader sees both. This runs before the
69
+ // status walk, because a guard abort carries no HTTP status. A timeout of the
70
+ // SDK carries no sentinel, thus it falls to the composed arm below, which
71
+ // forwards the message of the SDK.
72
+ const timeout = findRequestTimeout(e);
73
+ if (timeout !== undefined) {
74
+ return {
75
+ type: "provider",
76
+ retryable: true,
77
+ message: `The provider sent no data for ${workload} within the request timeout of ${timeout.requestTimeoutMs} ms.`,
78
+ cause: e,
79
+ };
80
+ }
49
81
  const { kind, retryable } = classifyProviderError(e);
50
82
  const detail = e instanceof Error ? e.message : String(e);
51
83
  if (kind === "auth") {
@@ -149,6 +181,22 @@ const CONNECTION_ERROR_CODES = new Set([
149
181
  "UND_ERR_SOCKET",
150
182
  ]);
151
183
  const CONNECTION_ERROR_PATTERN = /fetch failed|socket hang up|network error|connection (?:refused|reset|error|closed)|econnrefused|enotfound|etimedout|terminated/i;
184
+ /**
185
+ * Walk the `cause` chain up to {@link MAX_CAUSE_HOPS} links, and return the first
186
+ * result that `probe` gives. `probe` gives a value for a link that matches, or
187
+ * `undefined` for a link that does not. The walk stops at the first match, at
188
+ * the hop cap, or at the end of the chain.
189
+ */
190
+ function findInCauseChain(err, probe) {
191
+ let cursor = err;
192
+ for (let i = 0; i < MAX_CAUSE_HOPS && cursor; i++) {
193
+ const found = probe(cursor);
194
+ if (found !== undefined)
195
+ return found;
196
+ cursor = cursor.cause;
197
+ }
198
+ return undefined;
199
+ }
152
200
  /**
153
201
  * Walk the `cause` chain for the first numeric HTTP status. The single status
154
202
  * walker: `classifyProviderError` keys on what this returns, so a caller asking
@@ -156,16 +204,14 @@ const CONNECTION_ERROR_PATTERN = /fetch failed|socket hang up|network error|conn
156
204
  * than re-implement the traversal.
157
205
  */
158
206
  export function extractStatus(err) {
159
- let cursor = err;
160
- for (let i = 0; i < MAX_CAUSE_HOPS && cursor; i++) {
161
- const e = cursor;
207
+ return findInCauseChain(err, (link) => {
208
+ const e = link;
162
209
  if (typeof e.status === "number")
163
210
  return e.status;
164
211
  if (typeof e.statusCode === "number")
165
212
  return e.statusCode;
166
- cursor = e.cause;
167
- }
168
- return undefined;
213
+ return undefined;
214
+ });
169
215
  }
170
216
  /**
171
217
  * Walk the `cause` chain for the first captured provider response body.
@@ -186,23 +232,54 @@ function extractResponseBody(err) {
186
232
  }
187
233
  return undefined;
188
234
  }
235
+ /**
236
+ * Walk the `cause` chain for the request-timeout sentinel. The guard aborts a
237
+ * fetch with a `RequestTimeoutError`. The SDK carries the abort reason down the
238
+ * `cause` chain, thus the sentinel can sit one or more hops down.
239
+ */
240
+ function findRequestTimeout(err) {
241
+ return findInCauseChain(err, (link) => {
242
+ if (link instanceof RequestTimeoutError)
243
+ return link;
244
+ // A duplicate class identity across a realm boundary fails `instanceof`,
245
+ // thus match the name and the field as a fallback.
246
+ const e = link;
247
+ if (e.name === "RequestTimeoutError" && typeof e.requestTimeoutMs === "number")
248
+ return link;
249
+ return undefined;
250
+ });
251
+ }
252
+ /**
253
+ * The name of the abort reason that the `timeout` setting of the SDK raises. The
254
+ * SDK bounds each gap between two content chunks of a stream. On expiry it
255
+ * aborts the call with a bare `DOMException` that has this name, no cause, and
256
+ * no HTTP status. The message of that `DOMException` names the configured value.
257
+ */
258
+ const SDK_TIMEOUT_ERROR_NAME = "TimeoutError";
259
+ /**
260
+ * Walk the `cause` chain for the timeout that the SDK raises. The `DOMException`
261
+ * carries no field of its own, thus the name is the one signal. The SDK carries
262
+ * an abort reason down the `cause` chain, thus the reason can sit one or more
263
+ * hops down.
264
+ */
265
+ function findSdkTimeout(err) {
266
+ return findInCauseChain(err, (link) => {
267
+ const e = link;
268
+ return e.name === SDK_TIMEOUT_ERROR_NAME ? link : undefined;
269
+ });
270
+ }
189
271
  /** Does any link of the `cause` chain look like a transport-level failure? */
190
272
  function looksLikeConnectionError(err) {
191
- let cursor = err;
192
- for (let i = 0; i < MAX_CAUSE_HOPS && cursor; i++) {
193
- const e = cursor;
194
- if (typeof e.code === "string" && CONNECTION_ERROR_CODES.has(e.code)) {
273
+ return (findInCauseChain(err, (link) => {
274
+ const e = link;
275
+ if (typeof e.code === "string" && CONNECTION_ERROR_CODES.has(e.code))
195
276
  return true;
196
- }
197
- if (typeof e.name === "string" && /^APIConnection/.test(e.name)) {
277
+ if (typeof e.name === "string" && /^APIConnection/.test(e.name))
198
278
  return true;
199
- }
200
- if (typeof e.message === "string" && CONNECTION_ERROR_PATTERN.test(e.message)) {
279
+ if (typeof e.message === "string" && CONNECTION_ERROR_PATTERN.test(e.message))
201
280
  return true;
202
- }
203
- cursor = e.cause;
204
- }
205
- return false;
281
+ return undefined;
282
+ }) ?? false);
206
283
  }
207
284
  /**
208
285
  * Classify a provider failure by origin.
@@ -214,6 +291,14 @@ function looksLikeConnectionError(err) {
214
291
  * - Other `4xx` and parse / unknown errors → `provider`, not retryable.
215
292
  */
216
293
  export function classifyProviderError(e) {
294
+ // The request-timeout guard aborts an attempt with a typed sentinel, and the
295
+ // chunk bound of the SDK aborts one with a `TimeoutError`. Each classifies as
296
+ // a retryable provider timeout, thus the envelope retries it under the same
297
+ // policy as a connection error. This runs before the status and the
298
+ // connection paths, because a timeout carries no HTTP status, and it must not
299
+ // fall to a non-retryable path.
300
+ if (findRequestTimeout(e) !== undefined || findSdkTimeout(e) !== undefined)
301
+ return { kind: "provider", retryable: true };
217
302
  const status = extractStatus(e);
218
303
  // `auth` is its own kind rather than a plain non-retryable 4xx because the
219
304
  // remedy is categorically different: the request was well-formed and the
@@ -109,7 +109,23 @@ export interface AgentChat {
109
109
  }
110
110
  export interface ChatProvider extends AgentChat {
111
111
  chatStream(req: ChatRequest, session: AgentSession, signal?: AbortSignal): AsyncIterable<ChatStreamEvent>;
112
+ /**
113
+ * The request-timeout limit that this provider enforces, in milliseconds, or
114
+ * absent when it enforces none. The value bounds each silent interval of a
115
+ * request attempt: the wait until the response starts, and each gap between
116
+ * two content chunks of a stream. A consumer that scales a deadline reads this
117
+ * value from the provider instance, not from a harness constant.
118
+ */
119
+ readonly requestTimeoutMs?: number;
112
120
  }
121
+ /**
122
+ * The effective deadline in milliseconds for a provider-bound operation.
123
+ *
124
+ * An explicit value wins. Absent an explicit value, the deadline is the maximum
125
+ * of `floorMs` and the request-timeout limit that the provider advertises. Thus
126
+ * a configured request timeout can raise the floor. It cannot lower the floor.
127
+ */
128
+ export declare function effectiveDeadlineMs(provider: Pick<ChatProvider, "requestTimeoutMs">, floorMs: number, explicitMs?: number): number;
113
129
  export interface EmbeddingProvider {
114
130
  /**
115
131
  * Width of every vector `embed` returns. The write-side index paths create
@@ -6,4 +6,13 @@
6
6
  * attribution is resolved lazily at the provider wire boundary.
7
7
  */
8
8
  const _assertChatUsageFields = true;
9
- export {};
9
+ /**
10
+ * The effective deadline in milliseconds for a provider-bound operation.
11
+ *
12
+ * An explicit value wins. Absent an explicit value, the deadline is the maximum
13
+ * of `floorMs` and the request-timeout limit that the provider advertises. Thus
14
+ * a configured request timeout can raise the floor. It cannot lower the floor.
15
+ */
16
+ export function effectiveDeadlineMs(provider, floorMs, explicitMs) {
17
+ return explicitMs ?? Math.max(floorMs, provider.requestTimeoutMs ?? 0);
18
+ }