@namzu/sdk 32.0.0 → 33.1.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 (305) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/authorization/gate.d.ts +1 -1
  3. package/dist/authorization/gate.js +1 -1
  4. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  5. package/dist/bridge/a2a/mapper.js +13 -0
  6. package/dist/bridge/a2a/mapper.js.map +1 -1
  7. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  8. package/dist/bridge/sse/mapper.js +3 -0
  9. package/dist/bridge/sse/mapper.js.map +1 -1
  10. package/dist/connector/index.d.ts +1 -1
  11. package/dist/connector/index.d.ts.map +1 -1
  12. package/dist/connector/index.js +1 -1
  13. package/dist/connector/index.js.map +1 -1
  14. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  15. package/dist/connector/mcp/adapter.js +71 -16
  16. package/dist/connector/mcp/adapter.js.map +1 -1
  17. package/dist/connector/mcp/client.d.ts.map +1 -1
  18. package/dist/connector/mcp/client.js +3 -2
  19. package/dist/connector/mcp/client.js.map +1 -1
  20. package/dist/connector/mcp/http-redirect.d.ts +17 -0
  21. package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
  22. package/dist/connector/mcp/http-redirect.js +31 -0
  23. package/dist/connector/mcp/http-redirect.js.map +1 -0
  24. package/dist/connector/mcp/http-sse.d.ts.map +1 -1
  25. package/dist/connector/mcp/http-sse.js +5 -0
  26. package/dist/connector/mcp/http-sse.js.map +1 -1
  27. package/dist/connector/mcp/image-admission.d.ts +16 -0
  28. package/dist/connector/mcp/image-admission.d.ts.map +1 -0
  29. package/dist/connector/mcp/image-admission.js +288 -0
  30. package/dist/connector/mcp/image-admission.js.map +1 -0
  31. package/dist/connector/mcp/stdio.d.ts +13 -0
  32. package/dist/connector/mcp/stdio.d.ts.map +1 -1
  33. package/dist/connector/mcp/stdio.js +66 -23
  34. package/dist/connector/mcp/stdio.js.map +1 -1
  35. package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
  36. package/dist/connector/mcp/streamable-http.js +3 -0
  37. package/dist/connector/mcp/streamable-http.js.map +1 -1
  38. package/dist/constants/tools/index.js +1 -1
  39. package/dist/constants/tools/index.js.map +1 -1
  40. package/dist/execution/base.d.ts +13 -0
  41. package/dist/execution/base.d.ts.map +1 -1
  42. package/dist/execution/base.js +103 -8
  43. package/dist/execution/base.js.map +1 -1
  44. package/dist/execution/errors.d.ts +12 -0
  45. package/dist/execution/errors.d.ts.map +1 -0
  46. package/dist/execution/errors.js +21 -0
  47. package/dist/execution/errors.js.map +1 -0
  48. package/dist/execution/factory.d.ts.map +1 -1
  49. package/dist/execution/factory.js +1 -0
  50. package/dist/execution/factory.js.map +1 -1
  51. package/dist/execution/hybrid.d.ts +5 -0
  52. package/dist/execution/hybrid.d.ts.map +1 -1
  53. package/dist/execution/hybrid.js +36 -16
  54. package/dist/execution/hybrid.js.map +1 -1
  55. package/dist/execution/index.d.ts +1 -0
  56. package/dist/execution/index.d.ts.map +1 -1
  57. package/dist/execution/index.js +1 -0
  58. package/dist/execution/index.js.map +1 -1
  59. package/dist/execution/local.d.ts +13 -0
  60. package/dist/execution/local.d.ts.map +1 -1
  61. package/dist/execution/local.js +145 -19
  62. package/dist/execution/local.js.map +1 -1
  63. package/dist/execution/output.d.ts +27 -0
  64. package/dist/execution/output.d.ts.map +1 -0
  65. package/dist/execution/output.js +105 -0
  66. package/dist/execution/output.js.map +1 -0
  67. package/dist/execution/remote.d.ts +26 -2
  68. package/dist/execution/remote.d.ts.map +1 -1
  69. package/dist/execution/remote.js +86 -15
  70. package/dist/execution/remote.js.map +1 -1
  71. package/dist/plugin/lifecycle.d.ts.map +1 -1
  72. package/dist/plugin/lifecycle.js +9 -3
  73. package/dist/plugin/lifecycle.js.map +1 -1
  74. package/dist/process/kill-tree.d.ts +11 -13
  75. package/dist/process/kill-tree.d.ts.map +1 -1
  76. package/dist/process/kill-tree.js +11 -13
  77. package/dist/process/kill-tree.js.map +1 -1
  78. package/dist/provider/capabilities.d.ts +2 -0
  79. package/dist/provider/capabilities.d.ts.map +1 -1
  80. package/dist/provider/capabilities.js +6 -0
  81. package/dist/provider/capabilities.js.map +1 -1
  82. package/dist/provider/fallback.d.ts.map +1 -1
  83. package/dist/provider/fallback.js +31 -0
  84. package/dist/provider/fallback.js.map +1 -1
  85. package/dist/provider/idle-timeout.d.ts.map +1 -1
  86. package/dist/provider/idle-timeout.js +5 -0
  87. package/dist/provider/idle-timeout.js.map +1 -1
  88. package/dist/provider/retry.d.ts.map +1 -1
  89. package/dist/provider/retry.js +5 -0
  90. package/dist/provider/retry.js.map +1 -1
  91. package/dist/public-runtime.d.ts +2 -2
  92. package/dist/public-runtime.d.ts.map +1 -1
  93. package/dist/public-runtime.js +2 -2
  94. package/dist/public-runtime.js.map +1 -1
  95. package/dist/registry/tool/execute.d.ts +7 -1
  96. package/dist/registry/tool/execute.d.ts.map +1 -1
  97. package/dist/registry/tool/execute.js +201 -54
  98. package/dist/registry/tool/execute.js.map +1 -1
  99. package/dist/run/command-gate.d.ts.map +1 -1
  100. package/dist/run/command-gate.js +11 -0
  101. package/dist/run/command-gate.js.map +1 -1
  102. package/dist/run/workspace-fingerprint.d.ts +4 -4
  103. package/dist/run/workspace-fingerprint.d.ts.map +1 -1
  104. package/dist/run/workspace-fingerprint.js +6 -4
  105. package/dist/run/workspace-fingerprint.js.map +1 -1
  106. package/dist/runtime/bidi/index.d.ts +1 -1
  107. package/dist/runtime/bidi/index.d.ts.map +1 -1
  108. package/dist/runtime/bidi/index.js +1 -1
  109. package/dist/runtime/bidi/index.js.map +1 -1
  110. package/dist/runtime/bidi/session.d.ts +19 -0
  111. package/dist/runtime/bidi/session.d.ts.map +1 -1
  112. package/dist/runtime/bidi/session.js +150 -26
  113. package/dist/runtime/bidi/session.js.map +1 -1
  114. package/dist/runtime/query/executor.d.ts +43 -1
  115. package/dist/runtime/query/executor.d.ts.map +1 -1
  116. package/dist/runtime/query/executor.js +626 -161
  117. package/dist/runtime/query/executor.js.map +1 -1
  118. package/dist/runtime/query/index.d.ts +3 -1
  119. package/dist/runtime/query/index.d.ts.map +1 -1
  120. package/dist/runtime/query/index.js +48 -4
  121. package/dist/runtime/query/index.js.map +1 -1
  122. package/dist/runtime/query/iteration/index.d.ts +10 -0
  123. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  124. package/dist/runtime/query/iteration/index.js +67 -0
  125. package/dist/runtime/query/iteration/index.js.map +1 -1
  126. package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
  127. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  128. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  129. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  130. package/dist/runtime/query/iteration/phases/tool-review.js +84 -22
  131. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  132. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  133. package/dist/runtime/query/request-rich-content.js +33 -13
  134. package/dist/runtime/query/request-rich-content.js.map +1 -1
  135. package/dist/runtime/query/result.d.ts.map +1 -1
  136. package/dist/runtime/query/result.js +25 -22
  137. package/dist/runtime/query/result.js.map +1 -1
  138. package/dist/runtime/query/resume-pending.d.ts +5 -1
  139. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  140. package/dist/runtime/query/resume-pending.js +66 -1
  141. package/dist/runtime/query/resume-pending.js.map +1 -1
  142. package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
  143. package/dist/runtime/query/tool-output-budget.js +45 -16
  144. package/dist/runtime/query/tool-output-budget.js.map +1 -1
  145. package/dist/runtime/query/tooling.d.ts +6 -0
  146. package/dist/runtime/query/tooling.d.ts.map +1 -1
  147. package/dist/runtime/query/tooling.js +4 -0
  148. package/dist/runtime/query/tooling.js.map +1 -1
  149. package/dist/sandbox/provider/local.d.ts.map +1 -1
  150. package/dist/sandbox/provider/local.js +193 -41
  151. package/dist/sandbox/provider/local.js.map +1 -1
  152. package/dist/scheduler/delegating.d.ts +2 -1
  153. package/dist/scheduler/delegating.d.ts.map +1 -1
  154. package/dist/scheduler/delegating.js +6 -5
  155. package/dist/scheduler/delegating.js.map +1 -1
  156. package/dist/scheduler/local.d.ts +2 -1
  157. package/dist/scheduler/local.d.ts.map +1 -1
  158. package/dist/scheduler/local.js +2 -2
  159. package/dist/scheduler/local.js.map +1 -1
  160. package/dist/session/workspace/driver.d.ts +12 -4
  161. package/dist/session/workspace/driver.d.ts.map +1 -1
  162. package/dist/session/workspace/git-worktree.d.ts +17 -3
  163. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  164. package/dist/session/workspace/git-worktree.js +153 -34
  165. package/dist/session/workspace/git-worktree.js.map +1 -1
  166. package/dist/skills/registry.d.ts +15 -0
  167. package/dist/skills/registry.d.ts.map +1 -1
  168. package/dist/skills/registry.js +33 -0
  169. package/dist/skills/registry.js.map +1 -1
  170. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  171. package/dist/tools/builtins/computer-use.js +76 -2
  172. package/dist/tools/builtins/computer-use.js.map +1 -1
  173. package/dist/tools/builtins/run-code.d.ts.map +1 -1
  174. package/dist/tools/builtins/run-code.js +9 -11
  175. package/dist/tools/builtins/run-code.js.map +1 -1
  176. package/dist/tools/builtins/skill.d.ts +2 -1
  177. package/dist/tools/builtins/skill.d.ts.map +1 -1
  178. package/dist/tools/builtins/skill.js +250 -6
  179. package/dist/tools/builtins/skill.js.map +1 -1
  180. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  181. package/dist/tools/coordinator/agent.js +100 -30
  182. package/dist/tools/coordinator/agent.js.map +1 -1
  183. package/dist/types/agent/manager.d.ts +1 -1
  184. package/dist/types/agent/manager.d.ts.map +1 -1
  185. package/dist/types/agent/scheduler.d.ts +10 -1
  186. package/dist/types/agent/scheduler.d.ts.map +1 -1
  187. package/dist/types/authorization/index.d.ts +12 -12
  188. package/dist/types/bidi/index.d.ts +19 -2
  189. package/dist/types/bidi/index.d.ts.map +1 -1
  190. package/dist/types/computer-use/index.d.ts +18 -0
  191. package/dist/types/computer-use/index.d.ts.map +1 -1
  192. package/dist/types/connector/execution-context.d.ts +3 -1
  193. package/dist/types/connector/execution-context.d.ts.map +1 -1
  194. package/dist/types/errors/catalog.d.ts.map +1 -1
  195. package/dist/types/errors/catalog.js +13 -6
  196. package/dist/types/errors/catalog.js.map +1 -1
  197. package/dist/types/errors/index.d.ts.map +1 -1
  198. package/dist/types/errors/index.js +21 -8
  199. package/dist/types/errors/index.js.map +1 -1
  200. package/dist/types/execution/index.d.ts +43 -1
  201. package/dist/types/execution/index.d.ts.map +1 -1
  202. package/dist/types/hitl/index.d.ts +11 -0
  203. package/dist/types/hitl/index.d.ts.map +1 -1
  204. package/dist/types/hitl/index.js.map +1 -1
  205. package/dist/types/message/index.d.ts +1 -1
  206. package/dist/types/message/index.d.ts.map +1 -1
  207. package/dist/types/message/index.js +2 -1
  208. package/dist/types/message/index.js.map +1 -1
  209. package/dist/types/plugin/index.d.ts +14 -1
  210. package/dist/types/plugin/index.d.ts.map +1 -1
  211. package/dist/types/plugin/index.js +1 -0
  212. package/dist/types/plugin/index.js.map +1 -1
  213. package/dist/types/provider/config.d.ts +9 -0
  214. package/dist/types/provider/config.d.ts.map +1 -1
  215. package/dist/types/provider/interface.d.ts +14 -0
  216. package/dist/types/provider/interface.d.ts.map +1 -1
  217. package/dist/types/run/events.d.ts +17 -0
  218. package/dist/types/run/events.d.ts.map +1 -1
  219. package/dist/types/run/events.js.map +1 -1
  220. package/dist/types/sandbox/index.d.ts +15 -10
  221. package/dist/types/sandbox/index.d.ts.map +1 -1
  222. package/dist/types/sandbox/index.js.map +1 -1
  223. package/dist/types/tool/index.d.ts +77 -6
  224. package/dist/types/tool/index.d.ts.map +1 -1
  225. package/dist/types/tool/index.js.map +1 -1
  226. package/dist/types/tool/presentation.d.ts +6 -2
  227. package/dist/types/tool/presentation.d.ts.map +1 -1
  228. package/dist/types/workspace/ref.d.ts +5 -0
  229. package/dist/types/workspace/ref.d.ts.map +1 -1
  230. package/dist/utils/abort.d.ts +8 -0
  231. package/dist/utils/abort.d.ts.map +1 -1
  232. package/dist/utils/abort.js +16 -0
  233. package/dist/utils/abort.js.map +1 -1
  234. package/package.json +4 -4
  235. package/src/authorization/gate.ts +1 -1
  236. package/src/bridge/a2a/mapper.ts +23 -4
  237. package/src/bridge/sse/mapper.ts +3 -0
  238. package/src/connector/index.ts +2 -0
  239. package/src/connector/mcp/adapter.ts +82 -21
  240. package/src/connector/mcp/client.ts +2 -1
  241. package/src/connector/mcp/http-redirect.ts +32 -0
  242. package/src/connector/mcp/http-sse.ts +5 -0
  243. package/src/connector/mcp/image-admission.ts +272 -0
  244. package/src/connector/mcp/stdio.ts +63 -21
  245. package/src/connector/mcp/streamable-http.ts +3 -0
  246. package/src/constants/tools/index.ts +1 -1
  247. package/src/execution/base.ts +124 -7
  248. package/src/execution/errors.ts +24 -0
  249. package/src/execution/factory.ts +1 -0
  250. package/src/execution/hybrid.ts +47 -16
  251. package/src/execution/index.ts +1 -0
  252. package/src/execution/local.ts +164 -19
  253. package/src/execution/output.ts +104 -0
  254. package/src/execution/remote.ts +122 -17
  255. package/src/plugin/lifecycle.ts +9 -3
  256. package/src/process/kill-tree.ts +11 -13
  257. package/src/provider/capabilities.ts +10 -0
  258. package/src/provider/fallback.ts +39 -0
  259. package/src/provider/idle-timeout.ts +8 -0
  260. package/src/provider/retry.ts +8 -0
  261. package/src/public-runtime.ts +7 -1
  262. package/src/registry/tool/execute.ts +230 -64
  263. package/src/run/command-gate.ts +12 -0
  264. package/src/run/workspace-fingerprint.ts +5 -4
  265. package/src/runtime/bidi/index.ts +1 -1
  266. package/src/runtime/bidi/session.ts +177 -28
  267. package/src/runtime/query/executor.ts +769 -163
  268. package/src/runtime/query/index.ts +58 -7
  269. package/src/runtime/query/iteration/index.ts +72 -0
  270. package/src/runtime/query/iteration/phases/context.ts +5 -0
  271. package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
  272. package/src/runtime/query/request-rich-content.ts +39 -14
  273. package/src/runtime/query/result.ts +25 -23
  274. package/src/runtime/query/resume-pending.ts +83 -1
  275. package/src/runtime/query/tool-output-budget.ts +52 -17
  276. package/src/runtime/query/tooling.ts +10 -0
  277. package/src/sandbox/provider/local.ts +213 -45
  278. package/src/scheduler/delegating.ts +6 -5
  279. package/src/scheduler/local.ts +3 -2
  280. package/src/session/workspace/driver.ts +12 -4
  281. package/src/session/workspace/git-worktree.ts +173 -34
  282. package/src/skills/registry.ts +47 -0
  283. package/src/tools/builtins/computer-use.ts +86 -2
  284. package/src/tools/builtins/run-code.ts +9 -11
  285. package/src/tools/builtins/skill.ts +325 -7
  286. package/src/tools/coordinator/agent.ts +108 -33
  287. package/src/types/agent/manager.ts +1 -1
  288. package/src/types/agent/scheduler.ts +10 -1
  289. package/src/types/bidi/index.ts +19 -2
  290. package/src/types/computer-use/index.ts +19 -0
  291. package/src/types/connector/execution-context.ts +3 -0
  292. package/src/types/errors/catalog.ts +14 -6
  293. package/src/types/errors/index.ts +22 -8
  294. package/src/types/execution/index.ts +46 -1
  295. package/src/types/hitl/index.ts +11 -0
  296. package/src/types/message/index.ts +3 -2
  297. package/src/types/plugin/index.ts +15 -0
  298. package/src/types/provider/config.ts +9 -0
  299. package/src/types/provider/interface.ts +18 -0
  300. package/src/types/run/events.ts +17 -3
  301. package/src/types/sandbox/index.ts +15 -10
  302. package/src/types/tool/index.ts +85 -6
  303. package/src/types/tool/presentation.ts +10 -3
  304. package/src/types/workspace/ref.ts +5 -0
  305. package/src/utils/abort.ts +18 -0
@@ -12,6 +12,32 @@ import { generateRunId } from '../../utils/id.js'
12
12
  import { SCOPE_ATTRIBUTE } from '../../utils/log/types.js'
13
13
  import { type Logger, resolveLogger } from '../../utils/logger.js'
14
14
 
15
+ const DEFAULT_CLOSE_TIMEOUT_MS = 5_000
16
+ const MAX_TIMER_DELAY_MS = 2_147_483_647
17
+
18
+ export class BidiSessionCloseTimeoutError extends Error {
19
+ constructor(readonly timeoutMs: number) {
20
+ super(
21
+ `Duplex provider session close did not settle within ${timeoutMs}ms. The local run is fenced and its tool authority was revoked, but provider cleanup is still unconfirmed.`,
22
+ )
23
+ this.name = 'BidiSessionCloseTimeoutError'
24
+ }
25
+ }
26
+
27
+ async function waitForProviderClose(pending: Promise<void>, timeoutMs: number): Promise<void> {
28
+ if (timeoutMs === 0) return await pending
29
+ let timer: ReturnType<typeof setTimeout> | undefined
30
+ const timeout = new Promise<never>((_, reject) => {
31
+ timer = setTimeout(() => reject(new BidiSessionCloseTimeoutError(timeoutMs)), timeoutMs)
32
+ timer.unref?.()
33
+ })
34
+ try {
35
+ await Promise.race([pending, timeout])
36
+ } finally {
37
+ if (timer) clearTimeout(timer)
38
+ }
39
+ }
40
+
15
41
  /**
16
42
  * Run tools for a duplex session.
17
43
  *
@@ -38,7 +64,17 @@ export interface BidiRunParams {
38
64
  readonly connect: BidiConnectParams
39
65
  readonly workingDirectory: string
40
66
  readonly env?: Record<string, string>
67
+ /**
68
+ * Owns the complete run lifetime, not only connection establishment.
69
+ * Aborting it closes the provider session, ends local events and revokes
70
+ * every tool context without mutating the caller-owned controller.
71
+ */
41
72
  readonly signal?: AbortSignal
73
+ /**
74
+ * How long `close()` waits for provider cleanup after fencing locally.
75
+ * Defaults to five seconds. `0` preserves an unbounded provider-close wait.
76
+ */
77
+ readonly closeTimeoutMs?: number
42
78
  readonly log?: Logger
43
79
  /** Overrides the generated id, so a host can correlate its own. */
44
80
  readonly runId?: RunId
@@ -50,36 +86,133 @@ export interface BidiRun {
50
86
  events(): AsyncIterable<BidiRunEvent>
51
87
  /** Push input from the human. */
52
88
  send(input: Parameters<BidiSession['send']>[0]): Promise<void>
89
+ /**
90
+ * Fence the local run immediately, abort tool contexts and close the
91
+ * provider once. It does not wait for tool code that ignores cancellation;
92
+ * provider cleanup is observed for `BidiRunParams.closeTimeoutMs`.
93
+ */
53
94
  close(): Promise<void>
54
95
  }
55
96
 
56
97
  export async function startBidiRun(params: BidiRunParams): Promise<BidiRun> {
98
+ params.signal?.throwIfAborted()
99
+ const closeTimeoutMs = params.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS
100
+ if (
101
+ !Number.isInteger(closeTimeoutMs) ||
102
+ closeTimeoutMs < 0 ||
103
+ closeTimeoutMs > MAX_TIMER_DELAY_MS
104
+ ) {
105
+ throw new RangeError(
106
+ `BidiRunParams.closeTimeoutMs must be an integer from 0 through ${MAX_TIMER_DELAY_MS}`,
107
+ )
108
+ }
57
109
  const runId = params.runId ?? generateRunId()
58
110
  const log = resolveLogger(params.log).child({
59
111
  [SCOPE_ATTRIBUTE]: 'runtime/bidi/session',
60
112
  [NAMZU.RUN_ID]: runId,
61
113
  })
62
- const session = await params.provider.connect({
63
- ...params.connect,
64
- ...(params.signal ? { signal: params.signal } : {}),
114
+ const lifetime = new AbortController()
115
+ const queue: BidiRunEvent[] = []
116
+ let wake: (() => void) | undefined
117
+ let closed = false
118
+ let session: BidiSession | undefined
119
+ let transportCloseRequested = false
120
+ let transportClosePromise: Promise<void> | undefined
121
+
122
+ const requestTransportClose = (): Promise<void> => {
123
+ transportCloseRequested = true
124
+ if (!session) return Promise.resolve()
125
+ if (!transportClosePromise) {
126
+ const ownedSession = session
127
+ transportClosePromise = Promise.resolve().then(async () => await ownedSession.close())
128
+ }
129
+ return transportClosePromise
130
+ }
131
+
132
+ const beginClose = (reason: unknown, closeTransport: boolean): void => {
133
+ if (!closed) {
134
+ closed = true
135
+ lifetime.abort(reason)
136
+ wake?.()
137
+ }
138
+ if (closeTransport) {
139
+ void requestTransportClose().catch((error: unknown) => {
140
+ log.warn('Duplex provider session close failed', {
141
+ 'exception.message': toErrorMessage(error),
142
+ })
143
+ })
144
+ }
145
+ }
146
+
147
+ const onCallerAbort = (): void => beginClose(params.signal?.reason, true)
148
+ params.signal?.addEventListener('abort', onCallerAbort, { once: true })
149
+ lifetime.signal.addEventListener(
150
+ 'abort',
151
+ () => params.signal?.removeEventListener('abort', onCallerAbort),
152
+ { once: true },
153
+ )
154
+
155
+ const connecting = Promise.resolve()
156
+ .then(
157
+ async () =>
158
+ await params.provider.connect({
159
+ ...params.connect,
160
+ signal: lifetime.signal,
161
+ }),
162
+ )
163
+ .then((connected) => {
164
+ session = connected
165
+ if (transportCloseRequested) {
166
+ void requestTransportClose().catch((error: unknown) => {
167
+ log.warn('Late duplex provider session close failed', {
168
+ 'exception.message': toErrorMessage(error),
169
+ })
170
+ })
171
+ }
172
+ return connected
173
+ })
174
+
175
+ let removeConnectAbort: (() => void) | undefined
176
+ const connectionAbort = new Promise<never>((_, reject) => {
177
+ const rejectFromLifetime = () => reject(lifetime.signal.reason)
178
+ removeConnectAbort = () => lifetime.signal.removeEventListener('abort', rejectFromLifetime)
179
+ if (lifetime.signal.aborted) rejectFromLifetime()
180
+ else lifetime.signal.addEventListener('abort', rejectFromLifetime, { once: true })
65
181
  })
66
182
 
183
+ let activeSession: BidiSession
184
+ try {
185
+ activeSession = await Promise.race([connecting, connectionAbort])
186
+ lifetime.signal.throwIfAborted()
187
+ } catch (error) {
188
+ removeConnectAbort?.()
189
+ params.signal?.removeEventListener('abort', onCallerAbort)
190
+ if (lifetime.signal.aborted) {
191
+ void connecting.catch(() => undefined)
192
+ throw lifetime.signal.reason
193
+ }
194
+ throw error
195
+ }
196
+ removeConnectAbort?.()
197
+
67
198
  // One generation of work. Bumped by every interruption, so a tool that
68
199
  // started under an older generation knows its answer is stale without
69
200
  // needing a handle on the call that cancelled it.
70
201
  let generation = 0
71
- const inflight = new Set<Promise<void>>()
202
+ const executionIds = new Set<string>()
72
203
 
73
- const queue: BidiRunEvent[] = []
74
- let wake: (() => void) | undefined
75
- const emit = (event: BidiRunEvent) => {
204
+ const emit = (event: BidiRunEvent): boolean => {
205
+ if (closed) return false
76
206
  queue.push(event)
77
207
  wake?.()
208
+ return true
78
209
  }
79
210
 
80
211
  const executeCall = async (call: { id: string; name: string; arguments: string }) => {
81
212
  const startedUnder = generation
82
- emit({ type: 'tool_started', runId, toolUseId: call.id, toolName: call.name })
213
+ if (!emit({ type: 'tool_started', runId, toolUseId: call.id, toolName: call.name })) {
214
+ return
215
+ }
83
216
 
84
217
  let output: string
85
218
  let isError = false
@@ -93,7 +226,7 @@ export async function startBidiRun(params: BidiRunParams): Promise<BidiRun> {
93
226
  const context: ToolContext = {
94
227
  runId,
95
228
  workingDirectory: params.workingDirectory,
96
- abortSignal: params.signal ?? new AbortController().signal,
229
+ abortSignal: lifetime.signal,
97
230
  env: params.env ?? {},
98
231
  log: (level, message) => log[level](message),
99
232
  toolUseId: call.id,
@@ -106,6 +239,7 @@ export async function startBidiRun(params: BidiRunParams): Promise<BidiRun> {
106
239
  isError = true
107
240
  }
108
241
 
242
+ if (closed) return
109
243
  if (startedUnder !== generation) {
110
244
  // The human spoke over the model while this ran. Delivering the
111
245
  // answer now would put it in a conversation that has moved on.
@@ -113,6 +247,12 @@ export async function startBidiRun(params: BidiRunParams): Promise<BidiRun> {
113
247
  return
114
248
  }
115
249
 
250
+ // Entering the provider send is the publication commit point. A later
251
+ // conversational interruption cannot recall a write already handed to
252
+ // the provider, but closing the run still closes the whole session and
253
+ // fences the local terminal event.
254
+ await activeSession.sendToolResult(call.id, output, isError)
255
+ if (closed) return
116
256
  emit({
117
257
  type: 'tool_completed',
118
258
  runId,
@@ -121,13 +261,12 @@ export async function startBidiRun(params: BidiRunParams): Promise<BidiRun> {
121
261
  output,
122
262
  isError,
123
263
  })
124
- await session.sendToolResult(call.id, output, isError)
125
264
  }
126
265
 
127
- let closed = false
128
266
  const pump = (async () => {
129
267
  try {
130
- for await (const event of session.events()) {
268
+ for await (const event of activeSession.events()) {
269
+ if (closed) break
131
270
  switch (event.type) {
132
271
  case 'text':
133
272
  emit({ type: 'text', runId, text: event.text })
@@ -136,13 +275,20 @@ export async function startBidiRun(params: BidiRunParams): Promise<BidiRun> {
136
275
  emit({ type: 'audio', runId, data: event.data, mediaType: event.mediaType })
137
276
  break
138
277
  case 'tool_call': {
278
+ if (executionIds.has(event.id)) {
279
+ const error = new Error(
280
+ `Duplex provider repeated tool-call id "${event.id}". Re-executing it could repeat a side effect, so the session was closed.`,
281
+ )
282
+ emit({ type: 'error', runId, message: error.message })
283
+ beginClose(error, true)
284
+ break
285
+ }
286
+ executionIds.add(event.id)
139
287
  // Started, not awaited: awaiting here would stall the very
140
288
  // stream an interruption arrives on.
141
- const running = executeCall(event).catch((err: unknown) => {
289
+ void executeCall(event).catch((err: unknown) => {
142
290
  emit({ type: 'error', runId, message: toErrorMessage(err) })
143
291
  })
144
- inflight.add(running)
145
- void running.finally(() => inflight.delete(running))
146
292
  break
147
293
  }
148
294
  case 'turn_complete':
@@ -161,7 +307,7 @@ export async function startBidiRun(params: BidiRunParams): Promise<BidiRun> {
161
307
  runId,
162
308
  ...(event.reason !== undefined ? { reason: event.reason } : {}),
163
309
  })
164
- closed = true
310
+ beginClose(new Error(event.reason ?? 'the duplex provider session closed'), false)
165
311
  break
166
312
  default: {
167
313
  const _exhaustive: never = event
@@ -171,13 +317,15 @@ export async function startBidiRun(params: BidiRunParams): Promise<BidiRun> {
171
317
  if (closed) break
172
318
  }
173
319
  } catch (err) {
174
- emit({ type: 'error', runId, message: toErrorMessage(err) })
320
+ if (!closed) {
321
+ emit({ type: 'error', runId, message: toErrorMessage(err) })
322
+ beginClose(err, true)
323
+ }
175
324
  } finally {
176
- closed = true
177
- // Let a consumer parked on the queue notice the end.
178
- wake?.()
325
+ if (!closed) beginClose(new Error('the duplex provider event stream ended'), true)
179
326
  }
180
327
  })()
328
+ void pump.catch(() => undefined)
181
329
 
182
330
  async function* events(): AsyncIterable<BidiRunEvent> {
183
331
  while (true) {
@@ -196,15 +344,16 @@ export async function startBidiRun(params: BidiRunParams): Promise<BidiRun> {
196
344
  return {
197
345
  runId,
198
346
  events,
199
- send: (input) => session.send(input),
347
+ send: async (input) => {
348
+ lifetime.signal.throwIfAborted()
349
+ // As with tool results, entering the provider send is the atomic
350
+ // publication boundary. A caller must not retry merely because the
351
+ // lifetime changed after the provider accepted the input.
352
+ await activeSession.send(input)
353
+ },
200
354
  close: async () => {
201
- await session.close()
202
- closed = true
203
- wake?.()
204
- // Tools still running are awaited, so a close cannot leave one
205
- // writing into a session nobody is reading.
206
- await Promise.allSettled([...inflight])
207
- await pump.catch(() => undefined)
355
+ beginClose(new Error('the duplex run was closed by its host'), true)
356
+ await waitForProviderClose(requestTransportClose(), closeTimeoutMs)
208
357
  },
209
358
  }
210
359
  }