@qodo/sdk 0.13.4 → 2.0.0-next.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 (764) hide show
  1. package/LICENSE +31 -118
  2. package/README.md +133 -121
  3. package/bin/qodo-skills.mjs +13 -0
  4. package/bundled-skills/code-review/SKILL.md +41 -0
  5. package/bundled-skills/pr-summary/SKILL.md +59 -0
  6. package/bundled-skills/test-gen/SKILL.md +47 -0
  7. package/dist/auth/index.browser.d.ts +38 -0
  8. package/dist/auth/index.browser.d.ts.map +1 -0
  9. package/dist/auth/index.browser.js +62 -0
  10. package/dist/auth/index.browser.js.map +1 -0
  11. package/dist/auth/index.d.ts +44 -30
  12. package/dist/auth/index.d.ts.map +1 -1
  13. package/dist/auth/index.js +57 -110
  14. package/dist/auth/index.js.map +1 -1
  15. package/dist/client/AgentsClient.d.ts +33 -0
  16. package/dist/client/AgentsClient.d.ts.map +1 -0
  17. package/dist/client/AgentsClient.js +40 -0
  18. package/dist/client/AgentsClient.js.map +1 -0
  19. package/dist/client/ArtifactsClient.d.ts +43 -0
  20. package/dist/client/ArtifactsClient.d.ts.map +1 -0
  21. package/dist/client/ArtifactsClient.js +54 -0
  22. package/dist/client/ArtifactsClient.js.map +1 -0
  23. package/dist/client/BulletinClient.d.ts +45 -0
  24. package/dist/client/BulletinClient.d.ts.map +1 -0
  25. package/dist/client/BulletinClient.js +51 -0
  26. package/dist/client/BulletinClient.js.map +1 -0
  27. package/dist/client/InfoClient.d.ts +58 -0
  28. package/dist/client/InfoClient.d.ts.map +1 -0
  29. package/dist/client/InfoClient.js +135 -0
  30. package/dist/client/InfoClient.js.map +1 -0
  31. package/dist/client/PipelineClient.d.ts +162 -0
  32. package/dist/client/PipelineClient.d.ts.map +1 -0
  33. package/dist/client/PipelineClient.js +340 -0
  34. package/dist/client/PipelineClient.js.map +1 -0
  35. package/dist/client/QarRegistryClient.d.ts +396 -0
  36. package/dist/client/QarRegistryClient.d.ts.map +1 -0
  37. package/dist/client/QarRegistryClient.js +536 -0
  38. package/dist/client/QarRegistryClient.js.map +1 -0
  39. package/dist/client/QodoClient.d.ts +296 -0
  40. package/dist/client/QodoClient.d.ts.map +1 -0
  41. package/dist/client/QodoClient.js +803 -0
  42. package/dist/client/QodoClient.js.map +1 -0
  43. package/dist/client/SpecsClient.d.ts +121 -0
  44. package/dist/client/SpecsClient.d.ts.map +1 -0
  45. package/dist/client/SpecsClient.js +252 -0
  46. package/dist/client/SpecsClient.js.map +1 -0
  47. package/dist/client/StateClient.d.ts +35 -0
  48. package/dist/client/StateClient.d.ts.map +1 -0
  49. package/dist/client/StateClient.js +36 -0
  50. package/dist/client/StateClient.js.map +1 -0
  51. package/dist/client/TaskClient.d.ts +706 -0
  52. package/dist/client/TaskClient.d.ts.map +1 -0
  53. package/dist/client/TaskClient.js +2522 -0
  54. package/dist/client/TaskClient.js.map +1 -0
  55. package/dist/client/ToolClient.d.ts +278 -0
  56. package/dist/client/ToolClient.d.ts.map +1 -0
  57. package/dist/client/ToolClient.js +1115 -0
  58. package/dist/client/ToolClient.js.map +1 -0
  59. package/dist/client/a2a/index.d.ts +10 -0
  60. package/dist/client/a2a/index.d.ts.map +1 -0
  61. package/dist/client/a2a/index.js +9 -0
  62. package/dist/client/a2a/index.js.map +1 -0
  63. package/dist/client/a2a/registerA2A.d.ts +170 -0
  64. package/dist/client/a2a/registerA2A.d.ts.map +1 -0
  65. package/dist/client/a2a/registerA2A.js +85 -0
  66. package/dist/client/a2a/registerA2A.js.map +1 -0
  67. package/dist/client/connection.d.ts +800 -0
  68. package/dist/client/connection.d.ts.map +1 -0
  69. package/dist/client/connection.js +2020 -0
  70. package/dist/client/connection.js.map +1 -0
  71. package/dist/client/errors.d.ts +735 -0
  72. package/dist/client/errors.d.ts.map +1 -0
  73. package/dist/client/errors.js +921 -0
  74. package/dist/client/errors.js.map +1 -0
  75. package/dist/client/index.d.ts +26 -0
  76. package/dist/client/index.d.ts.map +1 -0
  77. package/dist/client/index.js +20 -0
  78. package/dist/client/index.js.map +1 -0
  79. package/dist/client/inlineGraph.d.ts +66 -0
  80. package/dist/client/inlineGraph.d.ts.map +1 -0
  81. package/dist/client/inlineGraph.js +500 -0
  82. package/dist/client/inlineGraph.js.map +1 -0
  83. package/dist/client/internal/thenable.d.ts +27 -0
  84. package/dist/client/internal/thenable.d.ts.map +1 -0
  85. package/dist/client/internal/thenable.js +31 -0
  86. package/dist/client/internal/thenable.js.map +1 -0
  87. package/dist/client/iterator.d.ts +32 -0
  88. package/dist/client/iterator.d.ts.map +1 -0
  89. package/dist/client/iterator.js +73 -0
  90. package/dist/client/iterator.js.map +1 -0
  91. package/dist/client/mcp/McpClientPool.browser.d.ts +76 -0
  92. package/dist/client/mcp/McpClientPool.browser.d.ts.map +1 -0
  93. package/dist/client/mcp/McpClientPool.browser.js +78 -0
  94. package/dist/client/mcp/McpClientPool.browser.js.map +1 -0
  95. package/dist/client/mcp/McpClientPool.d.ts +236 -0
  96. package/dist/client/mcp/McpClientPool.d.ts.map +1 -0
  97. package/dist/client/mcp/McpClientPool.js +585 -0
  98. package/dist/client/mcp/McpClientPool.js.map +1 -0
  99. package/dist/client/mcp/projection.d.ts +109 -0
  100. package/dist/client/mcp/projection.d.ts.map +1 -0
  101. package/dist/client/mcp/projection.js +446 -0
  102. package/dist/client/mcp/projection.js.map +1 -0
  103. package/dist/client/mcp/substituteEnv.browser.d.ts +18 -0
  104. package/dist/client/mcp/substituteEnv.browser.d.ts.map +1 -0
  105. package/dist/client/mcp/substituteEnv.browser.js +20 -0
  106. package/dist/client/mcp/substituteEnv.browser.js.map +1 -0
  107. package/dist/client/mcp/substituteEnv.d.ts +45 -0
  108. package/dist/client/mcp/substituteEnv.d.ts.map +1 -0
  109. package/dist/client/mcp/substituteEnv.js +63 -0
  110. package/dist/client/mcp/substituteEnv.js.map +1 -0
  111. package/dist/client/observers.d.ts +57 -0
  112. package/dist/client/observers.d.ts.map +1 -0
  113. package/dist/client/observers.js +203 -0
  114. package/dist/client/observers.js.map +1 -0
  115. package/dist/client/options.d.ts +269 -0
  116. package/dist/client/options.d.ts.map +1 -0
  117. package/dist/client/options.js +9 -0
  118. package/dist/client/options.js.map +1 -0
  119. package/dist/client/tools/_readlineApprovalPrompt.browser.d.ts +17 -0
  120. package/dist/client/tools/_readlineApprovalPrompt.browser.d.ts.map +1 -0
  121. package/dist/client/tools/_readlineApprovalPrompt.browser.js +24 -0
  122. package/dist/client/tools/_readlineApprovalPrompt.browser.js.map +1 -0
  123. package/dist/client/tools/_readlineApprovalPrompt.d.ts +33 -0
  124. package/dist/client/tools/_readlineApprovalPrompt.d.ts.map +1 -0
  125. package/dist/client/tools/_readlineApprovalPrompt.js +90 -0
  126. package/dist/client/tools/_readlineApprovalPrompt.js.map +1 -0
  127. package/dist/client/tools/approval.d.ts +280 -0
  128. package/dist/client/tools/approval.d.ts.map +1 -0
  129. package/dist/client/tools/approval.js +229 -0
  130. package/dist/client/tools/approval.js.map +1 -0
  131. package/dist/client/tools/bindFunctionToolDefs.d.ts +156 -0
  132. package/dist/client/tools/bindFunctionToolDefs.d.ts.map +1 -0
  133. package/dist/client/tools/bindFunctionToolDefs.js +360 -0
  134. package/dist/client/tools/bindFunctionToolDefs.js.map +1 -0
  135. package/dist/client/tools/defineFunctionTool.d.ts +277 -0
  136. package/dist/client/tools/defineFunctionTool.d.ts.map +1 -0
  137. package/dist/client/tools/defineFunctionTool.js +190 -0
  138. package/dist/client/tools/defineFunctionTool.js.map +1 -0
  139. package/dist/client/transport.browser.d.ts +20 -0
  140. package/dist/client/transport.browser.d.ts.map +1 -0
  141. package/dist/client/transport.browser.js +29 -0
  142. package/dist/client/transport.browser.js.map +1 -0
  143. package/dist/client/transport.d.ts +47 -0
  144. package/dist/client/transport.d.ts.map +1 -0
  145. package/dist/client/transport.js +102 -0
  146. package/dist/client/transport.js.map +1 -0
  147. package/dist/client/transport.shared.d.ts +30 -0
  148. package/dist/client/transport.shared.d.ts.map +1 -0
  149. package/dist/client/transport.shared.js +40 -0
  150. package/dist/client/transport.shared.js.map +1 -0
  151. package/dist/client/uuid.d.ts +32 -0
  152. package/dist/client/uuid.d.ts.map +1 -0
  153. package/dist/client/uuid.js +65 -0
  154. package/dist/client/uuid.js.map +1 -0
  155. package/dist/index.d.ts +88 -39
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +166 -43
  158. package/dist/index.js.map +1 -1
  159. package/dist/observability/attributes.d.ts +136 -0
  160. package/dist/observability/attributes.d.ts.map +1 -0
  161. package/dist/observability/attributes.js +184 -0
  162. package/dist/observability/attributes.js.map +1 -0
  163. package/dist/observability/index.d.ts +14 -0
  164. package/dist/observability/index.d.ts.map +1 -0
  165. package/dist/observability/index.js +11 -0
  166. package/dist/observability/index.js.map +1 -0
  167. package/dist/observability/resolveOTel.browser.d.ts +13 -0
  168. package/dist/observability/resolveOTel.browser.d.ts.map +1 -0
  169. package/dist/observability/resolveOTel.browser.js +14 -0
  170. package/dist/observability/resolveOTel.browser.js.map +1 -0
  171. package/dist/observability/resolveOTel.d.ts +28 -0
  172. package/dist/observability/resolveOTel.d.ts.map +1 -0
  173. package/dist/observability/resolveOTel.js +74 -0
  174. package/dist/observability/resolveOTel.js.map +1 -0
  175. package/dist/observability/spans.d.ts +198 -0
  176. package/dist/observability/spans.d.ts.map +1 -0
  177. package/dist/observability/spans.js +300 -0
  178. package/dist/observability/spans.js.map +1 -0
  179. package/dist/observability/traceContext.d.ts +51 -0
  180. package/dist/observability/traceContext.d.ts.map +1 -0
  181. package/dist/observability/traceContext.js +151 -0
  182. package/dist/observability/traceContext.js.map +1 -0
  183. package/dist/observability/transportMetrics.d.ts +58 -0
  184. package/dist/observability/transportMetrics.d.ts.map +1 -0
  185. package/dist/observability/transportMetrics.js +55 -0
  186. package/dist/observability/transportMetrics.js.map +1 -0
  187. package/dist/qar/agentSpec.d.ts +93 -0
  188. package/dist/qar/agentSpec.d.ts.map +1 -0
  189. package/dist/qar/agentSpec.js +184 -0
  190. package/dist/qar/agentSpec.js.map +1 -0
  191. package/dist/qar/clientEvents.d.ts +86 -0
  192. package/dist/qar/clientEvents.d.ts.map +1 -0
  193. package/dist/qar/clientEvents.js +36 -0
  194. package/dist/qar/clientEvents.js.map +1 -0
  195. package/dist/qar/envelopes.d.ts +227 -0
  196. package/dist/qar/envelopes.d.ts.map +1 -0
  197. package/dist/qar/envelopes.js +67 -0
  198. package/dist/qar/envelopes.js.map +1 -0
  199. package/dist/qar/generated/envelope.d.ts +332 -0
  200. package/dist/qar/generated/envelope.d.ts.map +1 -0
  201. package/dist/qar/generated/envelope.js +15 -0
  202. package/dist/qar/generated/envelope.js.map +1 -0
  203. package/dist/qar/generated/qar-info.d.ts +76 -0
  204. package/dist/qar/generated/qar-info.d.ts.map +1 -0
  205. package/dist/qar/generated/qar-info.js +15 -0
  206. package/dist/qar/generated/qar-info.js.map +1 -0
  207. package/dist/qar/generated/qodo-task-start-payload.d.ts +54 -0
  208. package/dist/qar/generated/qodo-task-start-payload.d.ts.map +1 -0
  209. package/dist/qar/generated/qodo-task-start-payload.js +15 -0
  210. package/dist/qar/generated/qodo-task-start-payload.js.map +1 -0
  211. package/dist/qar/ids.d.ts +19 -0
  212. package/dist/qar/ids.d.ts.map +1 -0
  213. package/dist/qar/ids.js +11 -0
  214. package/dist/qar/ids.js.map +1 -0
  215. package/dist/qar/index.d.ts +24 -0
  216. package/dist/qar/index.d.ts.map +1 -0
  217. package/dist/qar/index.js +16 -0
  218. package/dist/qar/index.js.map +1 -0
  219. package/dist/qar/info.d.ts +37 -0
  220. package/dist/qar/info.d.ts.map +1 -0
  221. package/dist/qar/info.js +17 -0
  222. package/dist/qar/info.js.map +1 -0
  223. package/dist/qar/json.d.ts +14 -0
  224. package/dist/qar/json.d.ts.map +1 -0
  225. package/dist/qar/json.js +9 -0
  226. package/dist/qar/json.js.map +1 -0
  227. package/dist/qar/payloads.d.ts +480 -0
  228. package/dist/qar/payloads.d.ts.map +1 -0
  229. package/dist/qar/payloads.js +37 -0
  230. package/dist/qar/payloads.js.map +1 -0
  231. package/dist/qar/specs.d.ts +604 -0
  232. package/dist/qar/specs.d.ts.map +1 -0
  233. package/dist/qar/specs.js +29 -0
  234. package/dist/qar/specs.js.map +1 -0
  235. package/dist/qar/taskEvents.d.ts +25 -0
  236. package/dist/qar/taskEvents.d.ts.map +1 -0
  237. package/dist/qar/taskEvents.js +22 -0
  238. package/dist/qar/taskEvents.js.map +1 -0
  239. package/dist/qar/trace.d.ts +12 -0
  240. package/dist/qar/trace.d.ts.map +1 -0
  241. package/dist/qar/trace.js +12 -0
  242. package/dist/qar/trace.js.map +1 -0
  243. package/dist/skills/activation.d.ts +177 -0
  244. package/dist/skills/activation.d.ts.map +1 -0
  245. package/dist/skills/activation.js +428 -0
  246. package/dist/skills/activation.js.map +1 -0
  247. package/dist/skills/cli/index.browser.d.ts +18 -0
  248. package/dist/skills/cli/index.browser.d.ts.map +1 -0
  249. package/dist/skills/cli/index.browser.js +27 -0
  250. package/dist/skills/cli/index.browser.js.map +1 -0
  251. package/dist/skills/cli/index.d.ts +37 -0
  252. package/dist/skills/cli/index.d.ts.map +1 -0
  253. package/dist/skills/cli/index.js +494 -0
  254. package/dist/skills/cli/index.js.map +1 -0
  255. package/dist/skills/events.d.ts +255 -0
  256. package/dist/skills/events.d.ts.map +1 -0
  257. package/dist/skills/events.js +224 -0
  258. package/dist/skills/events.js.map +1 -0
  259. package/dist/skills/index.d.ts +45 -0
  260. package/dist/skills/index.d.ts.map +1 -0
  261. package/dist/skills/index.js +34 -0
  262. package/dist/skills/index.js.map +1 -0
  263. package/dist/skills/inject.d.ts +57 -0
  264. package/dist/skills/inject.d.ts.map +1 -0
  265. package/dist/skills/inject.js +162 -0
  266. package/dist/skills/inject.js.map +1 -0
  267. package/dist/skills/lockfile.browser.d.ts +56 -0
  268. package/dist/skills/lockfile.browser.d.ts.map +1 -0
  269. package/dist/skills/lockfile.browser.js +55 -0
  270. package/dist/skills/lockfile.browser.js.map +1 -0
  271. package/dist/skills/lockfile.d.ts +137 -0
  272. package/dist/skills/lockfile.d.ts.map +1 -0
  273. package/dist/skills/lockfile.js +423 -0
  274. package/dist/skills/lockfile.js.map +1 -0
  275. package/dist/skills/manager.browser.d.ts +94 -0
  276. package/dist/skills/manager.browser.d.ts.map +1 -0
  277. package/dist/skills/manager.browser.js +159 -0
  278. package/dist/skills/manager.browser.js.map +1 -0
  279. package/dist/skills/manager.d.ts +362 -0
  280. package/dist/skills/manager.d.ts.map +1 -0
  281. package/dist/skills/manager.js +1386 -0
  282. package/dist/skills/manager.js.map +1 -0
  283. package/dist/skills/mcp/index.d.ts +15 -0
  284. package/dist/skills/mcp/index.d.ts.map +1 -0
  285. package/dist/skills/mcp/index.js +12 -0
  286. package/dist/skills/mcp/index.js.map +1 -0
  287. package/dist/skills/mcp/path.browser.d.ts +27 -0
  288. package/dist/skills/mcp/path.browser.d.ts.map +1 -0
  289. package/dist/skills/mcp/path.browser.js +33 -0
  290. package/dist/skills/mcp/path.browser.js.map +1 -0
  291. package/dist/skills/mcp/path.d.ts +57 -0
  292. package/dist/skills/mcp/path.d.ts.map +1 -0
  293. package/dist/skills/mcp/path.js +150 -0
  294. package/dist/skills/mcp/path.js.map +1 -0
  295. package/dist/skills/mcp/server.browser.d.ts +32 -0
  296. package/dist/skills/mcp/server.browser.d.ts.map +1 -0
  297. package/dist/skills/mcp/server.browser.js +53 -0
  298. package/dist/skills/mcp/server.browser.js.map +1 -0
  299. package/dist/skills/mcp/server.d.ts +144 -0
  300. package/dist/skills/mcp/server.d.ts.map +1 -0
  301. package/dist/skills/mcp/server.js +841 -0
  302. package/dist/skills/mcp/server.js.map +1 -0
  303. package/dist/skills/mcp/types.d.ts +72 -0
  304. package/dist/skills/mcp/types.d.ts.map +1 -0
  305. package/dist/skills/mcp/types.js +20 -0
  306. package/dist/skills/mcp/types.js.map +1 -0
  307. package/dist/skills/mcp/wireDefs.d.ts +58 -0
  308. package/dist/skills/mcp/wireDefs.d.ts.map +1 -0
  309. package/dist/skills/mcp/wireDefs.js +141 -0
  310. package/dist/skills/mcp/wireDefs.js.map +1 -0
  311. package/dist/skills/parser.d.ts +63 -0
  312. package/dist/skills/parser.d.ts.map +1 -0
  313. package/dist/skills/parser.js +755 -0
  314. package/dist/skills/parser.js.map +1 -0
  315. package/dist/skills/prefilter.d.ts +104 -0
  316. package/dist/skills/prefilter.d.ts.map +1 -0
  317. package/dist/skills/prefilter.js +398 -0
  318. package/dist/skills/prefilter.js.map +1 -0
  319. package/dist/skills/preprocess.d.ts +169 -0
  320. package/dist/skills/preprocess.d.ts.map +1 -0
  321. package/dist/skills/preprocess.js +535 -0
  322. package/dist/skills/preprocess.js.map +1 -0
  323. package/dist/skills/render.d.ts +83 -0
  324. package/dist/skills/render.d.ts.map +1 -0
  325. package/dist/skills/render.js +397 -0
  326. package/dist/skills/render.js.map +1 -0
  327. package/dist/skills/sources/index.browser.d.ts +29 -0
  328. package/dist/skills/sources/index.browser.d.ts.map +1 -0
  329. package/dist/skills/sources/index.browser.js +16 -0
  330. package/dist/skills/sources/index.browser.js.map +1 -0
  331. package/dist/skills/sources/index.d.ts +59 -0
  332. package/dist/skills/sources/index.d.ts.map +1 -0
  333. package/dist/skills/sources/index.js +471 -0
  334. package/dist/skills/sources/index.js.map +1 -0
  335. package/dist/skills/sources/walk.browser.d.ts +17 -0
  336. package/dist/skills/sources/walk.browser.d.ts.map +1 -0
  337. package/dist/skills/sources/walk.browser.js +19 -0
  338. package/dist/skills/sources/walk.browser.js.map +1 -0
  339. package/dist/skills/sources/walk.d.ts +68 -0
  340. package/dist/skills/sources/walk.d.ts.map +1 -0
  341. package/dist/skills/sources/walk.js +264 -0
  342. package/dist/skills/sources/walk.js.map +1 -0
  343. package/dist/skills/substitute.d.ts +87 -0
  344. package/dist/skills/substitute.d.ts.map +1 -0
  345. package/dist/skills/substitute.js +322 -0
  346. package/dist/skills/substitute.js.map +1 -0
  347. package/dist/skills/testing/SkillKit.browser.d.ts +62 -0
  348. package/dist/skills/testing/SkillKit.browser.d.ts.map +1 -0
  349. package/dist/skills/testing/SkillKit.browser.js +41 -0
  350. package/dist/skills/testing/SkillKit.browser.js.map +1 -0
  351. package/dist/skills/testing/SkillKit.d.ts +130 -0
  352. package/dist/skills/testing/SkillKit.d.ts.map +1 -0
  353. package/dist/skills/testing/SkillKit.js +316 -0
  354. package/dist/skills/testing/SkillKit.js.map +1 -0
  355. package/dist/skills/testing/index.d.ts +9 -0
  356. package/dist/skills/testing/index.d.ts.map +1 -0
  357. package/dist/skills/testing/index.js +8 -0
  358. package/dist/skills/testing/index.js.map +1 -0
  359. package/dist/skills/trust.d.ts +72 -0
  360. package/dist/skills/trust.d.ts.map +1 -0
  361. package/dist/skills/trust.js +183 -0
  362. package/dist/skills/trust.js.map +1 -0
  363. package/dist/skills/types.d.ts +627 -0
  364. package/dist/skills/types.d.ts.map +1 -0
  365. package/dist/skills/types.js +85 -0
  366. package/dist/skills/types.js.map +1 -0
  367. package/dist/skills/validator.d.ts +95 -0
  368. package/dist/skills/validator.d.ts.map +1 -0
  369. package/dist/skills/validator.js +486 -0
  370. package/dist/skills/validator.js.map +1 -0
  371. package/dist/tracing/PipelineTracer.d.ts +35 -22
  372. package/dist/tracing/PipelineTracer.d.ts.map +1 -1
  373. package/dist/tracing/PipelineTracer.js +106 -61
  374. package/dist/tracing/PipelineTracer.js.map +1 -1
  375. package/dist/tracing/SdkTracer.d.ts +63 -61
  376. package/dist/tracing/SdkTracer.d.ts.map +1 -1
  377. package/dist/tracing/SdkTracer.js +185 -177
  378. package/dist/tracing/SdkTracer.js.map +1 -1
  379. package/dist/tracing/index.d.ts +10 -1
  380. package/dist/tracing/index.d.ts.map +1 -1
  381. package/dist/tracing/index.js +9 -0
  382. package/dist/tracing/index.js.map +1 -1
  383. package/dist/tracing/types.d.ts +89 -16
  384. package/dist/tracing/types.d.ts.map +1 -1
  385. package/dist/tracing/types.js +17 -4
  386. package/dist/tracing/types.js.map +1 -1
  387. package/dist/types.d.ts +6 -1
  388. package/dist/types.d.ts.map +1 -1
  389. package/dist/types.js +4 -0
  390. package/dist/types.js.map +1 -1
  391. package/dist/version.d.ts.map +1 -1
  392. package/dist/version.js +10 -20
  393. package/dist/version.js.map +1 -1
  394. package/package.json +53 -39
  395. package/.claude/skills/qodo-agent/SKILL.md +0 -974
  396. package/.claude/skills/qodo-agent/assets/programmatic-agent.ts +0 -407
  397. package/.claude/skills/qodo-agent/references/builtin-tools.md +0 -342
  398. package/.claude/skills/qodo-agent/references/common-issues.md +0 -537
  399. package/bin/rg +0 -0
  400. package/dist/api/agent.d.ts +0 -105
  401. package/dist/api/agent.d.ts.map +0 -1
  402. package/dist/api/agent.js +0 -963
  403. package/dist/api/agent.js.map +0 -1
  404. package/dist/api/analytics.d.ts +0 -43
  405. package/dist/api/analytics.d.ts.map +0 -1
  406. package/dist/api/analytics.js +0 -163
  407. package/dist/api/analytics.js.map +0 -1
  408. package/dist/api/http.d.ts +0 -5
  409. package/dist/api/http.d.ts.map +0 -1
  410. package/dist/api/http.js +0 -62
  411. package/dist/api/http.js.map +0 -1
  412. package/dist/api/index.d.ts +0 -12
  413. package/dist/api/index.d.ts.map +0 -1
  414. package/dist/api/index.js +0 -17
  415. package/dist/api/index.js.map +0 -1
  416. package/dist/api/taskTracking.d.ts +0 -54
  417. package/dist/api/taskTracking.d.ts.map +0 -1
  418. package/dist/api/taskTracking.js +0 -208
  419. package/dist/api/taskTracking.js.map +0 -1
  420. package/dist/api/types.d.ts +0 -93
  421. package/dist/api/types.d.ts.map +0 -1
  422. package/dist/api/types.js +0 -2
  423. package/dist/api/types.js.map +0 -1
  424. package/dist/api/utils.d.ts +0 -8
  425. package/dist/api/utils.d.ts.map +0 -1
  426. package/dist/api/utils.js +0 -63
  427. package/dist/api/utils.js.map +0 -1
  428. package/dist/api/websocket.d.ts +0 -203
  429. package/dist/api/websocket.d.ts.map +0 -1
  430. package/dist/api/websocket.js +0 -1166
  431. package/dist/api/websocket.js.map +0 -1
  432. package/dist/bin/install-skill.d.ts +0 -14
  433. package/dist/bin/install-skill.d.ts.map +0 -1
  434. package/dist/bin/install-skill.js +0 -125
  435. package/dist/bin/install-skill.js.map +0 -1
  436. package/dist/bin/run-helpers.d.ts +0 -34
  437. package/dist/bin/run-helpers.d.ts.map +0 -1
  438. package/dist/bin/run-helpers.js +0 -186
  439. package/dist/bin/run-helpers.js.map +0 -1
  440. package/dist/bin/run.d.ts +0 -13
  441. package/dist/bin/run.d.ts.map +0 -1
  442. package/dist/bin/run.js +0 -57
  443. package/dist/bin/run.js.map +0 -1
  444. package/dist/clients/index.d.ts +0 -10
  445. package/dist/clients/index.d.ts.map +0 -1
  446. package/dist/clients/index.js +0 -8
  447. package/dist/clients/index.js.map +0 -1
  448. package/dist/clients/info/InfoClient.d.ts +0 -37
  449. package/dist/clients/info/InfoClient.d.ts.map +0 -1
  450. package/dist/clients/info/InfoClient.js +0 -69
  451. package/dist/clients/info/InfoClient.js.map +0 -1
  452. package/dist/clients/info/index.d.ts +0 -4
  453. package/dist/clients/info/index.d.ts.map +0 -1
  454. package/dist/clients/info/index.js +0 -2
  455. package/dist/clients/info/index.js.map +0 -1
  456. package/dist/clients/info/types.d.ts +0 -21
  457. package/dist/clients/info/types.d.ts.map +0 -1
  458. package/dist/clients/info/types.js +0 -2
  459. package/dist/clients/info/types.js.map +0 -1
  460. package/dist/clients/sessions/SessionsClient.d.ts +0 -34
  461. package/dist/clients/sessions/SessionsClient.d.ts.map +0 -1
  462. package/dist/clients/sessions/SessionsClient.js +0 -71
  463. package/dist/clients/sessions/SessionsClient.js.map +0 -1
  464. package/dist/clients/sessions/index.d.ts +0 -4
  465. package/dist/clients/sessions/index.d.ts.map +0 -1
  466. package/dist/clients/sessions/index.js +0 -2
  467. package/dist/clients/sessions/index.js.map +0 -1
  468. package/dist/clients/sessions/types.d.ts +0 -20
  469. package/dist/clients/sessions/types.d.ts.map +0 -1
  470. package/dist/clients/sessions/types.js +0 -2
  471. package/dist/clients/sessions/types.js.map +0 -1
  472. package/dist/clients/tools/ToolsClient.d.ts +0 -39
  473. package/dist/clients/tools/ToolsClient.d.ts.map +0 -1
  474. package/dist/clients/tools/ToolsClient.js +0 -95
  475. package/dist/clients/tools/ToolsClient.js.map +0 -1
  476. package/dist/clients/tools/index.d.ts +0 -4
  477. package/dist/clients/tools/index.d.ts.map +0 -1
  478. package/dist/clients/tools/index.js +0 -2
  479. package/dist/clients/tools/index.js.map +0 -1
  480. package/dist/clients/tools/types.d.ts +0 -14
  481. package/dist/clients/tools/types.d.ts.map +0 -1
  482. package/dist/clients/tools/types.js +0 -2
  483. package/dist/clients/tools/types.js.map +0 -1
  484. package/dist/config/ConfigManager.d.ts +0 -43
  485. package/dist/config/ConfigManager.d.ts.map +0 -1
  486. package/dist/config/ConfigManager.js +0 -472
  487. package/dist/config/ConfigManager.js.map +0 -1
  488. package/dist/config/index.d.ts +0 -6
  489. package/dist/config/index.d.ts.map +0 -1
  490. package/dist/config/index.js +0 -7
  491. package/dist/config/index.js.map +0 -1
  492. package/dist/config/urlConfig.d.ts +0 -15
  493. package/dist/config/urlConfig.d.ts.map +0 -1
  494. package/dist/config/urlConfig.js +0 -75
  495. package/dist/config/urlConfig.js.map +0 -1
  496. package/dist/constants/errors.d.ts +0 -2
  497. package/dist/constants/errors.d.ts.map +0 -1
  498. package/dist/constants/errors.js +0 -2
  499. package/dist/constants/errors.js.map +0 -1
  500. package/dist/constants/index.d.ts +0 -7
  501. package/dist/constants/index.d.ts.map +0 -1
  502. package/dist/constants/index.js +0 -11
  503. package/dist/constants/index.js.map +0 -1
  504. package/dist/constants/tools.d.ts +0 -4
  505. package/dist/constants/tools.d.ts.map +0 -1
  506. package/dist/constants/tools.js +0 -4
  507. package/dist/constants/tools.js.map +0 -1
  508. package/dist/constants/versions.d.ts +0 -2
  509. package/dist/constants/versions.d.ts.map +0 -1
  510. package/dist/constants/versions.js +0 -2
  511. package/dist/constants/versions.js.map +0 -1
  512. package/dist/context/buildUserContext.d.ts +0 -18
  513. package/dist/context/buildUserContext.d.ts.map +0 -1
  514. package/dist/context/buildUserContext.js +0 -34
  515. package/dist/context/buildUserContext.js.map +0 -1
  516. package/dist/context/index.d.ts +0 -9
  517. package/dist/context/index.d.ts.map +0 -1
  518. package/dist/context/index.js +0 -9
  519. package/dist/context/index.js.map +0 -1
  520. package/dist/context/messageManager.d.ts +0 -42
  521. package/dist/context/messageManager.d.ts.map +0 -1
  522. package/dist/context/messageManager.js +0 -322
  523. package/dist/context/messageManager.js.map +0 -1
  524. package/dist/context/taskFocus.d.ts +0 -2
  525. package/dist/context/taskFocus.d.ts.map +0 -1
  526. package/dist/context/taskFocus.js +0 -26
  527. package/dist/context/taskFocus.js.map +0 -1
  528. package/dist/context/userInput.d.ts +0 -3
  529. package/dist/context/userInput.d.ts.map +0 -1
  530. package/dist/context/userInput.js +0 -20
  531. package/dist/context/userInput.js.map +0 -1
  532. package/dist/mcp/MCPManager.d.ts +0 -109
  533. package/dist/mcp/MCPManager.d.ts.map +0 -1
  534. package/dist/mcp/MCPManager.js +0 -592
  535. package/dist/mcp/MCPManager.js.map +0 -1
  536. package/dist/mcp/approvedTools.d.ts +0 -4
  537. package/dist/mcp/approvedTools.d.ts.map +0 -1
  538. package/dist/mcp/approvedTools.js +0 -19
  539. package/dist/mcp/approvedTools.js.map +0 -1
  540. package/dist/mcp/baseServer.d.ts +0 -75
  541. package/dist/mcp/baseServer.d.ts.map +0 -1
  542. package/dist/mcp/baseServer.js +0 -107
  543. package/dist/mcp/baseServer.js.map +0 -1
  544. package/dist/mcp/builtinServers.d.ts +0 -15
  545. package/dist/mcp/builtinServers.d.ts.map +0 -1
  546. package/dist/mcp/builtinServers.js +0 -141
  547. package/dist/mcp/builtinServers.js.map +0 -1
  548. package/dist/mcp/dynamicBEServer.d.ts +0 -20
  549. package/dist/mcp/dynamicBEServer.d.ts.map +0 -1
  550. package/dist/mcp/dynamicBEServer.js +0 -52
  551. package/dist/mcp/dynamicBEServer.js.map +0 -1
  552. package/dist/mcp/index.d.ts +0 -18
  553. package/dist/mcp/index.d.ts.map +0 -1
  554. package/dist/mcp/index.js +0 -23
  555. package/dist/mcp/index.js.map +0 -1
  556. package/dist/mcp/mcpInitialization.d.ts +0 -2
  557. package/dist/mcp/mcpInitialization.d.ts.map +0 -1
  558. package/dist/mcp/mcpInitialization.js +0 -56
  559. package/dist/mcp/mcpInitialization.js.map +0 -1
  560. package/dist/mcp/servers/filesystem.d.ts +0 -44
  561. package/dist/mcp/servers/filesystem.d.ts.map +0 -1
  562. package/dist/mcp/servers/filesystem.js +0 -776
  563. package/dist/mcp/servers/filesystem.js.map +0 -1
  564. package/dist/mcp/servers/git.d.ts +0 -18
  565. package/dist/mcp/servers/git.d.ts.map +0 -1
  566. package/dist/mcp/servers/git.js +0 -441
  567. package/dist/mcp/servers/git.js.map +0 -1
  568. package/dist/mcp/servers/ripgrep.d.ts +0 -39
  569. package/dist/mcp/servers/ripgrep.d.ts.map +0 -1
  570. package/dist/mcp/servers/ripgrep.js +0 -550
  571. package/dist/mcp/servers/ripgrep.js.map +0 -1
  572. package/dist/mcp/servers/shell.d.ts +0 -20
  573. package/dist/mcp/servers/shell.d.ts.map +0 -1
  574. package/dist/mcp/servers/shell.js +0 -519
  575. package/dist/mcp/servers/shell.js.map +0 -1
  576. package/dist/mcp/serversRegistry.d.ts +0 -55
  577. package/dist/mcp/serversRegistry.d.ts.map +0 -1
  578. package/dist/mcp/serversRegistry.js +0 -416
  579. package/dist/mcp/serversRegistry.js.map +0 -1
  580. package/dist/mcp/toolProcessor.d.ts +0 -82
  581. package/dist/mcp/toolProcessor.d.ts.map +0 -1
  582. package/dist/mcp/toolProcessor.js +0 -392
  583. package/dist/mcp/toolProcessor.js.map +0 -1
  584. package/dist/mcp/types.d.ts +0 -29
  585. package/dist/mcp/types.d.ts.map +0 -1
  586. package/dist/mcp/types.js +0 -2
  587. package/dist/mcp/types.js.map +0 -1
  588. package/dist/messages/index.d.ts +0 -8
  589. package/dist/messages/index.d.ts.map +0 -1
  590. package/dist/messages/index.js +0 -7
  591. package/dist/messages/index.js.map +0 -1
  592. package/dist/messages/openai.d.ts +0 -26
  593. package/dist/messages/openai.d.ts.map +0 -1
  594. package/dist/messages/openai.js +0 -55
  595. package/dist/messages/openai.js.map +0 -1
  596. package/dist/messages/types.d.ts +0 -73
  597. package/dist/messages/types.d.ts.map +0 -1
  598. package/dist/messages/types.js +0 -78
  599. package/dist/messages/types.js.map +0 -1
  600. package/dist/parser/index.d.ts +0 -72
  601. package/dist/parser/index.d.ts.map +0 -1
  602. package/dist/parser/index.js +0 -967
  603. package/dist/parser/index.js.map +0 -1
  604. package/dist/parser/types.d.ts +0 -153
  605. package/dist/parser/types.d.ts.map +0 -1
  606. package/dist/parser/types.js +0 -6
  607. package/dist/parser/types.js.map +0 -1
  608. package/dist/parser/utils.d.ts +0 -18
  609. package/dist/parser/utils.d.ts.map +0 -1
  610. package/dist/parser/utils.js +0 -64
  611. package/dist/parser/utils.js.map +0 -1
  612. package/dist/sdk/QodoSDK.d.ts +0 -218
  613. package/dist/sdk/QodoSDK.d.ts.map +0 -1
  614. package/dist/sdk/QodoSDK.js +0 -1115
  615. package/dist/sdk/QodoSDK.js.map +0 -1
  616. package/dist/sdk/artifacts.d.ts +0 -156
  617. package/dist/sdk/artifacts.d.ts.map +0 -1
  618. package/dist/sdk/artifacts.js +0 -166
  619. package/dist/sdk/artifacts.js.map +0 -1
  620. package/dist/sdk/bootstrap.d.ts +0 -16
  621. package/dist/sdk/bootstrap.d.ts.map +0 -1
  622. package/dist/sdk/bootstrap.js +0 -28
  623. package/dist/sdk/bootstrap.js.map +0 -1
  624. package/dist/sdk/builders.d.ts +0 -54
  625. package/dist/sdk/builders.d.ts.map +0 -1
  626. package/dist/sdk/builders.js +0 -117
  627. package/dist/sdk/builders.js.map +0 -1
  628. package/dist/sdk/defaults.d.ts +0 -11
  629. package/dist/sdk/defaults.d.ts.map +0 -1
  630. package/dist/sdk/defaults.js +0 -39
  631. package/dist/sdk/defaults.js.map +0 -1
  632. package/dist/sdk/discovery.d.ts +0 -2
  633. package/dist/sdk/discovery.d.ts.map +0 -1
  634. package/dist/sdk/discovery.js +0 -25
  635. package/dist/sdk/discovery.js.map +0 -1
  636. package/dist/sdk/events.d.ts +0 -269
  637. package/dist/sdk/events.d.ts.map +0 -1
  638. package/dist/sdk/events.js +0 -69
  639. package/dist/sdk/events.js.map +0 -1
  640. package/dist/sdk/exit-expression.d.ts +0 -13
  641. package/dist/sdk/exit-expression.d.ts.map +0 -1
  642. package/dist/sdk/exit-expression.js +0 -35
  643. package/dist/sdk/exit-expression.js.map +0 -1
  644. package/dist/sdk/index.d.ts +0 -17
  645. package/dist/sdk/index.d.ts.map +0 -1
  646. package/dist/sdk/index.js +0 -17
  647. package/dist/sdk/index.js.map +0 -1
  648. package/dist/sdk/middleware.d.ts +0 -59
  649. package/dist/sdk/middleware.d.ts.map +0 -1
  650. package/dist/sdk/middleware.js +0 -69
  651. package/dist/sdk/middleware.js.map +0 -1
  652. package/dist/sdk/pipeline/PipelineBuilder.d.ts +0 -79
  653. package/dist/sdk/pipeline/PipelineBuilder.d.ts.map +0 -1
  654. package/dist/sdk/pipeline/PipelineBuilder.js +0 -129
  655. package/dist/sdk/pipeline/PipelineBuilder.js.map +0 -1
  656. package/dist/sdk/pipeline/PipelineRunner.d.ts +0 -28
  657. package/dist/sdk/pipeline/PipelineRunner.d.ts.map +0 -1
  658. package/dist/sdk/pipeline/PipelineRunner.js +0 -326
  659. package/dist/sdk/pipeline/PipelineRunner.js.map +0 -1
  660. package/dist/sdk/pipeline/compiler.d.ts +0 -24
  661. package/dist/sdk/pipeline/compiler.d.ts.map +0 -1
  662. package/dist/sdk/pipeline/compiler.js +0 -199
  663. package/dist/sdk/pipeline/compiler.js.map +0 -1
  664. package/dist/sdk/pipeline/declarative.d.ts +0 -34
  665. package/dist/sdk/pipeline/declarative.d.ts.map +0 -1
  666. package/dist/sdk/pipeline/declarative.js +0 -9
  667. package/dist/sdk/pipeline/declarative.js.map +0 -1
  668. package/dist/sdk/pipeline/index.d.ts +0 -20
  669. package/dist/sdk/pipeline/index.d.ts.map +0 -1
  670. package/dist/sdk/pipeline/index.js +0 -19
  671. package/dist/sdk/pipeline/index.js.map +0 -1
  672. package/dist/sdk/pipeline/types.d.ts +0 -93
  673. package/dist/sdk/pipeline/types.d.ts.map +0 -1
  674. package/dist/sdk/pipeline/types.js +0 -10
  675. package/dist/sdk/pipeline/types.js.map +0 -1
  676. package/dist/sdk/policies.d.ts +0 -163
  677. package/dist/sdk/policies.d.ts.map +0 -1
  678. package/dist/sdk/policies.js +0 -243
  679. package/dist/sdk/policies.js.map +0 -1
  680. package/dist/sdk/runner/AgentRunner.d.ts +0 -22
  681. package/dist/sdk/runner/AgentRunner.d.ts.map +0 -1
  682. package/dist/sdk/runner/AgentRunner.js +0 -222
  683. package/dist/sdk/runner/AgentRunner.js.map +0 -1
  684. package/dist/sdk/runner/finalize.d.ts +0 -56
  685. package/dist/sdk/runner/finalize.d.ts.map +0 -1
  686. package/dist/sdk/runner/finalize.js +0 -155
  687. package/dist/sdk/runner/finalize.js.map +0 -1
  688. package/dist/sdk/runner/formats.d.ts +0 -7
  689. package/dist/sdk/runner/formats.d.ts.map +0 -1
  690. package/dist/sdk/runner/formats.js +0 -76
  691. package/dist/sdk/runner/formats.js.map +0 -1
  692. package/dist/sdk/runner/index.d.ts +0 -9
  693. package/dist/sdk/runner/index.d.ts.map +0 -1
  694. package/dist/sdk/runner/index.js +0 -9
  695. package/dist/sdk/runner/index.js.map +0 -1
  696. package/dist/sdk/runner/progress.d.ts +0 -3
  697. package/dist/sdk/runner/progress.d.ts.map +0 -1
  698. package/dist/sdk/runner/progress.js +0 -16
  699. package/dist/sdk/runner/progress.js.map +0 -1
  700. package/dist/sdk/schemas.d.ts +0 -72
  701. package/dist/sdk/schemas.d.ts.map +0 -1
  702. package/dist/sdk/schemas.js +0 -282
  703. package/dist/sdk/schemas.js.map +0 -1
  704. package/dist/sdk/trigger-context.d.ts +0 -24
  705. package/dist/sdk/trigger-context.d.ts.map +0 -1
  706. package/dist/sdk/trigger-context.js +0 -136
  707. package/dist/sdk/trigger-context.js.map +0 -1
  708. package/dist/session/SessionContext.d.ts +0 -89
  709. package/dist/session/SessionContext.d.ts.map +0 -1
  710. package/dist/session/SessionContext.js +0 -410
  711. package/dist/session/SessionContext.js.map +0 -1
  712. package/dist/session/environment.d.ts +0 -52
  713. package/dist/session/environment.d.ts.map +0 -1
  714. package/dist/session/environment.js +0 -27
  715. package/dist/session/environment.js.map +0 -1
  716. package/dist/session/history.d.ts +0 -18
  717. package/dist/session/history.d.ts.map +0 -1
  718. package/dist/session/history.js +0 -68
  719. package/dist/session/history.js.map +0 -1
  720. package/dist/session/index.d.ts +0 -10
  721. package/dist/session/index.d.ts.map +0 -1
  722. package/dist/session/index.js +0 -9
  723. package/dist/session/index.js.map +0 -1
  724. package/dist/session/serverData.d.ts +0 -38
  725. package/dist/session/serverData.d.ts.map +0 -1
  726. package/dist/session/serverData.js +0 -261
  727. package/dist/session/serverData.js.map +0 -1
  728. package/dist/tracing/pipelineHelpers.d.ts +0 -29
  729. package/dist/tracing/pipelineHelpers.d.ts.map +0 -1
  730. package/dist/tracing/pipelineHelpers.js +0 -224
  731. package/dist/tracing/pipelineHelpers.js.map +0 -1
  732. package/dist/tracking/Tracker.d.ts +0 -55
  733. package/dist/tracking/Tracker.d.ts.map +0 -1
  734. package/dist/tracking/Tracker.js +0 -217
  735. package/dist/tracking/Tracker.js.map +0 -1
  736. package/dist/tracking/index.d.ts +0 -8
  737. package/dist/tracking/index.d.ts.map +0 -1
  738. package/dist/tracking/index.js +0 -8
  739. package/dist/tracking/index.js.map +0 -1
  740. package/dist/tracking/schemas.d.ts +0 -292
  741. package/dist/tracking/schemas.d.ts.map +0 -1
  742. package/dist/tracking/schemas.js +0 -91
  743. package/dist/tracking/schemas.js.map +0 -1
  744. package/dist/utils/extractSetFlags.d.ts +0 -6
  745. package/dist/utils/extractSetFlags.d.ts.map +0 -1
  746. package/dist/utils/extractSetFlags.js +0 -16
  747. package/dist/utils/extractSetFlags.js.map +0 -1
  748. package/dist/utils/formatTimeAgo.d.ts +0 -2
  749. package/dist/utils/formatTimeAgo.d.ts.map +0 -1
  750. package/dist/utils/formatTimeAgo.js +0 -20
  751. package/dist/utils/formatTimeAgo.js.map +0 -1
  752. package/dist/utils/index.d.ts +0 -12
  753. package/dist/utils/index.d.ts.map +0 -1
  754. package/dist/utils/index.js +0 -12
  755. package/dist/utils/index.js.map +0 -1
  756. package/dist/utils/machineId.d.ts +0 -14
  757. package/dist/utils/machineId.d.ts.map +0 -1
  758. package/dist/utils/machineId.js +0 -66
  759. package/dist/utils/machineId.js.map +0 -1
  760. package/dist/utils/pathUtils.d.ts +0 -22
  761. package/dist/utils/pathUtils.d.ts.map +0 -1
  762. package/dist/utils/pathUtils.js +0 -54
  763. package/dist/utils/pathUtils.js.map +0 -1
  764. package/scripts/download-ripgrep.js +0 -269
@@ -0,0 +1,2522 @@
1
+ /**
2
+ * `TaskClient` — task lifecycle surface, mirroring QAR's `task.*` envelope kinds.
3
+ */
4
+ import { collectInlineAgentSpecPreWireIssues, normalizeInlineAgentSpec } from '../qar/agentSpec.js';
5
+ import { TaskSubscription } from './connection.js';
6
+ import { AsyncQueue } from './iterator.js';
7
+ import { longestPathFrom, validateInlineGraphSpec, } from './inlineGraph.js';
8
+ import { classForServerErrorCode, QodoAdmissionStalledError, QodoAdmissionTimeoutError, QodoAgentSpecRejectedError, QodoCancelAbortedError, QodoIdempotencyKeyValidationError, QodoInlineAgentValidationError, QodoInlineGraphValidationError, QodoMcpUnavailableError, QodoStreamAbortedError, QodoUnknownMcpError, QodoUnknownMcpToolError, QodoUnknownServerError, RequiredGraphSpecError, } from './errors.js';
9
+ import { McpClientPool } from './mcp/McpClientPool.js';
10
+ import { projectMcpTools } from './mcp/projection.js';
11
+ import { qodoSkillsFunctionToolDefs } from '../skills/mcp/server.js';
12
+ import { asMessageId, asTaskId, uuidv7 } from './uuid.js';
13
+ import { QodoSkillError, SkillAmbiguousPinError, SkillNotFoundError, SkillsBudgetExceededError, } from '../skills/manager.js';
14
+ import { injectIntoAgentSpec, injectIntoGraphSpec, } from '../skills/inject.js';
15
+ import { activationFailureMessage, lookupFromSnapshot, renderActiveSkillsBlock, resolveAllActivations, } from '../skills/activation.js';
16
+ export class TaskClient {
17
+ resolveConnection;
18
+ spanRecorder;
19
+ registry;
20
+ resolveSpecs;
21
+ metrics;
22
+ skills;
23
+ bindFunctionToolDefs;
24
+ /**
25
+ * @param resolveConnection Returns the live `Connection` or throws if the
26
+ * client isn't connected yet.
27
+ * @param spanRecorder Recorder for `qar.client.task.*` spans. No-op
28
+ * when OTel isn't configured.
29
+ * @param registry Local agent + MCP registries. `tasks.start` reads
30
+ * this to resolve `RegisteredAgentRef` → wire
31
+ * `agent_id`.
32
+ * @param resolveSpecs Lazily-resolved `SpecsClient` used by the
33
+ * opt-in `preflight: true` path on
34
+ * `startWithGraph()`. Lazy because `QodoClient`
35
+ * constructs `tasks` before `specs`; defer
36
+ * dereference until the caller actually asks
37
+ * for pre-flight validation.
38
+ */
39
+ constructor(resolveConnection, spanRecorder, registry, resolveSpecs,
40
+ /**
41
+ * Transport-metric store passed through to each constructed
42
+ * `TaskSubscription` so the connection's reconnect/replay path can
43
+ * increment `replay_envelopes_received_total` /
44
+ * `replay_anchor_missing_total` against absorbed envelopes. Optional
45
+ * for back-compat with consumers wiring `TaskClient` directly in
46
+ * tests; production wires from `QodoClient`.
47
+ */
48
+ metrics,
49
+ /**
50
+ * Skills foundation manager. When provided, the `startWithAgent` and
51
+ * `startWithGraph` paths await the manager's `discover()` (idempotent)
52
+ * and append the rendered slim index to the spec's `instructions`
53
+ * before the wire write. Undefined when the consumer didn't pass
54
+ * `ClientOptions.skills` — those paths stay verbatim.
55
+ */
56
+ skills,
57
+ /**
58
+ * `defineFunctionTool` auto-bind hook. Called at every
59
+ * `startWithAgent` / `startWithGraph` entry point so handlers
60
+ * attached to `FunctionToolDef` entries (via the helper's
61
+ * symbol-keyed handler bag) get installed on the `ToolClient`
62
+ * before the wire `task.start` lands. Optional — when undefined
63
+ * (e.g. `TaskClient` constructed in isolation in tests), the bind
64
+ * step is skipped and consumers fall back to manual
65
+ * `client.tools.onRequest(...)` registration.
66
+ */
67
+ bindFunctionToolDefs) {
68
+ this.resolveConnection = resolveConnection;
69
+ this.spanRecorder = spanRecorder;
70
+ this.registry = registry;
71
+ this.resolveSpecs = resolveSpecs;
72
+ this.metrics = metrics;
73
+ this.skills = skills;
74
+ this.bindFunctionToolDefs = bindFunctionToolDefs;
75
+ }
76
+ /**
77
+ * Send `task.start` and yield the resulting `TaskEvent` stream until `task.done`.
78
+ *
79
+ * The iterator is the canonical streaming primitive — consumers `for await` and
80
+ * `switch (event.kind)` over the QAR discriminator. Breaking the iterator
81
+ * before `task.done` arrives sends a best-effort `task.cancel` so the runtime
82
+ * can free its resources promptly.
83
+ *
84
+ * `payload.agent_id` accepts either a wire-shape `string` or a
85
+ * `RegisteredAgentRef` from `client.qar.registerAgent`. Refs are resolved
86
+ * to `ref.name` for the outgoing envelope; the local agent body never
87
+ * crosses the wire.
88
+ */
89
+ start(payload, opts) {
90
+ // Validate `idempotencyKey` synchronously before the wire write.
91
+ // Throws `QodoIdempotencyKeyValidationError` on bad inputs — fail-fast
92
+ // DX rather than waiting for QAR to round-trip an error envelope.
93
+ if (opts?.idempotencyKey !== undefined) {
94
+ validateIdempotencyKey(opts.idempotencyKey);
95
+ }
96
+ const resolvedAgentId = this.registry.resolveAgentId(payload.agent_id);
97
+ const wirePayload = {
98
+ ...payload,
99
+ agent_id: resolvedAgentId,
100
+ // Forward the caller-supplied key under the wire snake_case name.
101
+ // Strip the field entirely when absent — server treats
102
+ // `idempotency_key: null` and field-absent identically per the
103
+ // Pydantic `default=None` semantic, but the SDK keeps the wire
104
+ // shape minimal so a fresh-eyes diff of the encoded JSON shows
105
+ // exactly the consumer's intent.
106
+ ...(opts?.idempotencyKey !== undefined ? { idempotency_key: opts.idempotencyKey } : {}),
107
+ };
108
+ // Pre-allocate the root message id at the public-API boundary so the
109
+ // derived `task_id` (= `task.start.message_id`) is available
110
+ // synchronously on the returned iterable. The iterable's `taskId`
111
+ // property lets consumers call `client.tasks.cancel(stream.taskId)`
112
+ // without waiting for the first inbound envelope.
113
+ const rootMessageId = asMessageId(uuidv7());
114
+ const taskId = asTaskId(rootMessageId);
115
+ // Deferred Promises for the `task.started` admission ack —
116
+ // `sessionId` for the server-derived session UUID and
117
+ // `admittedTaskId` for the canonical task_id. Both settle
118
+ // atomically on the same admission outcome.
119
+ const sessionIdDeferred = createDeferred();
120
+ const admittedTaskIdDeferred = createDeferred();
121
+ const admissionResultDeferred = createDeferred();
122
+ const ackDeferreds = {
123
+ sessionIdDeferred,
124
+ admittedTaskIdDeferred,
125
+ admissionResultDeferred,
126
+ };
127
+ // Closure used by both the single-shot path and the admission
128
+ // retry wrapper. Each call builds a fresh subscription against the
129
+ // supplied rootMessageId — on retry the wrapper passes a fresh id so
130
+ // the duplicate-message-id guard in `Connection.sendEnvelope` stays
131
+ // happy.
132
+ const dispatch = (rmid) => this.subscribeAndSend(() => ({ kind: 'task.start', payload: wirePayload, rootMessageId: rmid }), asTaskId(rmid), opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
133
+ sessionId,
134
+ messageId,
135
+ agentId: resolvedAgentId,
136
+ ...(wirePayload.skill !== undefined && wirePayload.skill !== null
137
+ ? { skillName: wirePayload.skill }
138
+ : {}),
139
+ }).lifecycle, rmid, ackDeferreds);
140
+ // Only the deterministic-key path ever sees `admission_in_progress`
141
+ // (the omitted-key path mints a fresh uuidv7 server-side per call —
142
+ // no collisions possible). Wrap with the retry layer when the key is
143
+ // present; otherwise stay on the existing fast path so the DX
144
+ // default doesn't pay the extra iterator-wrapping cost. Either way
145
+ // the first attempt is dispatched eagerly so the wire `task.start`
146
+ // is in flight before the public method returns (preserves the
147
+ // `cancel(stream.taskId)` race-free contract).
148
+ const firstSub = dispatch(rootMessageId);
149
+ const iter = opts?.idempotencyKey !== undefined
150
+ ? TaskClient.wrapWithAdmissionRetry(firstSub, dispatch, sessionIdDeferred, admittedTaskIdDeferred, admissionResultDeferred)
151
+ : // Wrap server errors on the by-id path too. It doesn't have its
152
+ // own spec rejection but the cancel/resume/HITL families apply
153
+ // to any running task.
154
+ TaskClient.wrapServerErrorsIterator(firstSub);
155
+ return attachTaskId(iter, taskId, admittedTaskIdDeferred.promise, sessionIdDeferred.promise, admissionResultDeferred.promise);
156
+ }
157
+ /**
158
+ * Send `task.start { agent, input }` and yield the resulting `TaskEvent`
159
+ * stream until `task.done` — inline-AgentSpec dispatch.
160
+ *
161
+ * The spec is validated client-side (the SDK-enforceable rule subset,
162
+ * see `collectInlineAgentSpecPreWireIssues`) BEFORE the envelope hits
163
+ * the wire. A `QodoInlineAgentValidationError` is thrown when any
164
+ * forbidden-key or missing-required-field rule fails — the error's
165
+ * `issues` array carries every violation with a `rule_id` +
166
+ * JSON-Pointer `path` so consumers can route both client-side and
167
+ * server-side rejections through the same branch logic.
168
+ *
169
+ * Opt into `{ preflight: true }` to additionally call
170
+ * `client.specs.validate(agent)` (HTTP) before opening the task — that
171
+ * catches the rules the SDK can't enforce locally (catalog tool
172
+ * registration, skill version pinning, spawnable `agent_id`
173
+ * resolution). Default `false`.
174
+ *
175
+ * Iterator + early-termination semantics match `start()` — breaking the
176
+ * iterator before `task.done` arrives sends best-effort `task.cancel`.
177
+ * Inline-agent spans carry `qar.agent.kind = "inline"` and
178
+ * `qar.agent_id = "inline:<message_id>"`.
179
+ *
180
+ * Server-side rejection: when QAR's `_handle_task_start` emits an
181
+ * `error { code: 'agent_spec_rejected' }` envelope, the iterator
182
+ * rejects with `QodoAgentSpecRejectedError` instead of yielding the
183
+ * envelope as a `kind: 'error'` event — consumers don't need to
184
+ * defensively narrow the error case out of `TaskEvent.kind`.
185
+ */
186
+ startWithAgent(payload, opts) {
187
+ // Cheap fail-fast `idempotencyKey` check runs BEFORE the agent-spec
188
+ // validator so a typo'd key surfaces ahead of the
189
+ // potentially-multi-issue spec validation pass.
190
+ if (opts?.idempotencyKey !== undefined) {
191
+ validateIdempotencyKey(opts.idempotencyKey);
192
+ }
193
+ // Pre-wire validation — synchronous, no IO, fail fast before either
194
+ // the optional pre-flight HTTP call or the WS write. Collect-then-throw
195
+ // so the consumer sees every rule in one go (better for IDE / Studio
196
+ // surfaces showing multiple diagnostics simultaneously).
197
+ const preIssues = collectInlineAgentSpecPreWireIssues(payload.agent);
198
+ if (preIssues.length > 0) {
199
+ throw new QodoInlineAgentValidationError(preIssues);
200
+ }
201
+ // `defineFunctionTool` auto-bind. Any `FunctionToolDef` in the
202
+ // spec's `tools[]` that carries the SDK-local handler symbol gets
203
+ // registered with the per-client `FunctionToolRouter` so the
204
+ // inbound `tool.request` dispatch knows where to route calls. The
205
+ // wire envelope itself stays unchanged — the handler lives behind a
206
+ // Symbol that `JSON.stringify` skips.
207
+ if (this.bindFunctionToolDefs !== undefined && payload.agent.tools != null) {
208
+ this.bindFunctionToolDefs(payload.agent.tools);
209
+ }
210
+ // Pre-allocate root message id at the public-API boundary so `taskId`
211
+ // is synchronously available on the returned iterable.
212
+ const rootMessageId = asMessageId(uuidv7());
213
+ const taskId = asTaskId(rootMessageId);
214
+ const sessionIdDeferred = createDeferred();
215
+ const admittedTaskIdDeferred = createDeferred();
216
+ const admissionResultDeferred = createDeferred();
217
+ const ackDeferreds = {
218
+ sessionIdDeferred,
219
+ admittedTaskIdDeferred,
220
+ admissionResultDeferred,
221
+ };
222
+ // Slim-index injection. When the manager has finished discovery,
223
+ // inject synchronously — the common case once `client.connect()` has
224
+ // resolved (connect awaits manager.discover()). When the snapshot
225
+ // isn't ready yet (consumer called startWithAgent before connect
226
+ // resolved), fall through to the eager-promise dispatch path so we
227
+ // await discover() before the wire write.
228
+ const skillsPinned = opts?.skills;
229
+ const needsSkillsAwait = this.skills !== undefined && this.skills.currentSnapshot === null;
230
+ // MCP projection. When the consumer set `mcpTools` (or the sibling
231
+ // `mcpToolOverrides`), the wire-boundary projection must await the
232
+ // pool's catalog-settle window — so route through the
233
+ // eager-promise dispatch path. The sync path stays for the
234
+ // no-discovery / no-overrides DX-default case.
235
+ const needsMcpProjection = payload.agent.mcpTools !== undefined ||
236
+ payload.agent.mcpToolOverrides !== undefined;
237
+ let agentForWire = normalizeInlineAgentSpec(payload.agent);
238
+ // Skills inject runs sync ONLY when neither async-path trigger is
239
+ // armed. `needsMcpProjection` forces the eager-async dispatch
240
+ // route, and that route re-runs `applyAgentSkillsInjection` inside
241
+ // `runAgentPreflightAndDispatch`. The slim-index / active-skills
242
+ // injector is not idempotent (it appends content), so double-
243
+ // applying duplicates the instruction text. Gate the sync branch
244
+ // on BOTH `!needsSkillsAwait` AND `!needsMcpProjection`.
245
+ if (this.skills !== undefined && !needsSkillsAwait && !needsMcpProjection) {
246
+ agentForWire = this.applyAgentSkillsInjection(agentForWire, skillsPinned, opts?.skillsCurrentFile);
247
+ }
248
+ // Auto-include the qodo-skills.* tool surface in the sync-dispatch
249
+ // path so the `<available_skills>` preamble's instruction ("Use the
250
+ // qodo-skills.get_skill tool to ...") is actionable. The async
251
+ // projection path runs the same merge inside
252
+ // `projectMcpToolsForWire` so callers that DO hit that branch get
253
+ // the same wire surface. Sync auto-include doesn't need a settle
254
+ // wait — the 3 tool defs are constants.
255
+ if (this.skills !== undefined && !needsMcpProjection) {
256
+ const original = agentForWire.tools ?? [];
257
+ const merged = mergeQodoSkillsTools(original);
258
+ if (merged !== original) {
259
+ agentForWire = { ...agentForWire, tools: merged };
260
+ }
261
+ }
262
+ const wirePayload = {
263
+ agent: agentForWire,
264
+ input: payload.input ?? {},
265
+ ...(payload.skill !== undefined && payload.skill !== null
266
+ ? { skill: payload.skill }
267
+ : {}),
268
+ // Forward the caller-supplied key under the wire snake_case name.
269
+ // Strip when absent so the encoded JSON matches the DX-default
270
+ // path one-to-one (server treats absent and `null` identically per
271
+ // Pydantic, but the SDK keeps the wire minimal for readability).
272
+ ...(opts?.idempotencyKey !== undefined
273
+ ? { idempotency_key: opts.idempotencyKey }
274
+ : {}),
275
+ };
276
+ // Pre-flight branch: validate FIRST via `POST /v1/specs/validate`,
277
+ // and only then call `subscribeAndSend()` to write `task.start` on
278
+ // the wire. Without this ordering the wire write fires before the
279
+ // HTTP validate resolves, and a rule-rejected spec would still
280
+ // start a server task. The non-preflight path stays synchronous so
281
+ // the common case avoids the async-generator microtask boundary.
282
+ if (opts?.preflight === true || needsSkillsAwait || needsMcpProjection) {
283
+ // Kick off preflight + (on success) `subscribeAndSend` eagerly so
284
+ // the wire `task.start` is in flight by the time the public method
285
+ // returns. Without this, a consumer that reads `stream.taskId` and
286
+ // calls `tasks.cancel(stream.taskId)` before iterating would race
287
+ // the cancel envelope ahead of the (still-deferred) `task.start`.
288
+ // The iterable then awaits the eager promise and yields from the
289
+ // resulting subscription.
290
+ const subPromise = this.runAgentPreflightAndDispatch(wirePayload, opts ?? {}, rootMessageId, taskId, ackDeferreds);
291
+ // Swallow the unhandled-rejection until the consumer iterates and
292
+ // we re-throw via `yieldFromPromisedSub`. Without this attach,
293
+ // Node treats the rejection as "unhandled" during the microtask
294
+ // window between dispatch failure and consumer iteration.
295
+ subPromise.catch((err) => {
296
+ // Surface the preflight failure to anyone awaiting
297
+ // `stream.sessionId` / `stream.admittedTaskId` so the promises
298
+ // don't hang.
299
+ const wrapped = err instanceof Error ? err : new Error(String(err));
300
+ if (!sessionIdDeferred.settled())
301
+ sessionIdDeferred.reject(wrapped);
302
+ if (!admittedTaskIdDeferred.settled())
303
+ admittedTaskIdDeferred.reject(wrapped);
304
+ if (!admissionResultDeferred.settled())
305
+ admissionResultDeferred.reject(wrapped);
306
+ });
307
+ return attachTaskId(yieldFromPromisedSub(subPromise, true), taskId, admittedTaskIdDeferred.promise, sessionIdDeferred.promise, admissionResultDeferred.promise);
308
+ }
309
+ // Closure used by both single-shot dispatch and the admission retry
310
+ // wrapper (when `idempotencyKey` is set). Each call builds a fresh
311
+ // subscription against the supplied rootMessageId.
312
+ const dispatch = (rmid) => this.subscribeAndSend(() => ({
313
+ kind: 'task.start',
314
+ payload: wirePayload,
315
+ rootMessageId: rmid,
316
+ }), asTaskId(rmid), opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
317
+ sessionId,
318
+ messageId,
319
+ agentId: `inline:${messageId}`,
320
+ agentKind: 'inline',
321
+ ...(wirePayload.skill !== undefined && wirePayload.skill !== null
322
+ ? { skillName: wirePayload.skill }
323
+ : {}),
324
+ }).lifecycle, rmid, ackDeferreds);
325
+ const firstSub = dispatch(rootMessageId);
326
+ const iter = opts?.idempotencyKey !== undefined
327
+ ? TaskClient.wrapWithAdmissionRetry(firstSub, dispatch, sessionIdDeferred, admittedTaskIdDeferred, admissionResultDeferred)
328
+ : TaskClient.wrapServerErrorsIterator(firstSub);
329
+ return attachTaskId(iter, taskId, admittedTaskIdDeferred.promise, sessionIdDeferred.promise, admissionResultDeferred.promise);
330
+ }
331
+ /**
332
+ * Eagerly run `client.specs.validate(agent)` and, on success, dispatch
333
+ * the `task.start` envelope via `subscribeAndSend`. Returns a
334
+ * `Promise<TaskSubscription>` that resolves once the wire write has
335
+ * happened — kicked off in the public method body so the dispatch is
336
+ * in flight by the time the consumer can read `stream.taskId`.
337
+ *
338
+ * `preflight: true` still means "validate before the wire write" per
339
+ * the documented contract; we just no longer wait for iteration to
340
+ * start the validate.
341
+ *
342
+ * Server-side rule failures throw `QodoAgentSpecRejectedError` so
343
+ * consumers route SDK + server rejections through one branch.
344
+ */
345
+ async runAgentPreflightAndDispatch(wirePayload, opts, rootMessageId, taskId,
346
+ /**
347
+ * Deferred pair for the `task.started` admission ack (sessionId +
348
+ * admittedTaskId). Forwarded into the eventual `subscribeAndSend` so
349
+ * the inbound ack resolves both caller-facing Promises on the
350
+ * `TaskStartIterable`.
351
+ */
352
+ ackDeferreds) {
353
+ // Await skills discovery (idempotent) and inject before (a) the
354
+ // optional /v1/specs/validate call, so the validator sees the full
355
+ // agent spec the consumer will dispatch, and (b) the wire write.
356
+ let finalPayload = wirePayload;
357
+ if (this.skills !== undefined) {
358
+ await this.skills.discover();
359
+ const injectedAgent = this.applyAgentSkillsInjection(wirePayload.agent, opts.skills, opts.skillsCurrentFile);
360
+ finalPayload = { ...wirePayload, agent: injectedAgent };
361
+ }
362
+ // MCP projection. Runs AFTER skills inject (skills mutate
363
+ // instructions, never tools) and BEFORE the optional /v1/specs/validate
364
+ // call so the validator sees the post-projection `tools[]` the wire
365
+ // envelope will carry. Throws synchronously on unknown / unreachable
366
+ // MCPs — the caller already wrapped this call in a `.catch` that
367
+ // settles `sessionId` / `admittedTaskId` deferreds so the thrown
368
+ // error reaches every consumer await.
369
+ const projectedAgent = await this.projectMcpToolsForWire(finalPayload.agent);
370
+ if (projectedAgent !== finalPayload.agent) {
371
+ finalPayload = { ...finalPayload, agent: projectedAgent };
372
+ }
373
+ if (opts.preflight !== true) {
374
+ // Skills-only async path — no preflight HTTP call. Dispatch now.
375
+ return this.subscribeAndSend(() => ({
376
+ kind: 'task.start',
377
+ payload: finalPayload,
378
+ rootMessageId,
379
+ }), taskId, opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
380
+ sessionId,
381
+ messageId,
382
+ agentId: `inline:${messageId}`,
383
+ agentKind: 'inline',
384
+ ...(finalPayload.skill !== undefined && finalPayload.skill !== null
385
+ ? { skillName: finalPayload.skill }
386
+ : {}),
387
+ }).lifecycle, rootMessageId, ackDeferreds);
388
+ }
389
+ const resolver = this.resolveSpecs;
390
+ if (resolver === undefined) {
391
+ throw new Error("tasks.startWithAgent({ preflight: true }) requires the client's SpecsClient — " +
392
+ 'this TaskClient was constructed without a specs resolver');
393
+ }
394
+ const result = await resolver().validate(finalPayload.agent);
395
+ if (!result.valid) {
396
+ // `SpecValidateResult` guarantees `errors.length > 0` on the
397
+ // `valid: false` arm, but a misbehaving server could violate it.
398
+ // Synthesize a single `preflight_rejected` issue so
399
+ // `QodoAgentSpecRejectedError`'s constructor invariant always
400
+ // holds and we surface a readable message rather than crashing
401
+ // the iterator with a TypeError.
402
+ const issues = result.errors.length > 0
403
+ ? result.errors.map((e) => ({
404
+ rule_id: e.rule_id,
405
+ path: e.path,
406
+ message: e.message,
407
+ }))
408
+ : [
409
+ {
410
+ rule_id: 'preflight_rejected',
411
+ path: '',
412
+ message: 'Pre-flight validation rejected the spec without reporting rule_ids',
413
+ },
414
+ ];
415
+ const head = issues[0];
416
+ throw new QodoAgentSpecRejectedError(issues, `Inline AgentSpec rejected by pre-flight (POST /v1/specs/validate): ` +
417
+ `${head.rule_id} @ ${head.path || '<root>'}: ${head.message}`);
418
+ }
419
+ // Pre-flight passed — write `task.start` on the wire now.
420
+ return this.subscribeAndSend(() => ({
421
+ kind: 'task.start',
422
+ payload: finalPayload,
423
+ rootMessageId,
424
+ }), taskId, opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
425
+ sessionId,
426
+ messageId,
427
+ agentId: `inline:${messageId}`,
428
+ agentKind: 'inline',
429
+ ...(finalPayload.skill !== undefined && finalPayload.skill !== null
430
+ ? { skillName: finalPayload.skill }
431
+ : {}),
432
+ }).lifecycle, rootMessageId, ackDeferreds);
433
+ }
434
+ /**
435
+ * Wrap the underlying subscription so a server-side `error { code: ... }`
436
+ * envelope arriving on the iterator surfaces as a rejected typed error
437
+ * instead of a yielded `kind: 'error'` event. Covers the documented
438
+ * typed-error catalog:
439
+ *
440
+ * - `agent_spec_rejected` → `QodoAgentSpecRejectedError`
441
+ * - the cancel / resume / HITL / graph code family → the matching
442
+ * `QodoServerError` subclass
443
+ *
444
+ * Other error envelopes (`replay_anchor_missing`,
445
+ * `envelope_parse_error`, future codes the SDK hasn't catalogued)
446
+ * pass through unchanged as `kind: 'error'` iterator events so
447
+ * existing consumer code reading them keeps working. Callers that
448
+ * want to convert an arbitrary error envelope into a typed throw
449
+ * can wrap it themselves with `QodoUnknownServerError` (exposed via
450
+ * the public surface) or `errorFromServerErrorEnvelope` (internal,
451
+ * stable across the typed catalog).
452
+ *
453
+ * Non-error envelopes pass through unchanged.
454
+ *
455
+ * **Why this isn't an `async function*`.** Native async generators don't
456
+ * propagate `.return()` to the inner iterator until the generator's
457
+ * body has entered (one `.next()` call has fired). The SDK's
458
+ * iterator-early-termination contract is that breaking the iterator
459
+ * BEFORE the first envelope arrives still fires a best-effort
460
+ * `task.cancel` — that relies on `.return()` reaching `sub.return()`.
461
+ * The explicit iterator object below forwards every protocol call
462
+ * straight to the underlying subscription's iterator so the
463
+ * cancel-on-break path stays intact.
464
+ */
465
+ static wrapServerErrorsIterator(inner) {
466
+ return {
467
+ [Symbol.asyncIterator]() {
468
+ const innerIter = inner[Symbol.asyncIterator]();
469
+ return {
470
+ async next() {
471
+ const result = await innerIter.next();
472
+ if (!result.done && result.value.kind === 'error') {
473
+ const code = result.value.payload.code;
474
+ if (typeof code === 'string' && isTypedErrorCode(code)) {
475
+ // Drain the underlying subscription before throwing — the
476
+ // `error` envelope already closed the inner queue on the
477
+ // SDK side, but defensive in case a downstream wraps a
478
+ // longer-lived iterator.
479
+ innerIter.return?.().catch(() => undefined);
480
+ throw errorFromServerErrorEnvelope(result.value);
481
+ }
482
+ }
483
+ return result;
484
+ },
485
+ async return(value) {
486
+ if (innerIter.return !== undefined) {
487
+ return innerIter.return(value);
488
+ }
489
+ return { value: value, done: true };
490
+ },
491
+ async throw(err) {
492
+ if (innerIter.throw !== undefined) {
493
+ return innerIter.throw(err);
494
+ }
495
+ throw err instanceof Error ? err : new Error(String(err));
496
+ },
497
+ };
498
+ },
499
+ };
500
+ }
501
+ /**
502
+ * Send `task.start { graph, input }` and yield the resulting `TaskEvent`
503
+ * stream until `task.done` — inline-graph-spec dispatch.
504
+ *
505
+ * The graph is validated client-side (the SDK-enforceable rule subset,
506
+ * see `validateInlineGraphSpec`) BEFORE the envelope hits the wire. A
507
+ * `QodoInlineGraphValidationError` is thrown when any rule fails — the
508
+ * error's `failures` array carries every violation with a `rule_id` +
509
+ * JSON-Pointer `path` so consumers can route both client-side and
510
+ * server-side rejections through the same branch logic.
511
+ *
512
+ * Opt into `{ preflight: true }` to additionally call
513
+ * `client.specs.validate(graph)` (HTTP) before opening the task — that
514
+ * catches the rules the SDK can't enforce locally (model registry probes,
515
+ * AgentSpec composition against the live agent table). Default `false`.
516
+ *
517
+ * Iterator + early-termination semantics match `start()` — breaking the
518
+ * iterator before `task.done` arrives sends best-effort `task.cancel`.
519
+ * Inline-graph spans carry `qar.graph.entry`, `qar.graph.depth`, and
520
+ * `qar.agent.kind = "inline"` attrs in addition to the standard
521
+ * `qar.client.task.start` set.
522
+ */
523
+ startWithGraph(payload, opts) {
524
+ // Cheap fail-fast `idempotencyKey` check runs BEFORE the graph
525
+ // validator so a typo'd key surfaces immediately, ahead of the
526
+ // potentially-multi-issue graph validation pass.
527
+ if (opts?.idempotencyKey !== undefined) {
528
+ validateIdempotencyKey(opts.idempotencyKey);
529
+ }
530
+ // Local validation — synchronous, no IO, fail fast before either the
531
+ // optional pre-flight HTTP call or the WS write.
532
+ const validateOpts = opts?.maxDepth !== undefined ? { maxDepth: opts.maxDepth } : undefined;
533
+ validateInlineGraphSpec(payload.graph, validateOpts);
534
+ // `defineFunctionTool` auto-bind for graph dispatch — walk every
535
+ // inline-agent node and bind any handlers attached to their
536
+ // `tools[]` entries. Static refs (`agent_id`) and nested subgraphs
537
+ // are handled recursively. Mirrors the matching bind in
538
+ // `startWithAgent`.
539
+ if (this.bindFunctionToolDefs !== undefined) {
540
+ collectGraphFunctionTools(payload.graph, this.bindFunctionToolDefs);
541
+ }
542
+ // Compute the graph depth once for the OTel span attr.
543
+ const depth = TaskClient.computeGraphDepth(payload.graph);
544
+ // Pre-allocate root message id so `taskId` is synchronously available
545
+ // on the returned iterable.
546
+ const rootMessageId = asMessageId(uuidv7());
547
+ const taskId = asTaskId(rootMessageId);
548
+ const sessionIdDeferred = createDeferred();
549
+ const admittedTaskIdDeferred = createDeferred();
550
+ const admissionResultDeferred = createDeferred();
551
+ const ackDeferreds = {
552
+ sessionIdDeferred,
553
+ admittedTaskIdDeferred,
554
+ admissionResultDeferred,
555
+ };
556
+ // Slim-index injection. Synchronous when the manager's catalog has
557
+ // already been discovered (the common case post-connect()); falls
558
+ // through to the eager-promise dispatch path when discovery is still
559
+ // pending.
560
+ const skillsPinned = opts?.skills;
561
+ const needsSkillsAwait = this.skills !== undefined && this.skills.currentSnapshot === null;
562
+ // MCP projection: any inline-agent node carrying `mcpTools` /
563
+ // `mcpToolOverrides` triggers the eager-async dispatch path so the
564
+ // pool catalogs settle before projection.
565
+ const needsMcpProjection = graphHasMcpProjection(payload.graph);
566
+ let graphForWire = payload.graph;
567
+ // Sync inject only when no async-path trigger is armed (see
568
+ // `startWithAgent`'s matching comment — same idempotency
569
+ // concern; `applyGraphSkillsInjection` walks every inline-agent
570
+ // node and would otherwise duplicate per-node instruction text).
571
+ if (this.skills !== undefined && !needsSkillsAwait && !needsMcpProjection) {
572
+ graphForWire = this.applyGraphSkillsInjection(graphForWire, skillsPinned, opts?.skillsCurrentFile);
573
+ }
574
+ // Auto-include the qodo-skills.* tool surface in every inline-agent
575
+ // node of the graph when skills are configured. Mirror of the
576
+ // matching block in `startWithAgent` — the async projection path
577
+ // runs the same merge per-node inside `projectMcpToolsForGraphWire`
578
+ // for the eager-dispatch branch.
579
+ if (this.skills !== undefined && !needsMcpProjection) {
580
+ graphForWire = mergeQodoSkillsToolsIntoGraph(graphForWire);
581
+ }
582
+ const wirePayload = {
583
+ graph: graphForWire,
584
+ input: payload.input ?? {},
585
+ ...(payload.skill !== undefined && payload.skill !== null
586
+ ? { skill: payload.skill }
587
+ : {}),
588
+ // Forward the caller-supplied key when present.
589
+ ...(opts?.idempotencyKey !== undefined
590
+ ? { idempotency_key: opts.idempotencyKey }
591
+ : {}),
592
+ };
593
+ // Opt-in pre-flight: the iterator we return is async, so the HTTP call
594
+ // gets wrapped in an async generator that runs `validate` first, then
595
+ // delegates to the regular subscription. A `valid: false` result raises
596
+ // `QodoInlineGraphValidationError` so consumers see the same error shape
597
+ // whether the SDK or QAR rejected the spec.
598
+ if (opts?.preflight === true || needsSkillsAwait || needsMcpProjection) {
599
+ // Eager dispatch — see the matching comment on `startWithAgent`:
600
+ // the preflight + wire write must run before the consumer can race
601
+ // a `tasks.cancel(stream.taskId)` against the deferred
602
+ // `task.start`.
603
+ const subPromise = this.runGraphPreflightAndDispatch(payload, wirePayload, depth, opts ?? {}, rootMessageId, taskId, ackDeferreds);
604
+ subPromise.catch((err) => {
605
+ const wrapped = err instanceof Error ? err : new Error(String(err));
606
+ if (!sessionIdDeferred.settled())
607
+ sessionIdDeferred.reject(wrapped);
608
+ if (!admittedTaskIdDeferred.settled())
609
+ admittedTaskIdDeferred.reject(wrapped);
610
+ if (!admissionResultDeferred.settled())
611
+ admissionResultDeferred.reject(wrapped);
612
+ });
613
+ // Wrap server errors on the preflighted graph path so consumers get
614
+ // typed errors regardless of which dispatch path they invoke.
615
+ return attachTaskId(yieldFromPromisedSub(subPromise, true), taskId, admittedTaskIdDeferred.promise, sessionIdDeferred.promise, admissionResultDeferred.promise);
616
+ }
617
+ // Closure used by both single-shot dispatch and the admission retry
618
+ // wrapper (when `idempotencyKey` is set). Each call builds a fresh
619
+ // subscription against the supplied rootMessageId.
620
+ const dispatch = (rmid) => this.subscribeAndSend(() => ({
621
+ kind: 'task.start',
622
+ payload: wirePayload,
623
+ rootMessageId: rmid,
624
+ }), asTaskId(rmid), opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
625
+ sessionId,
626
+ messageId,
627
+ ...(wirePayload.skill !== undefined && wirePayload.skill !== null
628
+ ? { skillName: wirePayload.skill }
629
+ : {}),
630
+ graphEntry: payload.graph.entry,
631
+ graphDepth: depth,
632
+ agentKind: TaskClient.computeAgentKind(payload.graph),
633
+ }).lifecycle, rmid, ackDeferreds);
634
+ // Route `error` envelopes through the typed-error wrapper so
635
+ // `graph_spec_rejected` / `agent_reconstruction_failed` / etc.
636
+ // surface as typed throws instead of yielded `kind: 'error'` events
637
+ // on the iterator.
638
+ const firstSub = dispatch(rootMessageId);
639
+ const iter = opts?.idempotencyKey !== undefined
640
+ ? TaskClient.wrapWithAdmissionRetry(firstSub, dispatch, sessionIdDeferred, admittedTaskIdDeferred, admissionResultDeferred)
641
+ : TaskClient.wrapServerErrorsIterator(firstSub);
642
+ return attachTaskId(iter, taskId, admittedTaskIdDeferred.promise, sessionIdDeferred.promise, admissionResultDeferred.promise);
643
+ }
644
+ /**
645
+ * Eagerly run `client.specs.validate(graph)` and, on success, dispatch
646
+ * the `task.start` envelope via `subscribeAndSend`. Returns a
647
+ * `Promise<TaskSubscription>` that resolves once the wire write has
648
+ * happened. Validation rejection surfaces as
649
+ * `QodoInlineGraphValidationError` so consumers don't need a second
650
+ * `instanceof` branch for `SpecValidateResult` failures.
651
+ */
652
+ async runGraphPreflightAndDispatch(inboundPayload, wirePayload, depth, opts, rootMessageId, taskId,
653
+ /**
654
+ * Deferred pair for the `task.started` admission ack (sessionId +
655
+ * admittedTaskId). Forwarded into the eventual `subscribeAndSend` so
656
+ * the caller's `TaskStartIterable.sessionId` + `.admittedTaskId`
657
+ * Promises resolve on the ack.
658
+ */
659
+ ackDeferreds) {
660
+ // Await skills discovery + inject into every inline-agent node before
661
+ // the validator round-trip and the wire write.
662
+ let finalPayload = wirePayload;
663
+ let inboundForSpan = inboundPayload;
664
+ if (this.skills !== undefined) {
665
+ await this.skills.discover();
666
+ const injectedGraph = this.applyGraphSkillsInjection(wirePayload.graph, opts.skills, opts.skillsCurrentFile);
667
+ finalPayload = { ...wirePayload, graph: injectedGraph };
668
+ inboundForSpan = { ...inboundPayload, graph: injectedGraph };
669
+ }
670
+ // MCP projection across every inline-agent node. Runs AFTER
671
+ // skills inject (skills mutate instructions; projection mutates
672
+ // `tools[]`) and BEFORE the optional /v1/specs/validate so the
673
+ // validator sees the post-projection wire surface.
674
+ const projectedGraph = await this.projectMcpToolsForGraphWire(finalPayload.graph);
675
+ if (projectedGraph !== finalPayload.graph) {
676
+ finalPayload = { ...finalPayload, graph: projectedGraph };
677
+ inboundForSpan = { ...inboundForSpan, graph: projectedGraph };
678
+ }
679
+ if (opts.preflight !== true) {
680
+ // Skills-only async path — no preflight HTTP call. Dispatch now.
681
+ return this.subscribeAndSend(() => ({
682
+ kind: 'task.start',
683
+ payload: finalPayload,
684
+ rootMessageId,
685
+ }), taskId, opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
686
+ sessionId,
687
+ messageId,
688
+ ...(finalPayload.skill !== undefined && finalPayload.skill !== null
689
+ ? { skillName: finalPayload.skill }
690
+ : {}),
691
+ graphEntry: inboundForSpan.graph.entry,
692
+ graphDepth: depth,
693
+ agentKind: TaskClient.computeAgentKind(inboundForSpan.graph),
694
+ }).lifecycle, rootMessageId, ackDeferreds);
695
+ }
696
+ const specsResolver = this.resolveSpecs;
697
+ if (specsResolver === undefined) {
698
+ throw new Error("tasks.startWithGraph({ preflight: true }) requires the client's SpecsClient — " +
699
+ "this TaskClient was constructed without a specs resolver");
700
+ }
701
+ const specs = specsResolver();
702
+ const result = await specs.validate(inboundForSpan.graph);
703
+ if (!result.valid) {
704
+ throw new QodoInlineGraphValidationError(result.errors.map((e) => ({
705
+ rule_id: e.rule_id,
706
+ path: e.path,
707
+ message: e.message,
708
+ })));
709
+ }
710
+ return this.subscribeAndSend(() => ({
711
+ kind: 'task.start',
712
+ payload: finalPayload,
713
+ rootMessageId,
714
+ }), taskId, opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskStartSpan({
715
+ sessionId,
716
+ messageId,
717
+ ...(finalPayload.skill !== undefined && finalPayload.skill !== null
718
+ ? { skillName: finalPayload.skill }
719
+ : {}),
720
+ graphEntry: inboundForSpan.graph.entry,
721
+ graphDepth: depth,
722
+ agentKind: TaskClient.computeAgentKind(inboundForSpan.graph),
723
+ }).lifecycle, rootMessageId, ackDeferreds);
724
+ }
725
+ /**
726
+ * Compute the longest-path depth (edge count) from `entry` for the
727
+ * `qar.graph.depth` span attr. Returns 0 when the graph has no edges
728
+ * (single-node graphs).
729
+ *
730
+ * Delegates to the validator's shared `longestPathFrom` so the two call
731
+ * sites can't drift on cycle handling.
732
+ */
733
+ static computeGraphDepth(graph) {
734
+ return longestPathFrom(graph.entry, graph.edges ?? []);
735
+ }
736
+ /**
737
+ * Return the documented `qar.agent.kind` value for a graph: `"inline"`
738
+ * when any node carries an inline `AgentSpec` (or a nested `GraphSpec`),
739
+ * `"agent_id"` when every node is a server-static reference. The
740
+ * `qar.agent.kind` attribute is documented with the closed value set
741
+ * `"inline" | "agent_id"` — keep this in sync with
742
+ * `src/observability/attributes.ts`.
743
+ */
744
+ static computeAgentKind(graph) {
745
+ for (const agent of Object.values(graph.agents ?? {})) {
746
+ const inner = agent.spec;
747
+ if (typeof inner === 'object' && inner !== null && 'kind' in inner) {
748
+ return 'inline';
749
+ }
750
+ }
751
+ return 'agent_id';
752
+ }
753
+ /**
754
+ * Send `task.continue` for an existing `task_id` and yield the resulting `TaskEvent`
755
+ * stream until `task.done`. Same iterator semantics as `start`.
756
+ *
757
+ * Payload shape mirrors QAR's `TaskContinuePayload` minus `task_id` — the id is
758
+ * already supplied as the first argument. `task.continue` carries no
759
+ * `agent_id`/`skill` because those are bound at `task.start` time.
760
+ */
761
+ continue(taskId, payload, opts) {
762
+ const fullPayload = { task_id: taskId, ...payload };
763
+ const sub = this.subscribeAndSend((rootMessageId) => ({ kind: 'task.continue', payload: fullPayload, rootMessageId }), taskId, opts, undefined, (sessionId, messageId) => this.spanRecorder.startTaskContinueSpan({ sessionId, messageId, taskId }).lifecycle);
764
+ // Typed-error wrap on continue — `task_not_found`,
765
+ // `cross_pod_resume_aborted`, `agent_reconstruction_failed`,
766
+ // `message_history_load_failed`, the HITL family, etc. all surface
767
+ // here when the server rejects the continue.
768
+ return TaskClient.wrapServerErrorsIterator(sub);
769
+ }
770
+ /**
771
+ * Cold-start helper. Unifies `task.start` and `task.continue` behind
772
+ * a single call so consumers with deterministic `idempotencyKey`
773
+ * derivation (Slack bots, CLI tools, IDE plugins, web apps without
774
+ * server affinity) don't have to track in-memory
775
+ * `(idempotencyKey → taskId)` mappings across process restarts.
776
+ *
777
+ * Behavior, gated on QAR's `task.started` admission response:
778
+ *
779
+ * - **No session yet** (`isNew === true`, no `previousTaskId`) —
780
+ * fresh dispatch. The returned handle iterates the newly-started
781
+ * task's event stream.
782
+ * - **Existing session, `state === 'auto-paused'`** (`isNew === false`) —
783
+ * QAR returned the existing `task_id`. The helper auto-emits a
784
+ * `tasks.continue(taskId, { input })` to deliver the caller's
785
+ * input; the returned handle iterates the continue subscription's
786
+ * events.
787
+ * - **Existing session, `state === 'running'`** (`isNew === false`) —
788
+ * QAR returned the existing `task_id`. Same auto-continue path;
789
+ * QAR queues the input until the next auto-pause boundary.
790
+ * - **Terminal re-dispatch** (`isNew === true` + `previousTaskId` +
791
+ * `previousState`) — QAR created a NEW task on the same
792
+ * `session_id`, inheriting the full `session_messages` history
793
+ * (B-raw). The handle iterates the new task's stream; consumer
794
+ * can read `previousTaskId` + `previousState` to surface
795
+ * "this is a new task after a prior failure" UX.
796
+ *
797
+ * `graphSpec` is required for the fresh-dispatch branch. On
798
+ * already-dispatched sessions QAR ignores it (the existing task's
799
+ * spec is the source of truth). The helper throws
800
+ * {@link RequiredGraphSpecError} synchronously if `graphSpec` is
801
+ * `undefined` — consumers who don't have a graphSpec on hand should
802
+ * call {@link continue} or {@link forceResume} directly.
803
+ *
804
+ * Paired with QAR-side idempotent admission. Older QAR builds without
805
+ * idempotent admission either reject `session_already_dispatched` (the
806
+ * case this helper fixes) or — for fresh sessions — behave identically
807
+ * (the helper defaults `isNew` to `true` when the field is absent in
808
+ * the ack).
809
+ */
810
+ async continueOrStart(idempotencyKey, graphSpec, options = {}) {
811
+ if (graphSpec === undefined) {
812
+ throw new RequiredGraphSpecError(idempotencyKey);
813
+ }
814
+ const stream = this.startWithGraph({
815
+ graph: graphSpec,
816
+ ...(options.input !== undefined ? { input: options.input } : {}),
817
+ }, { idempotencyKey });
818
+ const admission = await stream.admissionResult;
819
+ let iter;
820
+ if (admission.isNew) {
821
+ iter = stream;
822
+ }
823
+ else if (options.input !== undefined) {
824
+ // Idempotent return — pivot to `task.continue` to deliver the
825
+ // caller's input. QAR routes the input through the existing
826
+ // task's queue; the continue subscription owns the event stream
827
+ // from here.
828
+ //
829
+ // The cast widens `QodoTaskStartInput` (the typed `task.start.input`
830
+ // shape) to `TaskContinuePayload['input']` (codegen's
831
+ // `{ [k: string]: unknown }`). The runtime values are identical —
832
+ // QAR consumes the same `user_query` / `deps_overrides` / `metadata`
833
+ // fields on continue. Without the cast, TypeScript would reject
834
+ // the assignment because the typed shape lacks the open-shape
835
+ // index signature codegen emits.
836
+ iter = this.continue(admission.taskId, {
837
+ input: options.input,
838
+ });
839
+ }
840
+ else {
841
+ // is_new=false and no input — the caller just wanted a lookup.
842
+ // The start stream auto-closed in `TaskSubscription.consider` on
843
+ // `is_new === false`; expose an immediately-empty iterator.
844
+ iter = stream;
845
+ }
846
+ return {
847
+ [Symbol.asyncIterator]: () => iter[Symbol.asyncIterator](),
848
+ taskId: admission.taskId,
849
+ sessionId: admission.sessionId,
850
+ isNew: admission.isNew,
851
+ ...(admission.previousTaskId !== undefined
852
+ ? { previousTaskId: admission.previousTaskId }
853
+ : {}),
854
+ ...(admission.previousState !== undefined
855
+ ? { previousState: admission.previousState }
856
+ : {}),
857
+ };
858
+ }
859
+ /**
860
+ * Explicit stuck-session recovery. Sends `task.forceResume` and
861
+ * awaits the matching `task.force_resumed` ack.
862
+ *
863
+ * Use case: a prior consumer process died mid-tool-call or mid-HITL;
864
+ * the session is stuck `running` with outstanding `tool_call_id` /
865
+ * `hitl_id` that will never resolve. `forceResume` triggers QAR to:
866
+ *
867
+ * 1. Cancel the in-flight tool call (mirrors the system-cancel
868
+ * primitives used by `task.cancel` and the A2A bridge).
869
+ * 2. Synth-resolve pending HITLs.
870
+ * 3. Append a `{role: 'system', content: 'process restart — outstanding
871
+ * tool call cancelled'}` marker to `session_messages` (audit trail).
872
+ * 4. Transition the session to `'auto-paused'`.
873
+ *
874
+ * If the session is already `auto-paused` or terminal, QAR
875
+ * passthroughs and reports the actual state.
876
+ *
877
+ * Returns `{taskId, state}` — the consumer can then call
878
+ * `tasks.continue(taskId, { input })` with their next turn's input.
879
+ */
880
+ forceResume(idempotencyKey) {
881
+ // Same wire validation as `task.start.idempotencyKey` — surfaces
882
+ // bad-input synchronously rather than waiting for a server
883
+ // round-trip.
884
+ validateIdempotencyKey(idempotencyKey);
885
+ const connection = this.resolveConnection();
886
+ const rootMessageId = asMessageId(uuidv7());
887
+ const deferred = createDeferred();
888
+ const sub = new TaskSubscription(rootMessageId, undefined,
889
+ // onEarlyReturn — no-op. forceResume is a one-shot async call;
890
+ // there's no consumer-iterator to break, and a stray `task.cancel`
891
+ // here would target the wrong task (the recovered session's
892
+ // taskId isn't known yet).
893
+ () => undefined, (s) => connection.unsubscribe(s), undefined, this.metrics, false, undefined, {
894
+ resolve: (args) => deferred.resolve(args),
895
+ reject: (err) => deferred.reject(err),
896
+ });
897
+ connection.subscribe(sub);
898
+ try {
899
+ connection.sendEnvelope({
900
+ kind: 'task.forceResume',
901
+ payload: { idempotency_key: idempotencyKey },
902
+ }, { messageId: rootMessageId });
903
+ }
904
+ catch (err) {
905
+ sub.fail(err instanceof Error ? err : new Error(String(err)));
906
+ throw err;
907
+ }
908
+ return deferred.promise.then((args) => ({
909
+ taskId: args.taskId,
910
+ state: args.state,
911
+ }));
912
+ }
913
+ /**
914
+ * Send `task.cancel` and await the matching `task.done { status: "canceled" }`.
915
+ *
916
+ * The returned promise resolves once the server confirms the cancel via
917
+ * `task.done`. If the server reports failure (`task.done { status: "failed" }`)
918
+ * during cancellation, `status: 'failed'` surfaces here so callers can
919
+ * distinguish — they didn't ask for it, but it's still a terminal state.
920
+ *
921
+ * Cross-process recovery: pass `opts.sessionId` to cancel a task whose
922
+ * `task_id` was hydrated from durable storage (i.e. the SDK never
923
+ * observed the originating `task.started` in this process). Without the
924
+ * override the SDK throws {@link QodoColdAddressError} before the wire
925
+ * write.
926
+ *
927
+ * `opts.signal` semantic: **"signal aborts wait-for-task.done"**. If the
928
+ * signal fires before `task.done` arrives, `cancel` throws
929
+ * {@link QodoCancelAbortedError} with the task_id and the abort reason.
930
+ * The wire `task.cancel` was already sent; the underlying task's
931
+ * terminal ack still routes through its `tasks.start` /
932
+ * `tasks.continue` subscription. The signal does NOT cause a second
933
+ * `task.cancel` (that would be redundant — the consumer's intent is
934
+ * "stop waiting", not "cancel twice"). Pre-aborted signals throw
935
+ * synchronously before any wire write.
936
+ */
937
+ async cancel(taskId, payload, opts) {
938
+ const fullPayload = { task_id: taskId, ...(payload ?? {}) };
939
+ // Pre-aborted signals short-circuit BEFORE any wire write so the
940
+ // consumer's intent ("don't even bother") is honored without a
941
+ // server round-trip. `tasks.cancel` is `async`, so the throw
942
+ // surfaces as a rejected Promise; consumers' `try/catch` around
943
+ // `await tasks.cancel(...)` catches it identically to the
944
+ // post-send-abort case below. `kind: 'pre_aborted'` lets consumers
945
+ // branch on "did any wire traffic happen?" without parsing the
946
+ // error message.
947
+ const signal = opts?.signal;
948
+ if (signal?.aborted === true) {
949
+ throw new QodoCancelAbortedError('pre_aborted', taskId, signal.reason);
950
+ }
951
+ // Cancel is short-lived but distinct from the task's own span: it covers
952
+ // the WS write + the wait for `task.done(canceled)`. We open it with
953
+ // `undefined` for the subscription so the wrapped `task.done` doesn't
954
+ // double-close the task span (the underlying task's span is owned by the
955
+ // `tasks.start` / `tasks.continue` subscription, not this one).
956
+ //
957
+ // The span is created by the buildSpan callback (closing-over the local
958
+ // `cancelSpan` so we can fail it from the catch arm if `subscribeAndSend`
959
+ // itself throws — without that, a `Connection.sendEnvelope` failure
960
+ // before the subscription was returned would leak the span).
961
+ let cancelSpan;
962
+ let sub;
963
+ // Don't forward `signal` to subscribeAndSend — its generic
964
+ // bindAbort would emit a SECOND `task.cancel` on abort, which is
965
+ // redundant (we're already cancelling) and would race the user's
966
+ // payload `reason`. Pass only the cold-address `sessionId` override.
967
+ const subOpts = opts?.sessionId !== undefined ? { sessionId: opts.sessionId } : undefined;
968
+ try {
969
+ sub = this.subscribeAndSend((rootMessageId) => ({ kind: 'task.cancel', payload: fullPayload, rootMessageId }), taskId, subOpts, undefined, (sessionId, messageId) => {
970
+ cancelSpan = this.spanRecorder.startTaskCancelSpan({ sessionId, messageId, taskId });
971
+ return undefined;
972
+ });
973
+ }
974
+ catch (err) {
975
+ cancelSpan?.fail(err);
976
+ throw err;
977
+ }
978
+ // Wire the abort signal to close the subscription locally. The
979
+ // for-await below exits and falls through to the typed throw.
980
+ // Cleanup removes the listener so a long-lived signal doesn't leak
981
+ // references through the subscription closure once the cancel
982
+ // resolves.
983
+ //
984
+ // `abortedDuringWait` is read by the for-await exit branch below;
985
+ // TS's control-flow analysis narrows `signal.aborted` against the
986
+ // earlier pre-aborted-check at function entry, so we capture the
987
+ // late-aborted state in a separate flag.
988
+ let abortListener;
989
+ let abortedDuringWait = false;
990
+ let abortReasonDuringWait;
991
+ if (signal !== undefined) {
992
+ abortListener = () => {
993
+ abortedDuringWait = true;
994
+ abortReasonDuringWait = signal.reason;
995
+ sub.close();
996
+ };
997
+ signal.addEventListener('abort', abortListener, { once: true });
998
+ }
999
+ try {
1000
+ for await (const event of sub) {
1001
+ if (event.kind === 'task.done' && event.payload.task_id === taskId) {
1002
+ const done = event;
1003
+ // `done.payload.task_id` arrives from codegen as plain `string` (the
1004
+ // overlay brands the public `TaskDonePayload` type but the generated
1005
+ // envelope's `payload` references the unbranded codegen variant).
1006
+ // We've already matched against `taskId` by equality above, so it is
1007
+ // the same UUID — re-using the input branded value keeps the cast
1008
+ // off the boundary.
1009
+ //
1010
+ // Surface the server's actual terminal status verbatim — including
1011
+ // `'completed'`, which means the task finished before our cancel was
1012
+ // processed (the race the docs promise we'll surface).
1013
+ if (done.payload.status === 'failed') {
1014
+ cancelSpan?.fail(new Error(done.payload.error ?? 'cancel returned failed'));
1015
+ }
1016
+ else {
1017
+ cancelSpan?.succeed();
1018
+ }
1019
+ return {
1020
+ task_id: taskId,
1021
+ status: done.payload.status,
1022
+ ...(done.payload.error !== undefined && done.payload.error !== null
1023
+ ? { error: done.payload.error }
1024
+ : {}),
1025
+ };
1026
+ }
1027
+ // A server `error` envelope arriving on the cancel iterator
1028
+ // with a documented typed code (e.g. `cancel_routing_failed`,
1029
+ // `cancel_failed`, `task_not_found`, `task_not_waiting`) is
1030
+ // surfaced as a typed throw so consumers can branch on
1031
+ // `instanceof QodoCancelRoutingFailedError` etc. instead of
1032
+ // racing the absent `task.done`. Unrecognized codes continue
1033
+ // to fall through into the "connection closed before
1034
+ // task.done" fallback so we don't change behavior for codes
1035
+ // the SDK hasn't catalogued yet.
1036
+ if (event.kind === 'error') {
1037
+ const code = event.payload.code;
1038
+ if (typeof code === 'string' && isTypedErrorCode(code)) {
1039
+ const err = errorFromServerErrorEnvelope(event);
1040
+ cancelSpan?.fail(err);
1041
+ throw err;
1042
+ }
1043
+ }
1044
+ }
1045
+ // for-await drained without a `task.done` — either the abort
1046
+ // fired (consumer chose not to wait) or the connection closed.
1047
+ if (abortedDuringWait) {
1048
+ const err = new QodoCancelAbortedError('aborted_after_send', taskId, abortReasonDuringWait);
1049
+ cancelSpan?.fail(err);
1050
+ throw err;
1051
+ }
1052
+ cancelSpan?.fail(new Error('connection closed before task.done arrived'));
1053
+ return {
1054
+ task_id: taskId,
1055
+ status: 'failed',
1056
+ error: 'connection closed before task.done arrived',
1057
+ };
1058
+ }
1059
+ catch (err) {
1060
+ cancelSpan?.fail(err);
1061
+ throw err;
1062
+ }
1063
+ finally {
1064
+ if (abortListener !== undefined && signal !== undefined) {
1065
+ signal.removeEventListener('abort', abortListener);
1066
+ }
1067
+ }
1068
+ }
1069
+ /**
1070
+ * Send `task.resubscribe { task_id, since_message_id? }` and yield replayed
1071
+ * `TaskEvent`s from the server-held ring buffer (per-session cap of 1000
1072
+ * envelopes). No client outbox — durability is server-side.
1073
+ *
1074
+ * Use this when you have a `task_id` from a prior connection (e.g. across a
1075
+ * process restart) and want to catch up from a known anchor. For automatic
1076
+ * reconnect-on-drop within a single `connect()` lifetime, the SDK does this
1077
+ * for you transparently — see `qar.client.reconnect*` events on the
1078
+ * canonical iterator.
1079
+ *
1080
+ * If the server's ring buffer has rotated past `sinceMessageId` (very long
1081
+ * disconnects, or a missing/wrong anchor), the server emits an `error`
1082
+ * envelope with a `replay_anchor_missing`-style code; that event surfaces
1083
+ * as the iterator's final value and ends the stream. The consumer can
1084
+ * decide to start the task fresh or surrender.
1085
+ */
1086
+ resubscribe(taskId, opts) {
1087
+ const payload = {
1088
+ task_id: taskId,
1089
+ since_message_id: opts?.sinceMessageId ?? null,
1090
+ };
1091
+ return this.subscribeAndSend((rootMessageId) => ({ kind: 'task.resubscribe', payload, rootMessageId }), taskId, opts,
1092
+ // Resubscribe is observation, not initiation: breaking the iterator
1093
+ // must NOT cancel the underlying task. The user may still want it
1094
+ // running; they can call `tasks.cancel` separately if not.
1095
+ //
1096
+ // The outbound `task.resubscribe.message_id` is the replay anchor —
1097
+ // every descendant counts as a replay envelope so the SDK can stamp
1098
+ // `replay_envelopes_received_total`.
1099
+ { suppressEarlyReturnCancel: true, rootIsReplayAnchor: true }, (sessionId, messageId) => this.spanRecorder.startTaskResubscribeSpan({
1100
+ sessionId,
1101
+ messageId,
1102
+ taskId,
1103
+ ...(opts?.sinceMessageId !== undefined ? { sinceMessageId: opts.sinceMessageId } : {}),
1104
+ }).lifecycle);
1105
+ }
1106
+ /**
1107
+ * Pre-allocate a `MessageId`, register a `TaskSubscription` against it, then
1108
+ * send the outbound envelope using the same id. Subscribing before sending
1109
+ * eliminates the race where the server's first reply could land before the
1110
+ * subscription is in place.
1111
+ *
1112
+ * Pre-aborted signals short-circuit BEFORE the send so we never start
1113
+ * server-side work the consumer has already given up on.
1114
+ *
1115
+ * `preallocatedRootMessageId` lets the caller mint the root id at the
1116
+ * public-API boundary so it can derive `task_id` synchronously for
1117
+ * `task.start` envelopes (the wire protocol codifies
1118
+ * `task_id == task.start.message_id`). When omitted, we allocate fresh —
1119
+ * the `task.continue` / `task.cancel` / `task.resubscribe` call sites
1120
+ * already carry the task_id as an explicit arg.
1121
+ */
1122
+ subscribeAndSend(buildOutbound, knownTaskId, opts, behavior,
1123
+ /**
1124
+ * Optional span builder. Returns a `SpanLifecycle` to attach to the
1125
+ * subscription (closed on terminal/fail/early-return), or `undefined`
1126
+ * when the caller manages span lifetime itself (e.g. `tasks.cancel`).
1127
+ * The first arg is the per-Task `session_id` if known (`undefined`
1128
+ * on the `task.start` path before admission completes); the span
1129
+ * recorder treats it as an optional attribute.
1130
+ */
1131
+ buildSpan, preallocatedRootMessageId,
1132
+ /**
1133
+ * Optional `task.started` admission-ack deferreds wired in by the
1134
+ * `task.start` family (`tasks.start` / `tasks.startWithAgent` /
1135
+ * `tasks.startWithGraph`). The constructed `TaskSubscription`
1136
+ * resolves all three deferreds atomically when `task.started` arrives:
1137
+ * `sessionId` from the envelope's inherited `session_id` field,
1138
+ * `admittedTaskId` from the payload's `task_id` field, and
1139
+ * `admissionResultDeferred` with the full `TaskAdmissionResult`
1140
+ * (idempotent-admission fields `isNew` / `state` / `previousTaskId` /
1141
+ * `previousState`). `undefined` for non-start paths (`task.continue`
1142
+ * / `task.cancel` / `task.resubscribe`) which already know both ids
1143
+ * from a prior admission.
1144
+ */
1145
+ taskStartedDeferred) {
1146
+ const connection = this.resolveConnection();
1147
+ const rootMessageId = preallocatedRootMessageId ?? asMessageId(uuidv7());
1148
+ let onAbortCleanup;
1149
+ const suppressEarlyReturnCancel = behavior?.suppressEarlyReturnCancel === true;
1150
+ // Build the outbound shape NOW (rather than at send time below) so
1151
+ // we can gate `opts.sessionId` span attribution on the kind. On
1152
+ // `task.start` the consumer's `sessionId` is documented as ignored
1153
+ // (admission CREATES the session) — and using it for span
1154
+ // attribution would pollute the span with a caller-supplied value
1155
+ // the wire never sees. Only the ongoing kinds (`task.continue` /
1156
+ // `task.cancel` / `task.resubscribe`) legitimately consume the
1157
+ // override.
1158
+ //
1159
+ // `buildOutbound` is a pure constructor over `rootMessageId` (no
1160
+ // side effects), so calling it twice is safe — but we cache the
1161
+ // result and reuse below to avoid the redundant work.
1162
+ const outbound = buildOutbound(rootMessageId);
1163
+ const isStart = outbound.kind === 'task.start';
1164
+ // No connection-level session_id. For `task.continue` /
1165
+ // `task.cancel` / `task.resubscribe` we already know `knownTaskId`
1166
+ // here and can look up the per-Task session captured from the prior
1167
+ // `task.started`. For `task.start` (knownTaskId === undefined at the
1168
+ // public entry point — derived from the outbound message_id but no
1169
+ // ack yet), the span opens without a session attribute; the
1170
+ // subscription's later `task.started` handler stamps the canonical
1171
+ // id on the lifecycle once admission completes.
1172
+ //
1173
+ // When the consumer passed `opts.sessionId` (the cold-address path),
1174
+ // use that for span attribution on ongoing kinds — the in-memory map
1175
+ // will be empty on cross-process recovery, so the lookup would
1176
+ // return `undefined` and the span would miss its `qar.session_id`
1177
+ // attribute despite the consumer knowing the right value. On
1178
+ // `task.start` we ignore `opts.sessionId` per the contract
1179
+ // documented in `TaskOptions.sessionId`.
1180
+ const knownSessionId = isStart
1181
+ ? undefined
1182
+ : (opts?.sessionId ??
1183
+ (knownTaskId !== undefined ? connection.getSessionForTask(knownTaskId) : undefined));
1184
+ const span = buildSpan?.(knownSessionId, rootMessageId);
1185
+ const sub = new TaskSubscription(rootMessageId, knownTaskId, (taskIdAtCancel) => {
1186
+ // `tasks.resubscribe` is observation-only — breaking the iterator
1187
+ // there must NOT cancel the underlying task (the consumer may still
1188
+ // want it running; they'll call `tasks.cancel` separately if not).
1189
+ if (suppressEarlyReturnCancel)
1190
+ return;
1191
+ const cancelTaskId = taskIdAtCancel ?? knownTaskId;
1192
+ if (cancelTaskId !== undefined && connection.isOpen) {
1193
+ try {
1194
+ connection.sendEnvelope({
1195
+ kind: 'task.cancel',
1196
+ payload: { task_id: cancelTaskId, reason: 'iterator early-termination' },
1197
+ });
1198
+ }
1199
+ catch {
1200
+ // Connection may have closed mid-flight; cancel is best-effort.
1201
+ }
1202
+ }
1203
+ }, (s) => {
1204
+ connection.unsubscribe(s);
1205
+ // Retract any throttled frame still sitting in the connection's
1206
+ // backpressure queue. Without this, a queued `task.continue`
1207
+ // would still hit the wire on the next `flow.resume` even though
1208
+ // the consumer has already canceled / aborted / broken the
1209
+ // iterator — producing `cancel → resume → continue` reordering
1210
+ // on the server. No-op if the frame already flushed or never
1211
+ // entered the throttle path.
1212
+ connection.dropQueued(rootMessageId);
1213
+ // Drop the AbortSignal listener so a long-lived signal that's
1214
+ // reused across many tasks doesn't accumulate dead closures
1215
+ // pinning every completed subscription + connection in memory.
1216
+ onAbortCleanup?.();
1217
+ onAbortCleanup = undefined;
1218
+ }, span, this.metrics, behavior?.rootIsReplayAnchor === true, taskStartedDeferred !== undefined
1219
+ ? {
1220
+ resolve: ({ sessionId, taskId: ackTaskId, isNew, state, previousTaskId, previousState, }) => {
1221
+ taskStartedDeferred.sessionIdDeferred.resolve(sessionId);
1222
+ taskStartedDeferred.admittedTaskIdDeferred.resolve(ackTaskId);
1223
+ taskStartedDeferred.admissionResultDeferred.resolve({
1224
+ taskId: ackTaskId,
1225
+ sessionId,
1226
+ isNew,
1227
+ ...(state !== undefined ? { state } : {}),
1228
+ ...(previousTaskId !== undefined ? { previousTaskId } : {}),
1229
+ ...(previousState !== undefined ? { previousState } : {}),
1230
+ });
1231
+ },
1232
+ reject: (err) => {
1233
+ taskStartedDeferred.sessionIdDeferred.reject(err);
1234
+ taskStartedDeferred.admittedTaskIdDeferred.reject(err);
1235
+ taskStartedDeferred.admissionResultDeferred.reject(err);
1236
+ },
1237
+ }
1238
+ : undefined);
1239
+ if (opts?.signal?.aborted === true) {
1240
+ // Pre-aborted: never send the outbound envelope. For `task.continue`
1241
+ // / `task.resubscribe` / `task.cancel` (we know the id) and the
1242
+ // user wants to give up before we start, emit `task.cancel` so the
1243
+ // running task on the server tears down promptly UNLESS this is a
1244
+ // resubscribe-style observation that mustn't cancel the live task.
1245
+ // For `task.start` (no id yet) there's nothing for the server to
1246
+ // cancel — just close the iterator.
1247
+ if (knownTaskId !== undefined &&
1248
+ connection.isOpen &&
1249
+ !suppressEarlyReturnCancel) {
1250
+ try {
1251
+ // Forward the cold-address `sessionId` (if supplied) so the
1252
+ // pre-abort cancel resolves the same session the never-sent
1253
+ // ongoing envelope would have used. Without this, cold
1254
+ // continue/resubscribe with a pre-aborted signal would
1255
+ // silently drop the cancel via the catch below.
1256
+ connection.sendEnvelope({
1257
+ kind: 'task.cancel',
1258
+ payload: { task_id: knownTaskId, reason: 'pre-aborted signal' },
1259
+ }, opts?.sessionId !== undefined ? { sessionId: opts.sessionId } : undefined);
1260
+ }
1261
+ catch {
1262
+ // best-effort — common case is cold-`tasks.start` pre-abort,
1263
+ // where the task was never admitted server-side and there's
1264
+ // no session to address. `QodoColdAddressError` lands here
1265
+ // and we silently drop (the server has nothing to cancel).
1266
+ }
1267
+ }
1268
+ sub.close();
1269
+ return sub;
1270
+ }
1271
+ connection.subscribe(sub);
1272
+ // `outbound` was built earlier to gate span sessionId on the kind;
1273
+ // reuse here.
1274
+ try {
1275
+ // Run the outbound `sendEnvelope` inside the SDK span's context.
1276
+ // This is what makes `Connection`'s `TraceContextProvider.current()`
1277
+ // see our SDK span as the active span at envelope-encode time, so
1278
+ // the outbound `traceparent` references the SDK span's id —
1279
+ // letting QAR parent its server-side `qar.protocol.<kind>` span
1280
+ // under ours. Without `withContext`, the active span at send time
1281
+ // would still be the consumer's span (or none), and the SDK span
1282
+ // would be a leaf — breaking the cross-process trace tree.
1283
+ //
1284
+ // The `span` may be undefined when the call site doesn't open one
1285
+ // (e.g. `tasks.cancel` opens its own span out-of-band). Fall
1286
+ // through to the unwrapped path in that case.
1287
+ //
1288
+ // Forward the caller-supplied cold-address `sessionId` (if any)
1289
+ // on the three ongoing kinds. Omitted on `task.start` — that path
1290
+ // doesn't carry a wire `session_id` (admission CREATES the
1291
+ // session). `sendEnvelope`'s `resolveOutboundSessionId` honours
1292
+ // the override first, then falls back to the in-memory map, then
1293
+ // throws `QodoColdAddressError` — so consumers carrying their own
1294
+ // `{ taskId, sessionId }` pair survive the cold-address path
1295
+ // without a wire round-trip.
1296
+ const sessionOverride = opts?.sessionId;
1297
+ const send = () => {
1298
+ switch (outbound.kind) {
1299
+ case 'task.start':
1300
+ connection.sendEnvelope({ kind: 'task.start', payload: outbound.payload }, { messageId: rootMessageId });
1301
+ break;
1302
+ case 'task.continue':
1303
+ connection.sendEnvelope({ kind: 'task.continue', payload: outbound.payload }, sessionOverride !== undefined
1304
+ ? { messageId: rootMessageId, sessionId: sessionOverride }
1305
+ : { messageId: rootMessageId });
1306
+ break;
1307
+ case 'task.cancel':
1308
+ connection.sendEnvelope({ kind: 'task.cancel', payload: outbound.payload }, sessionOverride !== undefined
1309
+ ? { messageId: rootMessageId, sessionId: sessionOverride }
1310
+ : { messageId: rootMessageId });
1311
+ break;
1312
+ case 'task.resubscribe':
1313
+ connection.sendEnvelope({ kind: 'task.resubscribe', payload: outbound.payload }, sessionOverride !== undefined
1314
+ ? { messageId: rootMessageId, sessionId: sessionOverride }
1315
+ : { messageId: rootMessageId });
1316
+ break;
1317
+ }
1318
+ };
1319
+ if (span !== undefined) {
1320
+ span.withContext(send);
1321
+ }
1322
+ else {
1323
+ send();
1324
+ }
1325
+ }
1326
+ catch (err) {
1327
+ sub.fail(err instanceof Error ? err : new Error(String(err)));
1328
+ throw err;
1329
+ }
1330
+ if (opts?.signal !== undefined) {
1331
+ onAbortCleanup = this.bindAbort(connection, sub, opts.signal, opts.sessionId);
1332
+ }
1333
+ return sub;
1334
+ }
1335
+ /**
1336
+ * Retry wrapper for `admission_in_progress` errors.
1337
+ *
1338
+ * When a deterministic-key admission collides with an in-flight admission
1339
+ * on the same `(tenant_id, idempotency_key)`, QAR emits
1340
+ * `error { code: 'admission_in_progress', retry_after_ms? }` instead of
1341
+ * starting work. The SDK MUST retry with the same payload (server
1342
+ * derives the same `session_id`) after the hinted delay — or, if absent,
1343
+ * after exponential backoff with jitter — capped at
1344
+ * `PENDING_ADMISSION_TIMEOUT` (5 min). The wrapper only fires on the
1345
+ * deterministic-key path; the DX-default omitted-key path never sees
1346
+ * `admission_in_progress` (each call mints a fresh `uuidv7` server-side
1347
+ * — no collisions possible).
1348
+ *
1349
+ * Returns an `AsyncIterable<TaskEvent>` that wraps the underlying
1350
+ * subscription. The wrapper:
1351
+ *
1352
+ * 1. Calls `buildAttempt(rootMessageId)` to register a fresh
1353
+ * `TaskSubscription` and emit the wire `task.start`.
1354
+ * 2. Reads the first event.
1355
+ * 3. If `error { code: 'admission_in_progress' }`: capture the
1356
+ * scoped `session_id` and `retry_after_ms` hint, sleep, retry
1357
+ * with a fresh `rootMessageId`. Lifecycle bookkeeping
1358
+ * (subscription close, span end) flows through the underlying
1359
+ * subscription's terminal handling on the error envelope.
1360
+ * 4. If `admission_stalled` typed throw arrives via the
1361
+ * wrap-server-errors layer: propagate (terminal, non-retryable).
1362
+ * 5. Otherwise: yield the first event and pass through every
1363
+ * subsequent event.
1364
+ *
1365
+ * After {@link PENDING_ADMISSION_TIMEOUT_MS} elapsed wall-clock time
1366
+ * the wrapper throws {@link QodoAdmissionTimeoutError} carrying the
1367
+ * scoped `session_id` so the caller can `task.resubscribe` if a
1368
+ * recovery path exists.
1369
+ */
1370
+ static wrapWithAdmissionRetry(
1371
+ /**
1372
+ * Pre-dispatched first attempt — already subscribed and the wire
1373
+ * `task.start` is in flight before the wrapper is constructed.
1374
+ * Eager dispatch preserves the SDK contract that
1375
+ * `client.tasks.cancel(stream.taskId)` can be called immediately
1376
+ * after `client.tasks.start(...)` returns without iterating the
1377
+ * stream first.
1378
+ */
1379
+ firstSub, buildAttempt, sessionIdDeferred, admittedTaskIdDeferred, admissionResultDeferred) {
1380
+ // The retry loop runs in a **background driver** independent of
1381
+ // consumer iteration. Advancing retries inside `next()` would mean a
1382
+ // caller who awaited `stream.sessionId` BEFORE iterating could hang
1383
+ // forever (the retry would never sleep, never re-dispatch, never
1384
+ // observe `task.started`). Instead the driver:
1385
+ //
1386
+ // 1. Pulls from the current attempt's iterator eagerly.
1387
+ // 2. Detects `admission_in_progress`, sleeps, re-dispatches.
1388
+ // 3. Pushes every other event to a consumer-facing `AsyncQueue`.
1389
+ // 4. Settles `sessionIdDeferred` + `admittedTaskIdDeferred` from
1390
+ // the inbound `task.started` (resolved by the underlying
1391
+ // `TaskSubscription`'s resolver path) OR rejects both on
1392
+ // timeout / typed terminal so the caller's
1393
+ // `await stream.sessionId` always settles.
1394
+ //
1395
+ // The returned `AsyncIterable` is a thin adapter over the queue —
1396
+ // breaking it early closes the queue, which the driver respects
1397
+ // on its next loop iteration.
1398
+ const consumerQueue = new AsyncQueue();
1399
+ let driverAborted = false;
1400
+ // Hoist the current attempt's subscription reference to the wrapper
1401
+ // scope so the consumer-facing iterator's `return()` / `throw()` can
1402
+ // close the underlying subscription on abort. Without this, the
1403
+ // driver may be blocked inside `await iter.next()` on the AsyncQueue
1404
+ // and never observe `driverAborted` if no envelopes arrive — the
1405
+ // subscription leaks and best-effort cancel never fires.
1406
+ let currentSub = firstSub;
1407
+ const abortFromConsumer = () => {
1408
+ driverAborted = true;
1409
+ // Reject the admission deferreds: without this, `await
1410
+ // stream.sessionId` after an early-break by the consumer would
1411
+ // hang forever. Use a typed `QodoStreamAbortedError` so callers
1412
+ // can branch on voluntary-abort vs. genuine admission failure.
1413
+ const abortErr = new QodoStreamAbortedError();
1414
+ if (!sessionIdDeferred.settled())
1415
+ sessionIdDeferred.reject(abortErr);
1416
+ if (!admittedTaskIdDeferred.settled())
1417
+ admittedTaskIdDeferred.reject(abortErr);
1418
+ // Close the current attempt's subscription. `return()` (vs
1419
+ // `close()`) is the right call: it triggers
1420
+ // `onEarlyReturn` → best-effort `task.cancel` to QAR if we know
1421
+ // the task_id, then closes the queue and unregisters. Closing
1422
+ // the queue is what unblocks the driver's `await iter.next()`
1423
+ // → the driver sees `r.done`, checks `driverAborted`, exits.
1424
+ try {
1425
+ void currentSub.return();
1426
+ }
1427
+ catch {
1428
+ /* best-effort — subscription may already be closed via
1429
+ * terminal envelope or transport drop */
1430
+ }
1431
+ consumerQueue.close();
1432
+ };
1433
+ const runDriver = async () => {
1434
+ const startedAt = Date.now();
1435
+ let attempt = 0;
1436
+ let lastSessionId;
1437
+ while (true) {
1438
+ if (driverAborted) {
1439
+ consumerQueue.close();
1440
+ return;
1441
+ }
1442
+ let admissionInProgress;
1443
+ try {
1444
+ const wrapped = TaskClient.wrapServerErrorsIterator(currentSub);
1445
+ const iter = wrapped[Symbol.asyncIterator]();
1446
+ while (true) {
1447
+ const r = await iter.next();
1448
+ if (driverAborted) {
1449
+ try {
1450
+ await iter.return?.(undefined);
1451
+ }
1452
+ catch {
1453
+ /* best-effort */
1454
+ }
1455
+ consumerQueue.close();
1456
+ return;
1457
+ }
1458
+ if (r.done)
1459
+ break;
1460
+ const ev = r.value;
1461
+ if (ev.kind === 'error') {
1462
+ const code = ev.payload.code;
1463
+ if (typeof code === 'string' && code === 'admission_in_progress') {
1464
+ admissionInProgress = ev;
1465
+ // Drain the current attempt's iter — its queue was
1466
+ // closed by the error envelope already; return() is a
1467
+ // no-op safety net (idempotent per AsyncQueue contract).
1468
+ try {
1469
+ await iter.return?.(undefined);
1470
+ }
1471
+ catch {
1472
+ /* best-effort */
1473
+ }
1474
+ break;
1475
+ }
1476
+ }
1477
+ // Forward non-admission events to the consumer queue.
1478
+ consumerQueue.push(ev);
1479
+ }
1480
+ }
1481
+ catch (err) {
1482
+ // The wrap layer threw a typed `QodoServerError` (e.g.
1483
+ // `QodoAdmissionStalledError`) — propagate to the consumer
1484
+ // queue and settle deferreds. Same path covers transport
1485
+ // failures that surfaced as iterator throws.
1486
+ const wrapped = err instanceof Error ? err : new Error(String(err));
1487
+ if (!sessionIdDeferred.settled())
1488
+ sessionIdDeferred.reject(wrapped);
1489
+ if (!admittedTaskIdDeferred.settled())
1490
+ admittedTaskIdDeferred.reject(wrapped);
1491
+ if (!admissionResultDeferred.settled())
1492
+ admissionResultDeferred.reject(wrapped);
1493
+ consumerQueue.fail(wrapped);
1494
+ return;
1495
+ }
1496
+ if (admissionInProgress === undefined) {
1497
+ // Clean terminal (task.done) or non-admission error fell
1498
+ // through. Close the consumer queue.
1499
+ consumerQueue.close();
1500
+ return;
1501
+ }
1502
+ // Retry path
1503
+ const sid = admissionInProgress.session_id;
1504
+ if (typeof sid === 'string' && !isZeroUuid(sid))
1505
+ lastSessionId = sid;
1506
+ const hint = parseRetryAfterMsHint(admissionInProgress);
1507
+ const wait = hint ?? computeAdmissionRetryBackoffMs(attempt);
1508
+ const elapsed = Date.now() - startedAt;
1509
+ if (elapsed + wait > PENDING_ADMISSION_TIMEOUT_MS) {
1510
+ const timeoutErr = new QodoAdmissionTimeoutError(elapsed, attempt + 1, lastSessionId);
1511
+ if (!sessionIdDeferred.settled())
1512
+ sessionIdDeferred.reject(timeoutErr);
1513
+ if (!admittedTaskIdDeferred.settled())
1514
+ admittedTaskIdDeferred.reject(timeoutErr);
1515
+ if (!admissionResultDeferred.settled())
1516
+ admissionResultDeferred.reject(timeoutErr);
1517
+ consumerQueue.fail(timeoutErr);
1518
+ return;
1519
+ }
1520
+ await sleepMs(wait);
1521
+ if (driverAborted) {
1522
+ consumerQueue.close();
1523
+ return;
1524
+ }
1525
+ attempt += 1;
1526
+ currentSub = buildAttempt(asMessageId(uuidv7()));
1527
+ }
1528
+ };
1529
+ // Kick off the driver eagerly. Unhandled-rejection guard so a
1530
+ // driver-internal bug doesn't crash Node before the consumer
1531
+ // sees the queue's failure.
1532
+ void runDriver().catch((err) => {
1533
+ const wrapped = err instanceof Error ? err : new Error(String(err));
1534
+ if (!sessionIdDeferred.settled())
1535
+ sessionIdDeferred.reject(wrapped);
1536
+ if (!admittedTaskIdDeferred.settled())
1537
+ admittedTaskIdDeferred.reject(wrapped);
1538
+ if (!admissionResultDeferred.settled())
1539
+ admissionResultDeferred.reject(wrapped);
1540
+ if (!consumerQueue.isClosed)
1541
+ consumerQueue.fail(wrapped);
1542
+ });
1543
+ return {
1544
+ [Symbol.asyncIterator]() {
1545
+ return {
1546
+ next() {
1547
+ return consumerQueue.next();
1548
+ },
1549
+ async return(value) {
1550
+ abortFromConsumer();
1551
+ return { value: value, done: true };
1552
+ },
1553
+ async throw(err) {
1554
+ abortFromConsumer();
1555
+ throw err instanceof Error ? err : new Error(String(err));
1556
+ },
1557
+ };
1558
+ },
1559
+ };
1560
+ }
1561
+ /**
1562
+ * Wire an `AbortSignal` to the subscription. On abort, send `task.cancel`
1563
+ * (if we know the task id) and end the iterator. Returns a cleanup
1564
+ * function the subscription's `onClose` invokes so the listener doesn't
1565
+ * outlive the task (matters when a single AbortController is reused
1566
+ * across many short-lived tasks).
1567
+ */
1568
+ bindAbort(connection, sub, signal, sessionOverride) {
1569
+ const onAbort = () => {
1570
+ const taskId = sub.currentTaskId;
1571
+ if (taskId !== undefined && connection.isOpen) {
1572
+ try {
1573
+ // Forward the cold-address `sessionId` override. A cold
1574
+ // `task.continue` queued under `flow.pause` hasn't committed
1575
+ // its override to `taskSessions` yet when the abort fires;
1576
+ // the cancel needs the explicit value to resolve.
1577
+ connection.sendEnvelope({
1578
+ kind: 'task.cancel',
1579
+ payload: { task_id: taskId, reason: 'abort signal' },
1580
+ }, sessionOverride !== undefined ? { sessionId: sessionOverride } : undefined);
1581
+ }
1582
+ catch {
1583
+ // best-effort
1584
+ }
1585
+ }
1586
+ // `sub.close()` (NOT `sub.return()`) — `return()` invokes
1587
+ // `onEarlyReturn` which would emit a second `task.cancel`. The
1588
+ // abort path already sent the cancel above; closing the queue
1589
+ // ends the iterator without re-firing the early-return handler.
1590
+ sub.close();
1591
+ };
1592
+ signal.addEventListener('abort', onAbort, { once: true });
1593
+ return () => signal.removeEventListener('abort', onAbort);
1594
+ }
1595
+ // -------------------------------------------------------------------
1596
+ // Skills foundation glue
1597
+ // -------------------------------------------------------------------
1598
+ /**
1599
+ * Inject the rendered active-skills block + slim index into an
1600
+ * `InlineAgentSpec` synchronously. Validates caller-pinned skills
1601
+ * against the cached snapshot first; unresolved names throw
1602
+ * `SkillNotFoundError`. Returns the original spec when the catalog
1603
+ * is empty or when no `SkillsManager` is configured.
1604
+ *
1605
+ * When the caller pinned any skills, the SDK resolves the `requires:`
1606
+ * chain (with the depth/breadth/token caps applied), renders the
1607
+ * bodies into an `<active_skills>` block, and prepends that block to
1608
+ * the spec's `instructions` *before* the slim-index block. Trust
1609
+ * violations, cap breaches, cycles, or unresolved deps all throw
1610
+ * `QodoSkillError` so no partial body lands on the wire.
1611
+ */
1612
+ applyAgentSkillsInjection(spec, pinned, currentFile) {
1613
+ if (this.skills === undefined)
1614
+ return spec;
1615
+ const effectivePinned = this.resolveEffectivePinned(pinned);
1616
+ if (effectivePinned !== undefined && effectivePinned.length > 0) {
1617
+ this.validatePinnedSync(effectivePinned);
1618
+ }
1619
+ const activation = this.composeActiveSkills(effectivePinned);
1620
+ const renderOpts = this.buildRenderOptions(effectivePinned, currentFile);
1621
+ const injection = injectIntoAgentSpec(spec, this.skills, renderOpts);
1622
+ this.dispatchSkillsEvents(injection.events);
1623
+ this.throwIfPinsOmitted(injection.omittedPinned, renderOpts);
1624
+ this.dispatchSkillsEvents(activation.events);
1625
+ const finalSpec = activation.text.length > 0
1626
+ ? { ...injection.spec, instructions: prependInstructions(injection.spec.instructions, activation.text) }
1627
+ : injection.spec;
1628
+ return finalSpec;
1629
+ }
1630
+ /**
1631
+ * Inject the active-skills block + slim index into every inline-agent
1632
+ * node of an `InlineGraphSpec`. Mirrors `applyAgentSkillsInjection`
1633
+ * for the graph case.
1634
+ */
1635
+ applyGraphSkillsInjection(graph, pinned, currentFile) {
1636
+ if (this.skills === undefined)
1637
+ return graph;
1638
+ const effectivePinned = this.resolveEffectivePinned(pinned);
1639
+ if (effectivePinned !== undefined && effectivePinned.length > 0) {
1640
+ this.validatePinnedSync(effectivePinned);
1641
+ }
1642
+ const activation = this.composeActiveSkills(effectivePinned);
1643
+ const renderOpts = this.buildRenderOptions(effectivePinned, currentFile);
1644
+ const injection = injectIntoGraphSpec(graph, this.skills, renderOpts);
1645
+ this.dispatchSkillsEvents(injection.events);
1646
+ this.throwIfPinsOmitted(injection.omittedPinned, renderOpts);
1647
+ this.dispatchSkillsEvents(activation.events);
1648
+ if (activation.text.length === 0)
1649
+ return injection.spec;
1650
+ return {
1651
+ ...injection.spec,
1652
+ agents: prependActiveSkillsToGraphAgents(injection.spec.agents, activation.text),
1653
+ };
1654
+ }
1655
+ /**
1656
+ * MCP projection at the wire boundary. When the consumer set
1657
+ * `mcpTools` (or `mcpToolOverrides`), this:
1658
+ *
1659
+ * 1. Awaits the MCP pool's catalog-settle window so cold-start
1660
+ * `tools/list` round-trips can complete before projection.
1661
+ * 2. Runs `projectMcpTools` against the live pool snapshot.
1662
+ * 3. Partitions the diagnostics:
1663
+ * - `unknownMcps` and `unknownMcpTools` always reject (consumer
1664
+ * code drift; pre-deploy-detectable).
1665
+ * - `unreachableMcps` is policed per-MCP via the registry's
1666
+ * `unavailability` setting. `'fail'` raises
1667
+ * `QodoMcpUnavailableError`; `'warn'` omits the MCP's tools
1668
+ * and emits a console warning.
1669
+ * 4. Returns a fresh `InlineAgentSpec` with `tools[]` replaced by
1670
+ * the projected surface and `mcpTools` / `mcpToolOverrides`
1671
+ * stripped (SDK-only fields — QAR rejects them at the wire
1672
+ * validator via `D10-R5` if they slipped through).
1673
+ *
1674
+ * When the spec carries neither `mcpTools` nor `mcpToolOverrides`, the
1675
+ * helper returns the input verbatim — projection is opt-in, the v1
1676
+ * `tools: FunctionToolDef[]` ergonomic continues to work.
1677
+ */
1678
+ async projectMcpToolsForWire(spec) {
1679
+ const hasSelector = spec.mcpTools !== undefined;
1680
+ const hasOverrides = spec.mcpToolOverrides !== undefined;
1681
+ // Auto-include the `qodo-skills.*` tool surface whenever the
1682
+ // consumer opted into skills. The slim-index preamble (rendered into
1683
+ // `instructions` by the skills injector) tells the LLM "Use the
1684
+ // qodo-skills.get_skill tool to ..."; that instruction is only
1685
+ // actionable when the three tools actually land on the wire's
1686
+ // `function_tools`. Projection is the SDK's single surface that
1687
+ // decides what tools the model can see, so it owns the include.
1688
+ //
1689
+ // No skills + no selector + no overrides → projection is a no-op;
1690
+ // return verbatim to preserve the v1 ergonomic where
1691
+ // `tools: FunctionToolDef[]` reaches the wire untouched.
1692
+ const skillsConfigured = this.skills !== undefined;
1693
+ if (!hasSelector && !hasOverrides && !skillsConfigured)
1694
+ return spec;
1695
+ // Build prefix policy + unavailability policy from the registry
1696
+ // snapshot. The registry's `getMcpPolicy` returns defaults
1697
+ // (`prefixToolNames: true`, `unavailability: 'fail'`) for any
1698
+ // unregistered name, so passing the full registered set here is
1699
+ // sufficient; the projection function ignores entries not in the
1700
+ // pool snapshot.
1701
+ const prefixPolicy = new Map();
1702
+ const unavailabilityByName = new Map();
1703
+ for (const { name } of this.registry.listMcps()) {
1704
+ const policy = this.registry.getMcpPolicy(name);
1705
+ prefixPolicy.set(name, policy.prefixToolNames);
1706
+ unavailabilityByName.set(name, policy.unavailability);
1707
+ }
1708
+ // Acquire the live pool — fall back to a fresh empty pool when no
1709
+ // remote/stdio MCPs are registered so the projection function
1710
+ // doesn't need a null-pool branch. An empty pool yields
1711
+ // empty registered-names + empty catalog snapshot; the selector's
1712
+ // unknown-MCP diagnostics then flag every referenced name.
1713
+ const livePool = this.registry._getMcpPool();
1714
+ const pool = livePool ?? new McpClientPool();
1715
+ if (livePool !== null) {
1716
+ // Bounded wait — `waitForCatalogsToSettle` defaults to 250ms.
1717
+ // Catalogs already populated resolve immediately. Cold-start
1718
+ // failures (Promise rejection) are swallowed by the settle
1719
+ // helper; they surface here as `unreachableMcps`.
1720
+ await livePool.waitForCatalogsToSettle();
1721
+ }
1722
+ const projectionInput = {
1723
+ mcpTools: spec.mcpTools,
1724
+ consumerTools: spec.tools ?? undefined,
1725
+ mcpToolOverrides: spec
1726
+ .mcpToolOverrides,
1727
+ };
1728
+ // SDK-transport ('sdk') MCPs aren't in the pool — their dispatch
1729
+ // goes through the in-process handler bridge. Pass their names as
1730
+ // "extra known" so referencing them in `mcpTools` doesn't trip
1731
+ // `QodoUnknownMcpError`. Projection still won't auto-project tools
1732
+ // for them (no tools/list contract); consumer ships those via
1733
+ // `tools[]`.
1734
+ const sdkTransportNames = new Set(this.registry._listSdkTransportMcpNames());
1735
+ const result = projectMcpTools(projectionInput, pool, prefixPolicy, sdkTransportNames);
1736
+ // Partition diagnostics. Unknown MCPs / tools are unconditional
1737
+ // throws — they indicate consumer code drift, not transient flake.
1738
+ if (result.diagnostics.unknownMcps.length > 0) {
1739
+ throw new QodoUnknownMcpError(result.diagnostics.unknownMcps);
1740
+ }
1741
+ if (result.diagnostics.unknownMcpTools.length > 0) {
1742
+ throw new QodoUnknownMcpToolError(result.diagnostics.unknownMcpTools);
1743
+ }
1744
+ // Per-MCP unavailability policy. `'fail'` names collect into the
1745
+ // typed error payload; `'warn'` names emit a structured console
1746
+ // warning so observability consumers see the degradation without
1747
+ // the task failing.
1748
+ const failNames = [];
1749
+ const warnNames = [];
1750
+ for (const name of result.diagnostics.unreachableMcps) {
1751
+ const policy = unavailabilityByName.get(name) ?? 'fail';
1752
+ if (policy === 'fail')
1753
+ failNames.push(name);
1754
+ else
1755
+ warnNames.push(name);
1756
+ }
1757
+ if (warnNames.length > 0) {
1758
+ // Structured-payload warning. Stable JSON shape so
1759
+ // `gtrace`-style log aggregators parse cleanly; one
1760
+ // line per unreachable MCP so each event is independently
1761
+ // grep-able. `event` is the stable discriminator;
1762
+ // `reason` distinguishes cold-start (only case in v1) from a
1763
+ // future post-cache-disconnect surface.
1764
+ //
1765
+ // The typed-event-handler surface
1766
+ // (`QodoClientOptions.onMcpUnavailable`) is a follow-up — so
1767
+ // consumers wanting first-class observability can grep this
1768
+ // payload until that lands.
1769
+ const ts = new Date().toISOString();
1770
+ for (const name of warnNames) {
1771
+ // eslint-disable-next-line no-console
1772
+ console.warn(JSON.stringify({
1773
+ event: 'qodo.sdk.mcp.unavailable',
1774
+ mcpName: name,
1775
+ ts,
1776
+ reason: 'cold_start_unreachable',
1777
+ hint: "Check MCP connectivity; tools omitted from this task's projection.",
1778
+ }));
1779
+ }
1780
+ }
1781
+ if (failNames.length > 0) {
1782
+ throw new QodoMcpUnavailableError(failNames);
1783
+ }
1784
+ // Auto-include the qodo-skills.* tool surface when skills are
1785
+ // configured. The 3 entries are appended AFTER projection so a
1786
+ // consumer-shipped tool with a colliding name (rare — consumers
1787
+ // shouldn't author tools named `qodo-skills.*`) still wins via the
1788
+ // dedup-by-name pass below, mirroring the projection's
1789
+ // "consumer-shipped wins on collision" invariant.
1790
+ //
1791
+ // Whether the consumer also referenced `qodo-skills` in their
1792
+ // `mcpTools.allowlist` doesn't matter — the projection step above
1793
+ // can't synthesize tools for an sdk-transport MCP (no `tools/list`
1794
+ // catalog), so we'd land here with the 3 entries missing regardless.
1795
+ // De-dup-by-name guards against the unlikely future where projection
1796
+ // grows an sdk-transport bridge that DOES emit the qodo-skills
1797
+ // catalog; we'd still ship exactly three entries.
1798
+ const projectedTools = skillsConfigured
1799
+ ? mergeQodoSkillsTools(result.tools)
1800
+ : result.tools;
1801
+ // Strip the SDK-only fields and replace `tools[]` with the projected
1802
+ // surface. Object spread + delete keeps the original spec immutable
1803
+ // (consumers may hold references for retry / display).
1804
+ const stripped = stripMcpToolsFromSpec(spec);
1805
+ return { ...stripped, tools: projectedTools };
1806
+ }
1807
+ /**
1808
+ * MCP projection over an `InlineGraphSpec`. Walks every node whose
1809
+ * `spec` is an inline `InlineAgentSpec` and applies
1810
+ * {@link projectMcpToolsForWire} to it; static refs and nested
1811
+ * subgraphs are recursed into. Returns a fresh `InlineGraphSpec` with
1812
+ * the same shape but every per-node `tools[]` replaced by the
1813
+ * projected surface. Diagnostic errors propagate from the first
1814
+ * node that fails — earlier nodes have already projected
1815
+ * successfully, but the wire write hasn't started so the early
1816
+ * partial work is invisible.
1817
+ */
1818
+ async projectMcpToolsForGraphWire(graph) {
1819
+ const agentEntries = Object.entries(graph.agents);
1820
+ const newAgents = {};
1821
+ let touched = false;
1822
+ for (const [name, agentNode] of agentEntries) {
1823
+ const node = agentNode;
1824
+ const inner = node.spec;
1825
+ if (isInlineAgentNode(inner)) {
1826
+ const projected = await this.projectMcpToolsForWire(inner);
1827
+ if (projected !== inner) {
1828
+ newAgents[name] = { ...node, spec: projected };
1829
+ touched = true;
1830
+ }
1831
+ else {
1832
+ newAgents[name] = node;
1833
+ }
1834
+ }
1835
+ else if (isInlineGraphNode(inner)) {
1836
+ const projectedGraph = await this.projectMcpToolsForGraphWire(inner);
1837
+ if (projectedGraph !== inner) {
1838
+ newAgents[name] = { ...node, spec: projectedGraph };
1839
+ touched = true;
1840
+ }
1841
+ else {
1842
+ newAgents[name] = node;
1843
+ }
1844
+ }
1845
+ else {
1846
+ newAgents[name] = node;
1847
+ }
1848
+ }
1849
+ if (!touched)
1850
+ return graph;
1851
+ return { ...graph, agents: newAgents };
1852
+ }
1853
+ /**
1854
+ * Resolve the caller-pinned roots into a rendered active-skills block
1855
+ * + telemetry events. Pure — relies on the already-loaded snapshot
1856
+ * (callers await `manager.discover()` before getting here). Returns
1857
+ * an empty result when no pins are effective.
1858
+ */
1859
+ composeActiveSkills(effectivePinned) {
1860
+ if (this.skills === undefined)
1861
+ return { text: '', skills: [], events: [] };
1862
+ if (effectivePinned === undefined || effectivePinned.length === 0) {
1863
+ return { text: '', skills: [], events: [] };
1864
+ }
1865
+ const snapshot = this.skills.currentSnapshot;
1866
+ if (snapshot === null) {
1867
+ // Defensive — the public entry points await discover() before this
1868
+ // is invoked. Falling through with an empty block keeps the wire
1869
+ // payload coherent if a future refactor changes the call order.
1870
+ return { text: '', skills: [], events: [] };
1871
+ }
1872
+ const lookup = lookupFromSnapshot(snapshot.skills, this.skills);
1873
+ const roots = [];
1874
+ const seen = new Set();
1875
+ for (const spec of effectivePinned) {
1876
+ const skill = lookup.get(spec);
1877
+ if (skill === null) {
1878
+ // validatePinnedSync would normally catch this; throw the same
1879
+ // typed error in case it slipped through.
1880
+ throw new SkillNotFoundError(spec);
1881
+ }
1882
+ if (seen.has(skill.fqn))
1883
+ continue;
1884
+ seen.add(skill.fqn);
1885
+ roots.push(skill);
1886
+ }
1887
+ const resolution = resolveAllActivations(roots, lookup);
1888
+ if (resolution.outcome === 'failed') {
1889
+ this.dispatchSkillsEvents(resolution.events);
1890
+ const failureReason = mapFailureReason(resolution.failure.result.outcome);
1891
+ throw new QodoSkillError(resolution.failure.root.fqn, failureReason, activationFailureMessage(resolution.failure.root, resolution.failure.result));
1892
+ }
1893
+ const rendered = renderActiveSkillsBlock(resolution.ordered, this.skills);
1894
+ // Deliberately do NOT call `manager.markActive(skill.fqn)` here.
1895
+ //
1896
+ // `composeActiveSkills` runs **before** the wire write is guaranteed
1897
+ // to succeed. The preflight HTTP call can still reject
1898
+ // (`agent_spec_rejected`), and the WebSocket `task.start` write can
1899
+ // still fail. Mutating the shared `activeFqns` set here would leak
1900
+ // past those failure modes — the next LLM-driven `get_skill` for
1901
+ // the same FQN would see the stale entry and suppress the
1902
+ // `sdk.skill.activated { source: 'llm' }` event (the MCP server's
1903
+ // `newlyActive` branch). Telemetry would then under-count fresh
1904
+ // activations, and the active set would claim skills the model has
1905
+ // not actually loaded.
1906
+ //
1907
+ // The caller-pin activation signal is the
1908
+ // `sdk.skill.activated { source: 'caller' }` event the renderer
1909
+ // emits — that's what consumers wire for "this skill is now part of
1910
+ // the conversation". `getActiveSkills()` and
1911
+ // `skillsMcpServer.loadedSkillFqns` reflect the strictly per-client
1912
+ // LLM-driven `get_skill` history, which is what the body cache
1913
+ // actually maps to.
1914
+ return rendered;
1915
+ }
1916
+ /**
1917
+ * Surface a caller-pin overflow as a loud failure. The renderer drops
1918
+ * pinned skills that would push the block past its hard cap
1919
+ * (`2 × charBudget`, floored at `charBudget + 4096`). Silently
1920
+ * truncating the wire payload would make the model behave as if the
1921
+ * caller never pinned those skills — throw instead so the caller
1922
+ * fixes the pin list or raises the budget.
1923
+ */
1924
+ throwIfPinsOmitted(omitted, renderOpts) {
1925
+ if (omitted.length === 0)
1926
+ return;
1927
+ const budget = renderOpts.charBudget ?? 8000;
1928
+ const hardCap = Math.max(budget * 2, budget + 4096);
1929
+ throw new SkillsBudgetExceededError(omitted.map((s) => s.fqn), hardCap);
1930
+ }
1931
+ /**
1932
+ * Resolve per-call pinned skills against the constructor-level
1933
+ * `SkillsConfig.activate` default:
1934
+ *
1935
+ * - `opts.skills === undefined` → fall back to the constructor's
1936
+ * `activate` (or `undefined` if none).
1937
+ * - `opts.skills === []` → explicit empty, no fallback.
1938
+ * - `opts.skills === [...]` → per-call wins outright.
1939
+ */
1940
+ resolveEffectivePinned(perCall) {
1941
+ if (perCall !== undefined)
1942
+ return perCall;
1943
+ if (this.skills === undefined)
1944
+ return undefined;
1945
+ return this.skills.defaultActivate;
1946
+ }
1947
+ buildRenderOptions(pinned, currentFile) {
1948
+ const opts = {};
1949
+ if (pinned !== undefined) {
1950
+ opts.pinned = pinned;
1951
+ }
1952
+ if (currentFile !== undefined) {
1953
+ opts.currentFilePath = currentFile;
1954
+ }
1955
+ // Forward the constructor-level `indexCharBudget` so consumers who
1956
+ // configured a tighter budget actually see it applied.
1957
+ //
1958
+ // Validate before forwarding: `NaN`, `Infinity`, or non-positive
1959
+ // values would break `Math.max()` and `>` comparisons inside the
1960
+ // renderer's hard-cap logic (`x > NaN` is always false, so the cap
1961
+ // never fires). On invalid input, ignore and let the renderer fall
1962
+ // back to `DEFAULT_INDEX_CHAR_BUDGET`.
1963
+ if (this.skills !== undefined) {
1964
+ const budget = this.skills.indexCharBudget;
1965
+ if (budget !== undefined && Number.isFinite(budget) && budget > 0) {
1966
+ opts.charBudget = Math.floor(budget);
1967
+ }
1968
+ }
1969
+ return opts;
1970
+ }
1971
+ /**
1972
+ * Validate caller-pinned specifiers against the manager's already-loaded
1973
+ * snapshot. Throws `SkillNotFoundError` on a miss and
1974
+ * `SkillAmbiguousPinError` on a bare-name pin that matches multiple
1975
+ * vendors. Assumes the snapshot is present (the public entry-points
1976
+ * await `discover()` before calling this helper).
1977
+ */
1978
+ validatePinnedSync(pinned) {
1979
+ if (this.skills === undefined)
1980
+ return;
1981
+ const snapshot = this.skills.currentSnapshot;
1982
+ if (snapshot === null)
1983
+ return;
1984
+ for (const spec of pinned) {
1985
+ const result = resolveSpecifierInList(snapshot.skills, spec);
1986
+ if (result.outcome === 'not_found') {
1987
+ throw new SkillNotFoundError(spec);
1988
+ }
1989
+ if (result.outcome === 'ambiguous') {
1990
+ throw new SkillAmbiguousPinError(spec, result.candidates);
1991
+ }
1992
+ }
1993
+ }
1994
+ /** Forward renderer telemetry events to the manager's configured sink. */
1995
+ dispatchSkillsEvents(events) {
1996
+ if (this.skills === undefined || events.length === 0)
1997
+ return;
1998
+ this.skills.forwardEvents(events);
1999
+ }
2000
+ }
2001
+ /**
2002
+ * Prepend the rendered active-skills block to an `Instructions` field
2003
+ * (string or preset). For preset: write to `append` so the QAR-side prompt
2004
+ * builder concatenates after the preset body. The active block lands BEFORE
2005
+ * the existing append content + slim-index block, since the bodies are
2006
+ * authoritative.
2007
+ */
2008
+ function prependInstructions(current, blockText) {
2009
+ if (typeof current === 'string') {
2010
+ if (current.length === 0)
2011
+ return blockText;
2012
+ return `${blockText}\n\n${current}`;
2013
+ }
2014
+ const existingAppend = current.append ?? '';
2015
+ const newAppend = existingAppend.length === 0
2016
+ ? blockText
2017
+ : `${blockText}\n\n${existingAppend}`;
2018
+ return { preset: current.preset, append: newAppend };
2019
+ }
2020
+ /**
2021
+ * Walk a graph's agents map and prepend the active-skills block to every
2022
+ * inline `AgentSpec`. Static-ref nodes are returned untouched (no
2023
+ * instructions surface on the wire). Sub-graphs recurse.
2024
+ */
2025
+ function prependActiveSkillsToGraphAgents(agents, blockText) {
2026
+ const out = {};
2027
+ for (const [name, agent] of Object.entries(agents)) {
2028
+ out[name] = prependToGraphAgent(agent, blockText);
2029
+ }
2030
+ return out;
2031
+ }
2032
+ function prependToGraphAgent(agent, blockText) {
2033
+ const inner = agent.spec;
2034
+ if (typeof inner !== 'object' || inner === null || !('kind' in inner)) {
2035
+ return agent;
2036
+ }
2037
+ if (inner.kind === 'AgentSpec') {
2038
+ return {
2039
+ ...agent,
2040
+ spec: { ...inner, instructions: prependInstructions(inner.instructions, blockText) },
2041
+ };
2042
+ }
2043
+ if (inner.kind === 'GraphSpec') {
2044
+ return {
2045
+ ...agent,
2046
+ spec: { ...inner, agents: prependActiveSkillsToGraphAgents(inner.agents, blockText) },
2047
+ };
2048
+ }
2049
+ return agent;
2050
+ }
2051
+ /**
2052
+ * Map the resolver's failure outcome onto the typed `QodoSkillError.reason`
2053
+ * field. The two enums diverge only in the `'ok'` variant (filtered out
2054
+ * before this is called).
2055
+ */
2056
+ function mapFailureReason(outcome) {
2057
+ return outcome;
2058
+ }
2059
+ /**
2060
+ * Synchronous specifier resolver used by `TaskClient` for caller-pinned
2061
+ * validation. Mirrors `SkillsManager.get` for the cached-snapshot case,
2062
+ * plus an ambiguity check: a bare name that matches multiple
2063
+ * `fqnNoVersion` values returns `ambiguous` so the caller surfaces a
2064
+ * loud `SkillAmbiguousPinError` rather than silently picking one.
2065
+ */
2066
+ function resolveSpecifierInList(skills, spec) {
2067
+ const versionMatch = /^(.+?)@([^@]+)$/.exec(spec);
2068
+ if (versionMatch !== null) {
2069
+ const fqnNoVersion = versionMatch[1];
2070
+ const version = versionMatch[2];
2071
+ return skills.some((s) => s.fqnNoVersion === fqnNoVersion && s.version === version)
2072
+ ? { outcome: 'found' }
2073
+ : { outcome: 'not_found' };
2074
+ }
2075
+ if (spec.includes('/')) {
2076
+ return skills.some((s) => s.fqnNoVersion === spec)
2077
+ ? { outcome: 'found' }
2078
+ : { outcome: 'not_found' };
2079
+ }
2080
+ const distinctFqnNoVersion = new Set();
2081
+ for (const s of skills) {
2082
+ if (s.name === spec)
2083
+ distinctFqnNoVersion.add(s.fqnNoVersion);
2084
+ }
2085
+ if (distinctFqnNoVersion.size === 0)
2086
+ return { outcome: 'not_found' };
2087
+ if (distinctFqnNoVersion.size === 1)
2088
+ return { outcome: 'found' };
2089
+ return {
2090
+ outcome: 'ambiguous',
2091
+ candidates: [...distinctFqnNoVersion].sort(),
2092
+ };
2093
+ }
2094
+ /**
2095
+ * Parse a server `error` envelope into the appropriate typed error class:
2096
+ *
2097
+ * - `agent_spec_rejected` → `QodoAgentSpecRejectedError` (carries
2098
+ * `issues: InlineAgentSpecIssue[]`).
2099
+ * - Any code recognized by `classForServerErrorCode` →
2100
+ * the matching `QodoServerError` subclass.
2101
+ * - Anything else → `QodoUnknownServerError` so consumers' `catch
2102
+ * (err: QodoServerError)` blocks still see structured fields.
2103
+ *
2104
+ * The wire `payload.errors` array is duck-typed (codegen still narrows
2105
+ * to the base `ErrorPayload` shape without the `errors` extension), so
2106
+ * absent/malformed entries default to an empty array and the typed
2107
+ * error class carries just `code` + `message`.
2108
+ */
2109
+ function errorFromServerErrorEnvelope(envelope) {
2110
+ const payload = envelope.payload;
2111
+ if (payload.code === 'agent_spec_rejected') {
2112
+ return errorFromAgentSpecRejection(envelope);
2113
+ }
2114
+ const entries = [];
2115
+ if (Array.isArray(payload.errors)) {
2116
+ for (const entry of payload.errors) {
2117
+ if (entry === null || typeof entry !== 'object')
2118
+ continue;
2119
+ const rule_id = typeof entry.rule_id === 'string' ? entry.rule_id : payload.code;
2120
+ const path = typeof entry.path === 'string' ? entry.path : '';
2121
+ const message = typeof entry.message === 'string' ? entry.message : (payload.message ?? '');
2122
+ entries.push({ rule_id, path, message });
2123
+ }
2124
+ }
2125
+ const offendingMessageId = typeof payload.offending_message_id === 'string' ? payload.offending_message_id : undefined;
2126
+ const message = payload.message ?? `server error: ${payload.code}`;
2127
+ const Cls = classForServerErrorCode(payload.code);
2128
+ if (Cls !== undefined) {
2129
+ const err = new Cls(message, entries, offendingMessageId);
2130
+ // Attach the envelope's inherited `session_id` to
2131
+ // `QodoAdmissionStalledError` so consumers reading the typed throw
2132
+ // can pull the derived session UUID without dipping into the
2133
+ // envelope. The ErrorEnv carries the derived `session_id` for
2134
+ // post-derivation failures (admission_stalled is one) — anything
2135
+ // but the all-zero pre-bind UUID is a valid scope.
2136
+ if (err instanceof QodoAdmissionStalledError) {
2137
+ const sid = envelope.session_id;
2138
+ if (typeof sid === 'string' && sid.length > 0 && !isZeroUuid(sid)) {
2139
+ err.attachSessionId(sid);
2140
+ }
2141
+ }
2142
+ return err;
2143
+ }
2144
+ return new QodoUnknownServerError(payload.code, message, entries, offendingMessageId);
2145
+ }
2146
+ /**
2147
+ * Tell the pre-bind zero-UUID (`00000000-0000-0000-0000-000000000000`)
2148
+ * apart from a real derived session_id. The wire emits the zero-UUID on
2149
+ * pre-derivation errors (envelope parse, auth rejection, invalid
2150
+ * `idempotency_key`); post-derivation errors (`admission_stalled`,
2151
+ * `admission_in_progress`, etc.) carry the real derived UUID. Don't
2152
+ * surface the zero-UUID through typed errors — it's not a routable
2153
+ * address.
2154
+ */
2155
+ function isZeroUuid(value) {
2156
+ return value === '00000000-0000-0000-0000-000000000000';
2157
+ }
2158
+ /**
2159
+ * Parse QAR's `agent_spec_rejected` envelope into a typed
2160
+ * `QodoAgentSpecRejectedError`. The envelope's `payload` carries the
2161
+ * structured `errors: [{ rule_id, path, message }]` array; codegen
2162
+ * types the payload as the base `ErrorPayload` (`{ code, message,
2163
+ * offending_message_id? }`) without the `errors` extension, so the
2164
+ * parse here is duck-typed: we read `errors` if present, otherwise
2165
+ * fall back to a single-entry list with the envelope's `code` as
2166
+ * `rule_id`.
2167
+ */
2168
+ function errorFromAgentSpecRejection(envelope) {
2169
+ const payload = envelope.payload;
2170
+ const issues = [];
2171
+ if (Array.isArray(payload.errors)) {
2172
+ for (const entry of payload.errors) {
2173
+ if (entry === null || typeof entry !== 'object')
2174
+ continue;
2175
+ const rule_id = typeof entry.rule_id === 'string' ? entry.rule_id : payload.code;
2176
+ const path = typeof entry.path === 'string' ? entry.path : '';
2177
+ const message = typeof entry.message === 'string' ? entry.message : (payload.message ?? '');
2178
+ issues.push({ rule_id, path, message });
2179
+ }
2180
+ }
2181
+ if (issues.length === 0) {
2182
+ // Envelope didn't carry the structured `errors` array — synthesize a
2183
+ // single entry from the top-level `code` + `message` so the typed error
2184
+ // always carries at least one issue (the constructor's invariant).
2185
+ issues.push({
2186
+ rule_id: payload.code,
2187
+ path: '',
2188
+ message: payload.message ?? `Inline AgentSpec rejected with code ${payload.code}`,
2189
+ });
2190
+ }
2191
+ return new QodoAgentSpecRejectedError(issues);
2192
+ }
2193
+ /**
2194
+ * Yield events from a `Promise<TaskSubscription>`. Used by the preflighted
2195
+ * `tasks.startWith*` paths so the preflight HTTP call + the wire
2196
+ * `task.start` write can be kicked off eagerly in the public method
2197
+ * (closing the cancel-race-during-preflight foot-gun), while the
2198
+ * returned iterable still lazily yields events the consumer iterates
2199
+ * through.
2200
+ *
2201
+ * `wrapServerErrors` flips on so a server-side `error` envelope arriving
2202
+ * on the subscription surfaces as a typed `QodoServerError` (or
2203
+ * `QodoAgentSpecRejectedError` for the spec-rejection carve-out)
2204
+ * instead of a yielded `kind: 'error'` event.
2205
+ */
2206
+ async function* yieldFromPromisedSub(subPromise, wrapServerErrors) {
2207
+ const sub = await subPromise;
2208
+ for await (const event of sub) {
2209
+ if (wrapServerErrors && event.kind === 'error') {
2210
+ const code = event.payload.code;
2211
+ if (typeof code === 'string' && isTypedErrorCode(code)) {
2212
+ throw errorFromServerErrorEnvelope(event);
2213
+ }
2214
+ }
2215
+ yield event;
2216
+ }
2217
+ }
2218
+ /**
2219
+ * Whether the SDK has a typed `QodoServerError` subclass (or
2220
+ * `QodoAgentSpecRejectedError`) for this wire code. Used by the
2221
+ * iterator wrappers to decide whether to throw vs yield the
2222
+ * `kind: 'error'` event — codes without a typed class flow through
2223
+ * unchanged so existing consumer narrowing keeps working.
2224
+ */
2225
+ function isTypedErrorCode(code) {
2226
+ if (code === 'agent_spec_rejected')
2227
+ return true;
2228
+ return classForServerErrorCode(code) !== undefined;
2229
+ }
2230
+ /**
2231
+ * Decorate an `AsyncIterable<TaskEvent>` with the synchronous `taskId` and
2232
+ * the `sessionId` Promise so consumers can read both before iterating.
2233
+ *
2234
+ * `taskId` is the SDK-derived value — `task_id == task.start.message_id`
2235
+ * — available at envelope mint time.
2236
+ *
2237
+ * `sessionId` is the server-derived UUID arriving on the `task.started`
2238
+ * admission ack; the Promise resolves when the ack lands and rejects if
2239
+ * the subscription terminates without admission.
2240
+ *
2241
+ * Returns a thin wrapper object that delegates `[Symbol.asyncIterator]`
2242
+ * to the underlying iterable — we deliberately don't mutate `iter`.
2243
+ */
2244
+ function attachTaskId(iter, taskId, admittedTaskId, sessionId, admissionResult) {
2245
+ return {
2246
+ [Symbol.asyncIterator]: () => iter[Symbol.asyncIterator](),
2247
+ taskId,
2248
+ admittedTaskId,
2249
+ sessionId,
2250
+ admissionResult,
2251
+ };
2252
+ }
2253
+ /**
2254
+ * Cap on the SDK's `admission_in_progress` retry budget. Mirrors
2255
+ * `PENDING_ADMISSION_TIMEOUT` on the server side. After this wall-clock
2256
+ * window the SDK gives up retrying and throws
2257
+ * {@link QodoAdmissionTimeoutError}. Operator-facing default; consumers
2258
+ * who need a different cap can wrap the SDK call themselves.
2259
+ */
2260
+ const PENDING_ADMISSION_TIMEOUT_MS = 5 * 60 * 1000;
2261
+ /** Initial backoff for `admission_in_progress` retries when the server omits `retry_after_ms`. */
2262
+ const ADMISSION_RETRY_INITIAL_MS = 100;
2263
+ function createDeferred() {
2264
+ let resolve;
2265
+ let reject;
2266
+ let done = false;
2267
+ const promise = new Promise((res, rej) => {
2268
+ resolve = (v) => {
2269
+ if (done)
2270
+ return;
2271
+ done = true;
2272
+ res(v);
2273
+ };
2274
+ reject = (e) => {
2275
+ if (done)
2276
+ return;
2277
+ done = true;
2278
+ rej(e);
2279
+ };
2280
+ });
2281
+ // Default unhandled-rejection guard — the deferred may reject before any
2282
+ // consumer awaits `promise`. Without this attach Node logs an
2283
+ // `unhandledRejection` warning between rejection time and the consumer's
2284
+ // `await stream.sessionId` (which still re-surfaces the error).
2285
+ promise.catch(() => undefined);
2286
+ return { promise, resolve, reject, settled: () => done };
2287
+ }
2288
+ /**
2289
+ * Backoff schedule for `admission_in_progress` retries when the server
2290
+ * omits `retry_after_ms`. Exponential doubling from
2291
+ * `ADMISSION_RETRY_INITIAL_MS`, capped at the operator timeout. ±10%
2292
+ * multiplicative jitter so a pod stampede doesn't synchronize retry
2293
+ * waves.
2294
+ *
2295
+ * Pure function — `Math.random()` is the only impurity and is the source
2296
+ * of the jitter; tests can disable jitter by hand-rolling a wait (the
2297
+ * SDK doesn't expose a seam to override the RNG, matching the rest of
2298
+ * the codebase's transport timers).
2299
+ */
2300
+ function computeAdmissionRetryBackoffMs(attempt) {
2301
+ // Apply the cap AFTER jitter so the return value can NEVER exceed
2302
+ // `PENDING_ADMISSION_TIMEOUT_MS`. Capping `base` first and then adding
2303
+ // jitter on top would let the result drift up to +10% above the
2304
+ // timeout.
2305
+ const base = ADMISSION_RETRY_INITIAL_MS * Math.pow(2, attempt);
2306
+ const jitter = base * 0.1 * Math.random();
2307
+ return Math.floor(Math.min(base + jitter, PENDING_ADMISSION_TIMEOUT_MS));
2308
+ }
2309
+ /**
2310
+ * Read the server's `retry_after_ms` hint off an `admission_in_progress`
2311
+ * envelope. The field is optional + non-negative integer milliseconds;
2312
+ * ill-typed values fall back to `undefined` so the caller uses the SDK's
2313
+ * exponential backoff.
2314
+ */
2315
+ function parseRetryAfterMsHint(env) {
2316
+ const v = env.payload.retry_after_ms;
2317
+ if (typeof v === 'number' && Number.isFinite(v) && v >= 0)
2318
+ return Math.floor(v);
2319
+ return undefined;
2320
+ }
2321
+ /** Promise-returning setTimeout — distinct fn name keeps the call sites greppable. */
2322
+ function sleepMs(ms) {
2323
+ return new Promise((resolve) => setTimeout(resolve, ms));
2324
+ }
2325
+ /**
2326
+ * Client-side `idempotencyKey` validation — mirrors QAR's wire validator
2327
+ * (`Field(min_length=1, max_length=512, pattern=r"^[^\x00]+$")` on a
2328
+ * Pydantic `str`) so a bad value fails fast on the SDK side rather than
2329
+ * waiting for the server to round-trip an error envelope. Length is
2330
+ * measured in **Unicode code points** (`[...key].length`), matching
2331
+ * Pydantic's `Field` semantics on `str` — NOT UTF-16 code units (which
2332
+ * `key.length` would give) and NOT UTF-8 bytes (relevant for the NUL
2333
+ * exclusion, where the wire validator's `[^\x00]+` pattern matches code
2334
+ * points and would not be fooled by a stray `U+0000`).
2335
+ *
2336
+ * Throws {@link QodoIdempotencyKeyValidationError} with a discriminated
2337
+ * `reason` so callers can branch without parsing the message.
2338
+ *
2339
+ * **Observability hygiene**: NEVER log the raw key from this function or
2340
+ * its call sites. The thrown error carries the code-point count, not the
2341
+ * value; consumers handling the error are responsible for keeping the
2342
+ * original input out of operator-facing surfaces.
2343
+ */
2344
+ function validateIdempotencyKey(key) {
2345
+ // Spread iterates code points (handles surrogate pairs correctly);
2346
+ // `String.prototype.length` would count UTF-16 code units and miscount
2347
+ // anything outside the BMP. The wire limit is 512 **code points**.
2348
+ const codePoints = [...key].length;
2349
+ if (codePoints === 0) {
2350
+ throw new QodoIdempotencyKeyValidationError('too_short', 0);
2351
+ }
2352
+ if (codePoints > 512) {
2353
+ throw new QodoIdempotencyKeyValidationError('too_long', codePoints);
2354
+ }
2355
+ // NUL exclusion — mirrors QAR's `pattern=r"^[^\x00]+$"` validator.
2356
+ // We iterate code points (not UTF-16 code units) for consistency with
2357
+ // the length check above; `codePointAt(0) === 0` tests the U+0000 case
2358
+ // without embedding a NUL literal in source.
2359
+ for (const cp of key) {
2360
+ if (cp.codePointAt(0) === 0) {
2361
+ throw new QodoIdempotencyKeyValidationError('contains_nul', codePoints);
2362
+ }
2363
+ }
2364
+ }
2365
+ // ---------------------------------------------------------------------------
2366
+ // MCP projection helpers
2367
+ // ---------------------------------------------------------------------------
2368
+ /**
2369
+ * Discriminate an `InlineGraphAgent.spec` union arm — `true` for the
2370
+ * inline `InlineAgentSpec` branch (matched by `kind` literal or absence
2371
+ * of the `agent_id` static-ref field). Mirrors the runtime discriminator
2372
+ * in `inlineGraph.ts` so the projection graph walker doesn't have to
2373
+ * import the private helper.
2374
+ */
2375
+ function isInlineAgentNode(spec) {
2376
+ if (typeof spec !== 'object' || spec === null)
2377
+ return false;
2378
+ if (spec.agent_id !== undefined)
2379
+ return false;
2380
+ const kind = spec.kind;
2381
+ return kind === 'AgentSpec' || kind === undefined;
2382
+ }
2383
+ /** Discriminate the nested-subgraph arm of `InlineGraphAgent.spec`. */
2384
+ function isInlineGraphNode(spec) {
2385
+ if (typeof spec !== 'object' || spec === null)
2386
+ return false;
2387
+ return spec.kind === 'GraphSpec';
2388
+ }
2389
+ /**
2390
+ * Walk an `InlineGraphSpec` looking for any inline-agent node that
2391
+ * declares `mcpTools` / `mcpToolOverrides`. Returns `true` on the first
2392
+ * hit — the caller uses this to decide whether to route the dispatch
2393
+ * through the eager-async path that awaits MCP catalog settle. Recurses
2394
+ * into nested-subgraph nodes so a deeply-nested consumer still
2395
+ * triggers the async path at the root.
2396
+ */
2397
+ /**
2398
+ * Walk an `InlineGraphSpec`'s agent nodes and feed every inline
2399
+ * agent's `tools[]` into the supplied bind callback. Static-ref nodes
2400
+ * (`{ agent_id }`) have no tools surface; sub-graphs recurse.
2401
+ *
2402
+ * Used at `startWithGraph` time so `defineFunctionTool` handlers
2403
+ * attached to per-node `tools[]` get installed on the client's
2404
+ * `ToolClient` before the wire `task.start` write.
2405
+ */
2406
+ function collectGraphFunctionTools(graph, bind) {
2407
+ for (const node of Object.values(graph.agents)) {
2408
+ const inner = node.spec;
2409
+ if (isInlineAgentNode(inner)) {
2410
+ if (inner.tools != null && inner.tools.length > 0) {
2411
+ bind(inner.tools);
2412
+ }
2413
+ }
2414
+ else if (isInlineGraphNode(inner)) {
2415
+ collectGraphFunctionTools(inner, bind);
2416
+ }
2417
+ }
2418
+ }
2419
+ function graphHasMcpProjection(graph) {
2420
+ for (const node of Object.values(graph.agents)) {
2421
+ const inner = node.spec;
2422
+ if (isInlineAgentNode(inner)) {
2423
+ if (inner.mcpTools !== undefined ||
2424
+ inner.mcpToolOverrides !== undefined) {
2425
+ return true;
2426
+ }
2427
+ }
2428
+ else if (isInlineGraphNode(inner)) {
2429
+ if (graphHasMcpProjection(inner))
2430
+ return true;
2431
+ }
2432
+ }
2433
+ return false;
2434
+ }
2435
+ /**
2436
+ * Append the `qodo-skills.*` projected tool surface to the
2437
+ * post-projection wire `tools[]`, deduping by name so a consumer-shipped
2438
+ * tool with a colliding name (rare; consumers shouldn't author tools
2439
+ * under the SDK-owned `qodo-skills.` prefix) still wins.
2440
+ *
2441
+ * Returns the original array reference when nothing would change so
2442
+ * downstream `result !== input` comparisons stay meaningful.
2443
+ */
2444
+ function mergeQodoSkillsTools(projected) {
2445
+ const skillsDefs = qodoSkillsFunctionToolDefs();
2446
+ if (skillsDefs.length === 0)
2447
+ return projected;
2448
+ const existingNames = new Set();
2449
+ for (const tool of projected) {
2450
+ existingNames.add(tool.function.name);
2451
+ }
2452
+ const toAppend = [];
2453
+ for (const def of skillsDefs) {
2454
+ if (existingNames.has(def.function.name))
2455
+ continue;
2456
+ toAppend.push(def);
2457
+ }
2458
+ if (toAppend.length === 0)
2459
+ return projected;
2460
+ return [...projected, ...toAppend];
2461
+ }
2462
+ /**
2463
+ * Recursively apply {@link mergeQodoSkillsTools} to every inline-agent
2464
+ * node in a graph spec. Static refs and nested subgraph nodes are
2465
+ * walked through. Returns the original `graph` reference when nothing
2466
+ * needed to change.
2467
+ */
2468
+ function mergeQodoSkillsToolsIntoGraph(graph) {
2469
+ const agentEntries = Object.entries(graph.agents);
2470
+ const newAgents = {};
2471
+ let touched = false;
2472
+ for (const [name, agentNode] of agentEntries) {
2473
+ const node = agentNode;
2474
+ const inner = node.spec;
2475
+ if (isInlineAgentNode(inner)) {
2476
+ const original = inner.tools ?? [];
2477
+ const merged = mergeQodoSkillsTools(original);
2478
+ if (merged !== original) {
2479
+ newAgents[name] = { ...node, spec: { ...inner, tools: merged } };
2480
+ touched = true;
2481
+ }
2482
+ else {
2483
+ newAgents[name] = node;
2484
+ }
2485
+ }
2486
+ else if (isInlineGraphNode(inner)) {
2487
+ const projected = mergeQodoSkillsToolsIntoGraph(inner);
2488
+ if (projected !== inner) {
2489
+ newAgents[name] = { ...node, spec: projected };
2490
+ touched = true;
2491
+ }
2492
+ else {
2493
+ newAgents[name] = node;
2494
+ }
2495
+ }
2496
+ else {
2497
+ newAgents[name] = node;
2498
+ }
2499
+ }
2500
+ if (!touched)
2501
+ return graph;
2502
+ return { ...graph, agents: newAgents };
2503
+ }
2504
+ /**
2505
+ * Strip SDK-only `mcpTools` / `mcpToolOverrides` fields from an inline
2506
+ * `InlineAgentSpec`. The projection layer replaces `tools[]` separately —
2507
+ * this helper only handles the field removal so the wire envelope
2508
+ * doesn't carry surfaces QAR's wire validator (`D10-R5` on
2509
+ * `extra='forbid'`) would reject.
2510
+ */
2511
+ function stripMcpToolsFromSpec(spec) {
2512
+ const hasField = spec.mcpTools !== undefined ||
2513
+ spec.mcpToolOverrides !== undefined;
2514
+ if (!hasField)
2515
+ return spec;
2516
+ // Destructure removes the two fields; rest carries every wire field.
2517
+ const { mcpTools: _mcpTools, mcpToolOverrides: _mcpToolOverrides, ...rest } = spec;
2518
+ void _mcpTools;
2519
+ void _mcpToolOverrides;
2520
+ return rest;
2521
+ }
2522
+ //# sourceMappingURL=TaskClient.js.map