@downcity/agent 1.1.21 → 1.1.23

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 (116) hide show
  1. package/README.md +6 -5
  2. package/bin/core/AgentContextTypes.d.ts +21 -2
  3. package/bin/core/AgentContextTypes.d.ts.map +1 -1
  4. package/bin/index.d.ts +5 -1
  5. package/bin/index.d.ts.map +1 -1
  6. package/bin/index.js.map +1 -1
  7. package/bin/runtime/server/http/Server.d.ts +1 -1
  8. package/bin/runtime/server/http/Server.d.ts.map +1 -1
  9. package/bin/runtime/server/http/Server.js +4 -0
  10. package/bin/runtime/server/http/Server.js.map +1 -1
  11. package/bin/runtime/server/http/control/ExecuteBySession.js +1 -1
  12. package/bin/runtime/server/http/control/ExecuteBySession.js.map +1 -1
  13. package/bin/runtime/server/http/control/StreamBySession.d.ts +3 -3
  14. package/bin/runtime/server/http/control/StreamBySession.js +6 -6
  15. package/bin/runtime/server/http/control/StreamBySession.js.map +1 -1
  16. package/bin/runtime/server/http/execute/execute.js +1 -1
  17. package/bin/runtime/server/http/execute/execute.js.map +1 -1
  18. package/bin/runtime/server/http/sdk/Router.d.ts +14 -0
  19. package/bin/runtime/server/http/sdk/Router.d.ts.map +1 -0
  20. package/bin/runtime/server/http/sdk/Router.js +18 -0
  21. package/bin/runtime/server/http/sdk/Router.js.map +1 -0
  22. package/bin/runtime/server/http/sdk/SessionRoutes.d.ts +15 -0
  23. package/bin/runtime/server/http/sdk/SessionRoutes.d.ts.map +1 -0
  24. package/bin/runtime/server/http/sdk/SessionRoutes.js +190 -0
  25. package/bin/runtime/server/http/sdk/SessionRoutes.js.map +1 -0
  26. package/bin/sdk/AgentSdkTypes.d.ts +1 -124
  27. package/bin/sdk/AgentSdkTypes.d.ts.map +1 -1
  28. package/bin/sdk/RemoteAgent.d.ts +27 -8
  29. package/bin/sdk/RemoteAgent.d.ts.map +1 -1
  30. package/bin/sdk/RemoteAgent.js +266 -38
  31. package/bin/sdk/RemoteAgent.js.map +1 -1
  32. package/bin/sdk/Session.d.ts +31 -10
  33. package/bin/sdk/Session.d.ts.map +1 -1
  34. package/bin/sdk/Session.js +181 -73
  35. package/bin/sdk/Session.js.map +1 -1
  36. package/bin/sdk/SessionEventMapper.d.ts +32 -0
  37. package/bin/sdk/SessionEventMapper.d.ts.map +1 -0
  38. package/bin/sdk/{StreamEvents.js → SessionEventMapper.js} +43 -28
  39. package/bin/sdk/SessionEventMapper.js.map +1 -0
  40. package/bin/sdk/session/ServicePort.d.ts +51 -4
  41. package/bin/sdk/session/ServicePort.d.ts.map +1 -1
  42. package/bin/sdk/session/ServicePort.js +17 -10
  43. package/bin/sdk/session/ServicePort.js.map +1 -1
  44. package/bin/sdk/session/runtime/SessionEventHub.d.ts +24 -0
  45. package/bin/sdk/session/runtime/SessionEventHub.d.ts.map +1 -0
  46. package/bin/sdk/session/runtime/SessionEventHub.js +39 -0
  47. package/bin/sdk/session/runtime/SessionEventHub.js.map +1 -0
  48. package/bin/sdk/session/runtime/SessionPromptRuntime.d.ts +68 -0
  49. package/bin/sdk/session/runtime/SessionPromptRuntime.d.ts.map +1 -0
  50. package/bin/sdk/session/runtime/SessionPromptRuntime.js +170 -0
  51. package/bin/sdk/session/runtime/SessionPromptRuntime.js.map +1 -0
  52. package/bin/service/builtins/chat/runtime/ChatQueueWorker.js +1 -1
  53. package/bin/service/builtins/chat/runtime/ChatQueueWorker.js.map +1 -1
  54. package/bin/service/builtins/chat/runtime/ChatSession.d.ts +1 -1
  55. package/bin/service/builtins/chat/runtime/ChatSession.js +1 -1
  56. package/bin/service/builtins/contact/runtime/ChatRuntime.js +1 -1
  57. package/bin/service/builtins/contact/runtime/ChatRuntime.js.map +1 -1
  58. package/bin/session/Executor.d.ts.map +1 -1
  59. package/bin/session/Executor.js +1 -1
  60. package/bin/session/Executor.js.map +1 -1
  61. package/bin/session/core-engine/CoreEngineSignals.d.ts +1 -1
  62. package/bin/session/core-engine/CoreEngineSignals.d.ts.map +1 -1
  63. package/bin/session/core-engine/CoreEngineSignals.js +1 -23
  64. package/bin/session/core-engine/CoreEngineSignals.js.map +1 -1
  65. package/bin/types/sdk/AgentSessionEvent.d.ts +160 -0
  66. package/bin/types/sdk/AgentSessionEvent.d.ts.map +1 -0
  67. package/bin/types/sdk/AgentSessionEvent.js +9 -0
  68. package/bin/types/sdk/AgentSessionEvent.js.map +1 -0
  69. package/bin/types/sdk/AgentSessionPrompt.d.ts +21 -0
  70. package/bin/types/sdk/AgentSessionPrompt.d.ts.map +1 -0
  71. package/bin/types/sdk/AgentSessionPrompt.js +9 -0
  72. package/bin/types/sdk/AgentSessionPrompt.js.map +1 -0
  73. package/bin/types/sdk/AgentSessionTurn.d.ts +59 -0
  74. package/bin/types/sdk/AgentSessionTurn.d.ts.map +1 -0
  75. package/bin/types/sdk/AgentSessionTurn.js +10 -0
  76. package/bin/types/sdk/AgentSessionTurn.js.map +1 -0
  77. package/bin/types/sdk/AgentUiChunkEvent.d.ts +100 -0
  78. package/bin/types/sdk/AgentUiChunkEvent.d.ts.map +1 -0
  79. package/bin/types/sdk/AgentUiChunkEvent.js +9 -0
  80. package/bin/types/sdk/AgentUiChunkEvent.js.map +1 -0
  81. package/package.json +2 -1
  82. package/scripts/session-prompt-runtime.test.mjs +167 -0
  83. package/src/core/AgentContextTypes.ts +24 -2
  84. package/src/index.ts +11 -3
  85. package/src/runtime/server/http/Server.ts +5 -1
  86. package/src/runtime/server/http/control/ExecuteBySession.ts +1 -1
  87. package/src/runtime/server/http/control/StreamBySession.ts +11 -11
  88. package/src/runtime/server/http/execute/execute.ts +1 -1
  89. package/src/runtime/server/http/sdk/Router.ts +22 -0
  90. package/src/runtime/server/http/sdk/SessionRoutes.ts +232 -0
  91. package/src/sdk/AgentSdkTypes.ts +1 -137
  92. package/src/sdk/RemoteAgent.ts +368 -49
  93. package/src/sdk/Session.ts +235 -86
  94. package/src/sdk/{StreamEvents.ts → SessionEventMapper.ts} +50 -40
  95. package/src/sdk/session/ServicePort.ts +74 -12
  96. package/src/sdk/session/runtime/SessionEventHub.ts +47 -0
  97. package/src/sdk/session/runtime/SessionPromptRuntime.ts +269 -0
  98. package/src/service/builtins/chat/runtime/ChatQueueWorker.ts +1 -1
  99. package/src/service/builtins/chat/runtime/ChatSession.ts +1 -1
  100. package/src/service/builtins/contact/runtime/ChatRuntime.ts +1 -1
  101. package/src/session/Executor.ts +1 -4
  102. package/src/session/README.md +8 -7
  103. package/src/session/core-engine/CoreEngineSignals.ts +0 -23
  104. package/src/types/sdk/AgentSessionEvent.ts +195 -0
  105. package/src/types/sdk/AgentSessionPrompt.ts +21 -0
  106. package/src/types/sdk/AgentSessionTurn.ts +65 -0
  107. package/src/types/sdk/AgentUiChunkEvent.ts +108 -0
  108. package/tsconfig.tsbuildinfo +1 -1
  109. package/bin/sdk/AsyncQueue.d.ts +0 -26
  110. package/bin/sdk/AsyncQueue.d.ts.map +0 -1
  111. package/bin/sdk/AsyncQueue.js +0 -75
  112. package/bin/sdk/AsyncQueue.js.map +0 -1
  113. package/bin/sdk/StreamEvents.d.ts +0 -36
  114. package/bin/sdk/StreamEvents.d.ts.map +0 -1
  115. package/bin/sdk/StreamEvents.js.map +0 -1
  116. package/src/sdk/AsyncQueue.ts +0 -92
@@ -9,9 +9,7 @@
9
9
 
10
10
  import type { LanguageModel, Tool } from "ai";
11
11
  import type { BaseService } from "@/service/builtins/BaseService.js";
12
- import type { JsonValue } from "@/types/common/Json.js";
13
12
  import type { Plugin } from "@/plugin/types/Plugin.js";
14
- import type { SessionMessageV1 } from "@/session/types/SessionMessages.js";
15
13
  import type { AgentPlatformRuntime } from "@/types/runtime/host/AgentHost.js";
16
14
  import type { LocalRpcServerHandle } from "@/types/runtime/rpc/LocalRpc.js";
17
15
  import type { ServerInstance } from "@/runtime/server/http/Server.js";
@@ -292,140 +290,6 @@ export interface AgentSessionConfigSnapshot {
292
290
  modelLabel?: string;
293
291
  }
294
292
 
295
- /**
296
- * Session 运行输入。
297
- */
298
- export interface AgentSessionRunInput {
299
- /**
300
- * 当前轮用户查询文本。
301
- */
302
- query: string;
303
- }
304
-
305
- /**
306
- * Session 运行结果。
307
- */
308
- export interface AgentSessionRunResult {
309
- /**
310
- * 本轮执行是否成功。
311
- */
312
- success: boolean;
313
-
314
- /**
315
- * 失败时的错误文本。
316
- */
317
- error?: string;
318
-
319
- /**
320
- * 最终 assistant 文本。
321
- */
322
- text: string;
323
-
324
- /**
325
- * 最终 assistant 原始 UIMessage。
326
- */
327
- assistantMessage: SessionMessageV1;
328
- }
329
-
330
- /**
331
- * SDK 对外的流式事件。
332
- */
333
- export type AgentSessionStreamEvent =
334
- | {
335
- /**
336
- * 文本增量事件。
337
- */
338
- type: "text-delta";
339
- /**
340
- * 当前追加的文本片段。
341
- */
342
- text: string;
343
- }
344
- | {
345
- /**
346
- * reasoning 增量事件。
347
- */
348
- type: "reasoning-delta";
349
- /**
350
- * 当前追加的 reasoning 文本片段。
351
- */
352
- text: string;
353
- }
354
- | {
355
- /**
356
- * 工具调用可用事件。
357
- */
358
- type: "tool-call";
359
- /**
360
- * 当前工具调用唯一标识。
361
- */
362
- toolCallId: string;
363
- /**
364
- * 工具名称。
365
- */
366
- toolName: string;
367
- /**
368
- * 工具输入参数。
369
- */
370
- args: JsonValue;
371
- }
372
- | {
373
- /**
374
- * 工具调用结果事件。
375
- */
376
- type: "tool-result";
377
- /**
378
- * 当前工具调用唯一标识。
379
- */
380
- toolCallId: string;
381
- /**
382
- * 工具名称。
383
- */
384
- toolName: string;
385
- /**
386
- * 工具输出结果。
387
- */
388
- result: JsonValue;
389
- }
390
- | {
391
- /**
392
- * 工具调用失败事件。
393
- */
394
- type: "tool-error";
395
- /**
396
- * 当前工具调用唯一标识。
397
- */
398
- toolCallId: string;
399
- /**
400
- * 工具名称。
401
- */
402
- toolName: string;
403
- /**
404
- * 错误文本。
405
- */
406
- error: string;
407
- }
408
- | {
409
- /**
410
- * 运行结束事件。
411
- */
412
- type: "finish";
413
- /**
414
- * 最终完成原因(若底层可提供)。
415
- */
416
- finishReason?: string;
417
- }
418
- | {
419
- /**
420
- * 运行错误事件。
421
- */
422
- type: "error";
423
- /**
424
- * 错误文本。
425
- */
426
- error: string;
427
- };
428
-
429
293
  /**
430
294
  * Session system block 来源类型。
431
295
  */
@@ -490,7 +354,7 @@ export interface AgentSessionSystemSessionInfo {
490
354
  *
491
355
  * 关键点(中文)
492
356
  * - 这是 session 初始化时落盘的稳定参考时间,按 Date/ISO 字符串对外展示。
493
- * - 它不是每轮运行的当前时间,不会随着 `run()` / `stream()` 改变。
357
+ * - 它不是每轮运行的当前时间,不会随着后续 turn 执行而改变。
494
358
  */
495
359
  createdAt: string;
496
360
 
@@ -3,19 +3,76 @@
3
3
  *
4
4
  * 关键点(中文)
5
5
  * - 面向已通过 `agent.start({ http: { ... } })` 暴露出来的远程/本地 HTTP 服务。
6
- * - 与本地 `Agent` 尽量保持同一套 session 使用面。
6
+ * - 与本地 `Session` 保持同一套 Session actor 使用面:`prompt()` + `subscribe()`。
7
+ * - `RemoteAgent` 只负责 transport 与本地 turn handle 合成,不重复实现第二套会话编排器。
7
8
  */
8
9
 
9
10
  import type {
10
11
  AgentSessionMetadata,
11
- AgentSessionRunInput,
12
- AgentSessionRunResult,
13
12
  AgentSessionSetInput,
14
- AgentSessionStreamEvent,
15
13
  AgentSessionSystemSnapshot,
16
14
  RemoteAgentOptions,
17
15
  } from "@/sdk/AgentSdkTypes.js";
18
16
  import type { SessionMessageV1 } from "@/session/types/SessionMessages.js";
17
+ import type {
18
+ AgentSessionEvent,
19
+ AgentSessionSubscriber,
20
+ AgentSessionUnsubscribe,
21
+ } from "@/types/sdk/AgentSessionEvent.js";
22
+ import type { AgentSessionPromptInput } from "@/types/sdk/AgentSessionPrompt.js";
23
+ import type {
24
+ AgentSessionTurnHandle,
25
+ AgentSessionTurnResult,
26
+ } from "@/types/sdk/AgentSessionTurn.js";
27
+ import { SessionEventHub } from "@/sdk/session/runtime/SessionEventHub.js";
28
+
29
+ type Deferred<T> = {
30
+ /**
31
+ * 当前延迟 Promise。
32
+ */
33
+ promise: Promise<T>;
34
+ /**
35
+ * 兑现 Promise。
36
+ */
37
+ resolve: (value: T) => void;
38
+ };
39
+
40
+ type RemoteTurnLifecycle = {
41
+ /**
42
+ * 当前 turnId。
43
+ */
44
+ turnId: string;
45
+ /**
46
+ * 当前 turn 的最终结果快照。
47
+ */
48
+ result: AgentSessionTurnResult | null;
49
+ /**
50
+ * 当前 turn 完成 Promise 控制器。
51
+ */
52
+ deferredFinished: Deferred<AgentSessionTurnResult>;
53
+ };
54
+
55
+ type EventConnectionReady = {
56
+ /**
57
+ * 当前 ready promise 是否已经完成。
58
+ */
59
+ settled: boolean;
60
+ /**
61
+ * 标记事件连接已经在服务端完成订阅。
62
+ */
63
+ resolve: () => void;
64
+ /**
65
+ * 标记事件连接在 ready 前失败。
66
+ */
67
+ reject: (error: unknown) => void;
68
+ };
69
+
70
+ type SdkEventsReadyFrame = {
71
+ /**
72
+ * SDK HTTP events 连接内部 ready 标记。
73
+ */
74
+ type: "sdk-events-ready";
75
+ };
19
76
 
20
77
  /**
21
78
  * 远程 Session 客户端。
@@ -23,6 +80,13 @@ import type { SessionMessageV1 } from "@/session/types/SessionMessages.js";
23
80
  class RemoteSession {
24
81
  readonly id: string;
25
82
  private readonly baseUrl: string;
83
+ private readonly eventHub = new SessionEventHub();
84
+ private readonly turnsById = new Map<string, RemoteTurnLifecycle>();
85
+ private readonly completedTurnIds: string[] = [];
86
+ private eventPumpConnectPromise: Promise<void> | null = null;
87
+ private eventPumpAbortController: AbortController | null = null;
88
+ private eventPumpRunning = false;
89
+ private eventSubscriberCount = 0;
26
90
 
27
91
  constructor(baseUrl: string, sessionId: string) {
28
92
  this.baseUrl = baseUrl;
@@ -39,25 +103,60 @@ class RemoteSession {
39
103
  }
40
104
 
41
105
  /**
42
- * 执行一轮非流式请求。
106
+ * 向当前远程 session 追加一条新的 prompt。
43
107
  */
44
- async run(input: AgentSessionRunInput): Promise<AgentSessionRunResult> {
45
- const response = await fetch(`${this.baseUrl}/api/sdk/sessions/${encodeURIComponent(this.id)}/run`, {
46
- method: "POST",
47
- headers: {
48
- "Content-Type": "application/json",
108
+ async prompt(input: AgentSessionPromptInput): Promise<AgentSessionTurnHandle> {
109
+ const query = String(input.query || "").trim();
110
+ if (!query) {
111
+ throw new Error("remote session.prompt requires a non-empty query");
112
+ }
113
+
114
+ await this.ensureEventPump();
115
+
116
+ const response = await fetch(
117
+ `${this.baseUrl}/api/sdk/sessions/${encodeURIComponent(this.id)}/prompt`,
118
+ {
119
+ method: "POST",
120
+ headers: {
121
+ "Content-Type": "application/json",
122
+ },
123
+ body: JSON.stringify({
124
+ query,
125
+ }),
49
126
  },
50
- body: JSON.stringify(input),
51
- });
127
+ );
52
128
  const payload = (await response.json()) as {
53
129
  success?: boolean;
54
130
  error?: string;
55
- result?: AgentSessionRunResult;
131
+ turn?: {
132
+ id?: string;
133
+ };
56
134
  };
57
- if (!response.ok || !payload.success || !payload.result) {
58
- throw new Error(String(payload.error || "Remote session run failed"));
135
+ if (!response.ok || !payload.success || !payload.turn?.id) {
136
+ throw new Error(String(payload.error || "Remote session prompt failed"));
59
137
  }
60
- return payload.result;
138
+
139
+ const lifecycle = this.ensureTurnLifecycle(payload.turn.id);
140
+ return createTurnHandle(lifecycle);
141
+ }
142
+
143
+ /**
144
+ * 订阅当前远程 session 的 future 事件。
145
+ */
146
+ subscribe(subscriber: AgentSessionSubscriber): AgentSessionUnsubscribe {
147
+ this.eventSubscriberCount += 1;
148
+ void this.ensureEventPump().catch((error) => {
149
+ this.eventHub.publish({
150
+ type: "error",
151
+ message: error instanceof Error ? error.message : String(error),
152
+ });
153
+ });
154
+ const unsubscribe = this.eventHub.subscribe(subscriber);
155
+ return () => {
156
+ unsubscribe();
157
+ this.eventSubscriberCount = Math.max(0, this.eventSubscriberCount - 1);
158
+ this.maybeStopEventPump();
159
+ };
61
160
  }
62
161
 
63
162
  /**
@@ -128,48 +227,187 @@ class RemoteSession {
128
227
  return new RemoteSession(this.baseUrl, payload.session.sessionId);
129
228
  }
130
229
 
131
- /**
132
- * 执行一轮流式请求。
133
- */
134
- async *stream(
135
- input: AgentSessionRunInput,
136
- ): AsyncIterable<AgentSessionStreamEvent> {
137
- const response = await fetch(
138
- `${this.baseUrl}/api/sdk/sessions/${encodeURIComponent(this.id)}/stream`,
139
- {
140
- method: "POST",
141
- headers: {
142
- "Content-Type": "application/json",
230
+ private async ensureEventPump(): Promise<void> {
231
+ if (this.eventPumpConnectPromise) {
232
+ await this.eventPumpConnectPromise;
233
+ return;
234
+ }
235
+ if (this.eventPumpRunning) return;
236
+
237
+ this.eventPumpConnectPromise = (async () => {
238
+ const abortController = new AbortController();
239
+ this.eventPumpAbortController = abortController;
240
+ const response = await fetch(
241
+ `${this.baseUrl}/api/sdk/sessions/${encodeURIComponent(this.id)}/events`,
242
+ {
243
+ signal: abortController.signal,
143
244
  },
144
- body: JSON.stringify(input),
145
- },
146
- );
147
- if (!response.ok || !response.body) {
148
- const text = await response.text().catch(() => "");
149
- throw new Error(text || `Remote session stream failed (${response.status})`);
245
+ );
246
+ if (!response.ok || !response.body) {
247
+ const text = await response.text().catch(() => "");
248
+ throw new Error(
249
+ text || `Remote session events failed (${response.status})`,
250
+ );
251
+ }
252
+ this.eventPumpRunning = true;
253
+ const ready = createEventConnectionReady();
254
+ void this.consumeEventConnection(response.body, abortController, ready)
255
+ .finally(() => {
256
+ const wasAborted = abortController.signal.aborted;
257
+ this.eventPumpRunning = false;
258
+ if (this.eventPumpAbortController === abortController) {
259
+ this.eventPumpAbortController = null;
260
+ }
261
+ if (!wasAborted) {
262
+ this.failPendingTurns("Remote session events connection closed");
263
+ this.eventHub.publish({
264
+ type: "error",
265
+ message: "Remote session events connection closed",
266
+ });
267
+ }
268
+ });
269
+ await ready.promise;
270
+ })();
271
+
272
+ try {
273
+ await this.eventPumpConnectPromise;
274
+ } catch (error) {
275
+ this.eventPumpAbortController = null;
276
+ throw error;
277
+ } finally {
278
+ this.eventPumpConnectPromise = null;
150
279
  }
280
+ }
151
281
 
282
+ private async consumeEventConnection(
283
+ body: ReadableStream<Uint8Array>,
284
+ abortController: AbortController,
285
+ ready: EventConnectionReady,
286
+ ): Promise<void> {
152
287
  const decoder = new TextDecoder();
153
- const reader = response.body.getReader();
288
+ const reader = body.getReader();
154
289
  let buffered = "";
155
- while (true) {
156
- const { done, value } = await reader.read();
157
- if (done) break;
158
- buffered += decoder.decode(value, { stream: true });
159
- let newlineIndex = buffered.indexOf("\n");
160
- while (newlineIndex >= 0) {
161
- const line = buffered.slice(0, newlineIndex).trim();
162
- buffered = buffered.slice(newlineIndex + 1);
163
- if (line) {
164
- yield JSON.parse(line) as AgentSessionStreamEvent;
290
+
291
+ try {
292
+ while (true) {
293
+ const { done, value } = await reader.read();
294
+ if (done) break;
295
+ buffered += decoder.decode(value, { stream: true });
296
+ let newlineIndex = buffered.indexOf("\n");
297
+ while (newlineIndex >= 0) {
298
+ const line = buffered.slice(0, newlineIndex).trim();
299
+ buffered = buffered.slice(newlineIndex + 1);
300
+ if (line) {
301
+ this.handleEventLine(JSON.parse(line) as unknown, ready);
302
+ }
303
+ newlineIndex = buffered.indexOf("\n");
165
304
  }
166
- newlineIndex = buffered.indexOf("\n");
305
+ }
306
+ const tail = buffered.trim();
307
+ if (tail) {
308
+ this.handleEventLine(JSON.parse(tail) as unknown, ready);
309
+ }
310
+ rejectEventConnectionReady(
311
+ ready,
312
+ "Remote session events connection closed before ready",
313
+ );
314
+ } catch (error) {
315
+ if (!abortController.signal.aborted) {
316
+ const message = error instanceof Error ? error.message : String(error);
317
+ rejectEventConnectionReady(ready, message);
318
+ this.failPendingTurns(message);
319
+ this.eventHub.publish({
320
+ type: "error",
321
+ message,
322
+ });
323
+ }
324
+ } finally {
325
+ try {
326
+ reader.releaseLock();
327
+ } catch {
328
+ // ignore
167
329
  }
168
330
  }
169
- const tail = buffered.trim();
170
- if (tail) {
171
- yield JSON.parse(tail) as AgentSessionStreamEvent;
331
+ }
332
+
333
+ private handleEventLine(
334
+ value: unknown,
335
+ ready: EventConnectionReady,
336
+ ): void {
337
+ if (isSdkEventsReadyFrame(value)) {
338
+ resolveEventConnectionReady(ready);
339
+ return;
172
340
  }
341
+ this.handleEvent(value as AgentSessionEvent);
342
+ }
343
+
344
+ private handleEvent(event: AgentSessionEvent): void {
345
+ const turnId = extractTurnId(event);
346
+ if (turnId) {
347
+ this.ensureTurnLifecycle(turnId);
348
+ }
349
+
350
+ if (event.type === "turn-finish") {
351
+ const lifecycle = this.ensureTurnLifecycle(event.turnId);
352
+ const result: AgentSessionTurnResult = {
353
+ turnId: event.turnId,
354
+ text: event.text,
355
+ success: event.success,
356
+ ...(event.error ? { error: event.error } : {}),
357
+ };
358
+ lifecycle.result = result;
359
+ lifecycle.deferredFinished.resolve(result);
360
+ this.rememberCompletedTurn(event.turnId);
361
+ this.maybeStopEventPump();
362
+ }
363
+
364
+ this.eventHub.publish(event);
365
+ }
366
+
367
+ private ensureTurnLifecycle(turnId: string): RemoteTurnLifecycle {
368
+ const cached = this.turnsById.get(turnId);
369
+ if (cached) return cached;
370
+ const created: RemoteTurnLifecycle = {
371
+ turnId,
372
+ result: null,
373
+ deferredFinished: createDeferred<AgentSessionTurnResult>(),
374
+ };
375
+ this.turnsById.set(turnId, created);
376
+ return created;
377
+ }
378
+
379
+ private failPendingTurns(message: string): void {
380
+ for (const lifecycle of this.turnsById.values()) {
381
+ if (lifecycle.result) continue;
382
+ const result: AgentSessionTurnResult = {
383
+ turnId: lifecycle.turnId,
384
+ text: "",
385
+ success: false,
386
+ error: message,
387
+ };
388
+ lifecycle.result = result;
389
+ lifecycle.deferredFinished.resolve(result);
390
+ this.rememberCompletedTurn(lifecycle.turnId);
391
+ }
392
+ this.maybeStopEventPump();
393
+ }
394
+
395
+ private rememberCompletedTurn(turnId: string): void {
396
+ this.completedTurnIds.push(turnId);
397
+ while (this.completedTurnIds.length > 200) {
398
+ const oldestTurnId = this.completedTurnIds.shift();
399
+ if (oldestTurnId) {
400
+ this.turnsById.delete(oldestTurnId);
401
+ }
402
+ }
403
+ }
404
+
405
+ private maybeStopEventPump(): void {
406
+ if (this.eventSubscriberCount > 0) return;
407
+ if ([...this.turnsById.values()].some((item) => item.result === null)) return;
408
+ if (!this.eventPumpAbortController) return;
409
+ this.eventPumpAbortController.abort();
410
+ this.eventPumpAbortController = null;
173
411
  }
174
412
  }
175
413
 
@@ -227,3 +465,84 @@ export class RemoteAgent {
227
465
  return payload.sessions;
228
466
  }
229
467
  }
468
+
469
+ function createDeferred<T>(): Deferred<T> {
470
+ let resolve!: (value: T) => void;
471
+ const promise = new Promise<T>((innerResolve) => {
472
+ resolve = innerResolve;
473
+ });
474
+ return {
475
+ promise,
476
+ resolve,
477
+ };
478
+ }
479
+
480
+ function createEventConnectionReady(): EventConnectionReady & {
481
+ /**
482
+ * 等待事件连接 ready 的 Promise。
483
+ */
484
+ promise: Promise<void>;
485
+ } {
486
+ let resolve!: () => void;
487
+ let reject!: (error: unknown) => void;
488
+ const promise = new Promise<void>((innerResolve, innerReject) => {
489
+ resolve = innerResolve;
490
+ reject = innerReject;
491
+ });
492
+ return {
493
+ promise,
494
+ settled: false,
495
+ resolve,
496
+ reject,
497
+ };
498
+ }
499
+
500
+ function resolveEventConnectionReady(ready: EventConnectionReady): void {
501
+ if (ready.settled) return;
502
+ ready.settled = true;
503
+ ready.resolve();
504
+ }
505
+
506
+ function rejectEventConnectionReady(
507
+ ready: EventConnectionReady,
508
+ error: unknown,
509
+ ): void {
510
+ if (ready.settled) return;
511
+ ready.settled = true;
512
+ ready.reject(error);
513
+ }
514
+
515
+ function isSdkEventsReadyFrame(value: unknown): value is SdkEventsReadyFrame {
516
+ return (
517
+ typeof value === "object" &&
518
+ value !== null &&
519
+ "type" in value &&
520
+ (value as { type?: unknown }).type === "sdk-events-ready"
521
+ );
522
+ }
523
+
524
+ function createTurnHandle(
525
+ lifecycle: RemoteTurnLifecycle,
526
+ ): AgentSessionTurnHandle {
527
+ return {
528
+ id: lifecycle.turnId,
529
+ get result() {
530
+ return lifecycle.result;
531
+ },
532
+ finished: lifecycle.deferredFinished.promise,
533
+ };
534
+ }
535
+
536
+ function extractTurnId(event: AgentSessionEvent): string | null {
537
+ switch (event.type) {
538
+ case "turn-start":
539
+ case "text-delta":
540
+ case "reasoning-delta":
541
+ case "tool-call":
542
+ case "tool-result":
543
+ case "turn-finish":
544
+ return event.turnId;
545
+ default:
546
+ return null;
547
+ }
548
+ }