@downcity/agent 1.1.400 → 1.1.408

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 (84) hide show
  1. package/LICENSE +183 -0
  2. package/README.md +4 -3
  3. package/bin/agent/WorkspaceEntry.js +1 -1
  4. package/bin/agent/WorkspaceEntry.js.map +1 -1
  5. package/bin/city/types/PluginHostContext.d.ts +3 -0
  6. package/bin/city/types/PluginHostContext.d.ts.map +1 -1
  7. package/bin/executor/core-engine/CoreEngineError.js +8 -2
  8. package/bin/executor/core-engine/CoreEngineError.js.map +1 -1
  9. package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
  10. package/bin/executor/core-engine/CoreEngineRunner.js +36 -0
  11. package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
  12. package/bin/executor/model/ModelGenerate.d.ts.map +1 -1
  13. package/bin/executor/model/ModelGenerate.js +2 -1
  14. package/bin/executor/model/ModelGenerate.js.map +1 -1
  15. package/bin/executor/model/ModelStepRunner.d.ts +2 -0
  16. package/bin/executor/model/ModelStepRunner.d.ts.map +1 -1
  17. package/bin/executor/model/ModelStepRunner.js +33 -7
  18. package/bin/executor/model/ModelStepRunner.js.map +1 -1
  19. package/bin/executor/model/ModelStreamFailure.d.ts +22 -0
  20. package/bin/executor/model/ModelStreamFailure.d.ts.map +1 -0
  21. package/bin/executor/model/ModelStreamFailure.js +35 -0
  22. package/bin/executor/model/ModelStreamFailure.js.map +1 -0
  23. package/bin/group/GroupDispatchSession.d.ts +36 -0
  24. package/bin/group/GroupDispatchSession.d.ts.map +1 -0
  25. package/bin/group/GroupDispatchSession.js +154 -0
  26. package/bin/group/GroupDispatchSession.js.map +1 -0
  27. package/bin/group/GroupSession.d.ts +3 -1
  28. package/bin/group/GroupSession.d.ts.map +1 -1
  29. package/bin/group/GroupSession.js +65 -28
  30. package/bin/group/GroupSession.js.map +1 -1
  31. package/bin/index.d.ts +2 -0
  32. package/bin/index.d.ts.map +1 -1
  33. package/bin/index.js.map +1 -1
  34. package/bin/session/messages/SessionAssistantMessageWriter.d.ts.map +1 -1
  35. package/bin/session/messages/SessionAssistantMessageWriter.js +10 -6
  36. package/bin/session/messages/SessionAssistantMessageWriter.js.map +1 -1
  37. package/bin/types/group/DispatchStrategy.d.ts +3 -0
  38. package/bin/types/group/DispatchStrategy.d.ts.map +1 -1
  39. package/bin/types/group/DispatchStrategy.js +66 -18
  40. package/bin/types/group/DispatchStrategy.js.map +1 -1
  41. package/bin/types/group/GroupDispatchSession.d.ts +65 -0
  42. package/bin/types/group/GroupDispatchSession.d.ts.map +1 -0
  43. package/bin/types/group/GroupDispatchSession.js +8 -0
  44. package/bin/types/group/GroupDispatchSession.js.map +1 -0
  45. package/bin/types/group/GroupSession.d.ts +2 -0
  46. package/bin/types/group/GroupSession.d.ts.map +1 -1
  47. package/bin/types/group/GroupSessionStore.d.ts +6 -1
  48. package/bin/types/group/GroupSessionStore.d.ts.map +1 -1
  49. package/bin/types/plugin/PluginNotification.d.ts +31 -0
  50. package/bin/types/plugin/PluginNotification.d.ts.map +1 -0
  51. package/bin/types/plugin/PluginNotification.js +8 -0
  52. package/bin/types/plugin/PluginNotification.js.map +1 -0
  53. package/bin/workspace/store/LocalGroupDispatchSessionDataStore.d.ts +28 -0
  54. package/bin/workspace/store/LocalGroupDispatchSessionDataStore.d.ts.map +1 -0
  55. package/bin/workspace/store/LocalGroupDispatchSessionDataStore.js +102 -0
  56. package/bin/workspace/store/LocalGroupDispatchSessionDataStore.js.map +1 -0
  57. package/bin/workspace/store/LocalGroupSessionDataStore.d.ts +2 -0
  58. package/bin/workspace/store/LocalGroupSessionDataStore.d.ts.map +1 -1
  59. package/bin/workspace/store/LocalGroupSessionDataStore.js +25 -8
  60. package/bin/workspace/store/LocalGroupSessionDataStore.js.map +1 -1
  61. package/city-test/group.test.mjs +252 -1
  62. package/package.json +15 -17
  63. package/scripts/agent-multi-workspace.test.mjs +67 -0
  64. package/scripts/core-engine-context-compaction.test.mjs +49 -0
  65. package/scripts/core-engine-model-stream.test.mjs +69 -0
  66. package/src/agent/WorkspaceEntry.ts +1 -1
  67. package/src/city/types/PluginHostContext.ts +4 -0
  68. package/src/executor/core-engine/CoreEngineError.ts +8 -2
  69. package/src/executor/core-engine/CoreEngineRunner.ts +44 -0
  70. package/src/executor/model/ModelGenerate.ts +3 -1
  71. package/src/executor/model/ModelStepRunner.ts +36 -5
  72. package/src/executor/model/ModelStreamFailure.ts +44 -0
  73. package/src/group/GroupDispatchSession.ts +175 -0
  74. package/src/group/GroupSession.ts +83 -28
  75. package/src/index.ts +10 -0
  76. package/src/session/messages/SessionAssistantMessageWriter.ts +9 -5
  77. package/src/types/group/DispatchStrategy.ts +79 -19
  78. package/src/types/group/GroupDispatchSession.ts +80 -0
  79. package/src/types/group/GroupSession.ts +2 -0
  80. package/src/types/group/GroupSessionStore.ts +6 -1
  81. package/src/types/plugin/PluginNotification.ts +38 -0
  82. package/src/workspace/store/LocalGroupDispatchSessionDataStore.ts +118 -0
  83. package/src/workspace/store/LocalGroupSessionDataStore.ts +35 -9
  84. package/tsconfig.tsbuildinfo +1 -1
@@ -34,6 +34,7 @@ import {
34
34
  type ModelStepResult,
35
35
  type ModelStepToolCall,
36
36
  } from "@executor/model/ModelStepRunner.js";
37
+ import { is_retryable_empty_model_stream_failure } from "@executor/model/ModelStreamFailure.js";
37
38
  import { CoreEngineMessageState } from "@executor/core-engine/CoreEngineMessageState.js";
38
39
  import {
39
40
  deep_compact_model_messages,
@@ -58,6 +59,10 @@ const TURN_STOPPED_MESSAGE = "Turn stopped";
58
59
 
59
60
  /** Provider context-length error 在当前 step 内最多压缩重试三次。 */
60
61
  const MAX_CONTEXT_ERROR_COMPACTION_RETRIES = 3;
62
+ /** 无任何可见输出时,临时 Provider 流错误的最大自动重试次数。 */
63
+ const MAX_EMPTY_STREAM_RETRIES = 2;
64
+ /** 无输出流错误的重试退避,避免立即重复冲击同一 Provider。 */
65
+ const EMPTY_STREAM_RETRY_DELAYS_MS = [300, 1_000] as const;
61
66
 
62
67
  interface CoreEngineRunnerOptions {
63
68
  /** 当前 Session 稳定标识。 */
@@ -181,6 +186,7 @@ export class CoreEngineRunner {
181
186
 
182
187
  let incomplete_response_recovery_count = 0;
183
188
  let context_error_compaction_retries = 0;
189
+ let empty_stream_retry_count = 0;
184
190
  let compact_pending = false;
185
191
  let compact_validation_pending = Boolean(
186
192
  persisted_compaction_summary_id &&
@@ -293,6 +299,24 @@ export class CoreEngineRunner {
293
299
  ) {
294
300
  await input.turn_context.output.assistant.abort_step();
295
301
  }
302
+ if (
303
+ is_retryable_empty_model_stream_failure(error) &&
304
+ empty_stream_retry_count < MAX_EMPTY_STREAM_RETRIES
305
+ ) {
306
+ empty_stream_retry_count += 1;
307
+ await this.logger.log("warn", "[agent] model_stream.retry", {
308
+ session_id,
309
+ retry_count: empty_stream_retry_count,
310
+ error_code: error.code,
311
+ error: error.message,
312
+ provider_request_id: error.provider_request_id ?? null,
313
+ });
314
+ await wait_for_stream_retry(
315
+ EMPTY_STREAM_RETRY_DELAYS_MS[empty_stream_retry_count - 1] ?? 1_000,
316
+ input.turn_context.lifecycle.abort_signal,
317
+ );
318
+ continue;
319
+ }
296
320
  const compact_error = this.should_compact_on_error(error)
297
321
  ? error
298
322
  : last_observed_stream_error;
@@ -328,6 +352,7 @@ export class CoreEngineRunner {
328
352
  }
329
353
 
330
354
  context_error_compaction_retries = 0;
355
+ empty_stream_retry_count = 0;
331
356
  const last_step = executed_steps[executed_steps.length - 1];
332
357
  if (!last_step) break;
333
358
 
@@ -533,6 +558,25 @@ export class CoreEngineRunner {
533
558
  }
534
559
  }
535
560
 
561
+ /** 等待下一次模型流重试,并在 Turn 停止时立即结束等待。 */
562
+ async function wait_for_stream_retry(
563
+ delay_ms: number,
564
+ abort_signal: AbortSignal,
565
+ ): Promise<void> {
566
+ if (abort_signal.aborted) throw abort_signal.reason;
567
+ await new Promise<void>((resolve, reject) => {
568
+ const timer = setTimeout(() => {
569
+ abort_signal.removeEventListener("abort", on_abort);
570
+ resolve();
571
+ }, delay_ms);
572
+ const on_abort = (): void => {
573
+ clearTimeout(timer);
574
+ reject(abort_signal.reason);
575
+ };
576
+ abort_signal.addEventListener("abort", on_abort, { once: true });
577
+ });
578
+ }
579
+
536
580
  /** 在工具执行前接入 Session canonical Interaction 生命周期。 */
537
581
  async function resolve_tool_approval(input: {
538
582
  call: ModelStepToolCall;
@@ -51,7 +51,9 @@ export async function generate_model(
51
51
  tools.set(event.content_id, { tool_call_id: event.tool_call_id, tool_name: event.tool_name });
52
52
  } else if (event.type === "tool_call_finish") {
53
53
  const tool = tools.get(event.content_id);
54
- if (tool) tool_calls.push({ type: "tool_call", ...tool, input: event.input });
54
+ if (tool && !event.input_error) {
55
+ tool_calls.push({ type: "tool_call", ...tool, input: event.input });
56
+ }
55
57
  } else if (event.type === "model_usage") usage = event.usage;
56
58
  else if (event.type === "model_finish") finish_reason = event.finish_reason;
57
59
  }
@@ -26,6 +26,7 @@ import type {
26
26
  SessionAssistantMessagePart,
27
27
  SessionAssistantToolPart,
28
28
  } from "@/types/session/SessionMessage.js";
29
+ import { ModelStreamFailure } from "@/executor/model/ModelStreamFailure.js";
29
30
 
30
31
  /** 单个工具调用的执行事实。 */
31
32
  export interface ModelStepToolCall {
@@ -35,6 +36,8 @@ export interface ModelStepToolCall {
35
36
  tool_name: string;
36
37
  /** 已解析工具输入。 */
37
38
  input: ModelJsonValue;
39
+ /** 模型生成的工具输入无法解析时记录的错误。 */
40
+ input_error?: string;
38
41
  }
39
42
 
40
43
  /** 单个工具结果的执行事实。 */
@@ -162,7 +165,14 @@ class StepEventCollector {
162
165
 
163
166
  /** 消费单个标准模型事件。 */
164
167
  accept(event: ModelStreamEvent): void {
165
- if (event.type === "model_error") throw new Error(event.error.message);
168
+ if (event.type === "model_error") {
169
+ throw new ModelStreamFailure(
170
+ event.error,
171
+ this.text.length > 0 ||
172
+ this.reasoning_by_id.size > 0 ||
173
+ this.tool_by_content_id.size > 0,
174
+ );
175
+ }
166
176
  if (event.type === "text_start") {
167
177
  this.text_by_id.set(event.content_id, "");
168
178
  } else if (event.type === "text_delta") {
@@ -213,6 +223,7 @@ class StepEventCollector {
213
223
  const tool = this.tool_by_content_id.get(event.content_id);
214
224
  if (tool) {
215
225
  tool.input = event.input;
226
+ if (event.input_error) tool.input_error = event.input_error;
216
227
  this.model_content.push({
217
228
  type: "tool_call",
218
229
  tool_call_id: tool.tool_call_id,
@@ -265,6 +276,14 @@ async function execute_tools(
265
276
  ): Promise<ModelStepToolResult[]> {
266
277
  const results: ModelStepToolResult[] = [];
267
278
  for (const call of calls) {
279
+ if (call.input_error) {
280
+ results.push({
281
+ ...call,
282
+ success: false,
283
+ output: { error: call.input_error },
284
+ });
285
+ continue;
286
+ }
268
287
  const tool = input.tools[call.tool_name];
269
288
  if (!tool || typeof tool.execute !== "function") {
270
289
  results.push({
@@ -292,7 +311,11 @@ async function execute_tools(
292
311
  abort_signal: input.abort_signal,
293
312
  };
294
313
  const output = await tool.execute(call.input, options);
295
- results.push({ ...call, success: true, output });
314
+ results.push({
315
+ ...call,
316
+ success: !is_structured_tool_failure(output),
317
+ output,
318
+ });
296
319
  } catch (error) {
297
320
  results.push({
298
321
  ...call,
@@ -304,6 +327,12 @@ async function execute_tools(
304
327
  return results;
305
328
  }
306
329
 
330
+ /** 识别 Tool 正常返回的结构化失败,避免把业务失败标记成执行成功。 */
331
+ function is_structured_tool_failure(output: unknown): boolean {
332
+ if (!output || typeof output !== "object" || Array.isArray(output)) return false;
333
+ return (output as { success?: unknown }).success === false;
334
+ }
335
+
307
336
  /** 把 System 快照转换成标准 ModelMessage。 */
308
337
  function convert_system_messages(messages: SessionSystemMessage[]): ModelMessage[] {
309
338
  return messages.map((message) => ({
@@ -373,9 +402,11 @@ function append_tool_results(
373
402
 
374
403
  /** 从结构化工具失败结果中提取可展示错误文本。 */
375
404
  function read_tool_error(value: unknown): string {
376
- if (value && typeof value === "object" && "error" in value) {
377
- const error = (value as { error?: unknown }).error;
378
- if (typeof error === "string" && error.trim()) return error;
405
+ if (value && typeof value === "object") {
406
+ const result = value as { error?: unknown; message?: unknown; output?: unknown };
407
+ for (const candidate of [result.error, result.message, result.output]) {
408
+ if (typeof candidate === "string" && candidate.trim()) return candidate;
409
+ }
379
410
  }
380
411
  return "Tool execution failed";
381
412
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 模型流失败对象。
3
+ *
4
+ * 该对象完整保留 Model Protocol 的错误分类,避免 Executor 只能依赖错误文本
5
+ * 判断是否可以重试。它只描述一次模型流失败,不负责决定恢复策略。
6
+ */
7
+
8
+ import type { ModelError } from "@downcity/type";
9
+
10
+ /** 一次具有稳定协议语义的模型流失败。 */
11
+ export class ModelStreamFailure extends Error {
12
+ /** Downcity 稳定模型错误码。 */
13
+ readonly code: ModelError["code"];
14
+
15
+ /** 当前模型调用是否可以安全重试。 */
16
+ readonly retryable: boolean;
17
+
18
+ /** 失败前是否已经向用户发布正文或工具输入。 */
19
+ readonly has_partial_output: boolean;
20
+
21
+ /** Provider 返回的可观测请求标识。 */
22
+ readonly provider_request_id?: string;
23
+
24
+ constructor(error: ModelError, has_partial_output: boolean) {
25
+ super(error.message);
26
+ this.name = "ModelStreamFailure";
27
+ this.code = error.code;
28
+ this.retryable = error.retryable;
29
+ this.has_partial_output = has_partial_output;
30
+ if (error.provider_request_id) {
31
+ this.provider_request_id = error.provider_request_id;
32
+ }
33
+ }
34
+ }
35
+
36
+ /** 判断未知错误是否为可安全自动重试的无输出模型流失败。 */
37
+ export function is_retryable_empty_model_stream_failure(
38
+ error: unknown,
39
+ ): error is ModelStreamFailure {
40
+ return error instanceof ModelStreamFailure &&
41
+ error.retryable &&
42
+ !error.has_partial_output &&
43
+ error.code !== "cancelled";
44
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * GroupDispatchSession:单个 GroupSession 独享的持久化调度执行边界。
3
+ *
4
+ * 本对象统一拥有调度队列、当前模型取消信号和调度 Turn 状态。调度策略只负责决策,
5
+ * 不再自行拥有跨 GroupSession 的运行状态或停止语义。
6
+ */
7
+
8
+ import { nanoid } from "nanoid";
9
+ import type {
10
+ GroupDispatchSessionDataStore,
11
+ GroupDispatchSessionInput,
12
+ GroupDispatchSessionOptions,
13
+ GroupDispatchSessionResult,
14
+ GroupDispatchTurnRecord,
15
+ } from "@/types/group/GroupDispatchSession.js";
16
+
17
+ /** 主动停止 Dispatch Session 时使用的内部错误。 */
18
+ export class GroupDispatchStoppedError extends Error {
19
+ constructor() {
20
+ super("Group dispatch stopped");
21
+ this.name = "GroupDispatchStoppedError";
22
+ }
23
+ }
24
+
25
+ /** GroupSession 私有的调度 Session。 */
26
+ export class GroupDispatchSession {
27
+ private readonly dispatch_strategy: GroupDispatchSessionOptions["dispatch_strategy"];
28
+ private store?: GroupDispatchSessionDataStore;
29
+ private queue_tail: Promise<void> = Promise.resolve();
30
+ private active_abort_controller?: AbortController;
31
+ private stopping = false;
32
+ private disposed = false;
33
+
34
+ constructor(options: GroupDispatchSessionOptions) {
35
+ this.dispatch_strategy = options.dispatch_strategy;
36
+ }
37
+
38
+ /** 绑定持久化视图,并把进程中断留下的运行态明确收口为 stopped。 */
39
+ async initialize(store: GroupDispatchSessionDataStore): Promise<this> {
40
+ if (this.store && this.store !== store) {
41
+ throw new Error("GroupDispatchSession is already initialized with another Store");
42
+ }
43
+ if (this.store === store) return this;
44
+ this.store = store;
45
+ const interrupted_turns = (await store.list_turns()).filter((turn) => (
46
+ turn.status === "queued" || turn.status === "running"
47
+ ));
48
+ for (const turn of interrupted_turns) {
49
+ await store.append_turn({
50
+ ...turn,
51
+ status: "stopped",
52
+ error: "Group dispatch interrupted before recovery",
53
+ updated_at: Date.now(),
54
+ });
55
+ }
56
+ return this;
57
+ }
58
+
59
+ /** 按提交顺序执行一次调度,并在返回前持久化最终决定。 */
60
+ decide(input: GroupDispatchSessionInput): Promise<GroupDispatchSessionResult> {
61
+ this.assert_available();
62
+ const store = this.require_store();
63
+ const created_at = Date.now();
64
+ const queued_turn: GroupDispatchTurnRecord = {
65
+ dispatch_id: `group-dispatch-${nanoid(12)}`,
66
+ trigger: input.trigger,
67
+ message_id: input.message.id,
68
+ pending_message_ids: input.pending_messages.map((message) => message.id),
69
+ status: "queued",
70
+ created_at,
71
+ updated_at: created_at,
72
+ };
73
+ const persist_queued = store.append_turn(queued_turn);
74
+ const operation = this.queue_tail.then(async () => {
75
+ await persist_queued;
76
+ return await this.execute_turn(input, queued_turn);
77
+ });
78
+ this.queue_tail = operation.then(() => undefined, () => undefined);
79
+ return operation;
80
+ }
81
+
82
+ /** 中断当前模型调用,并让尚未开始的调度 Turn 按队列顺序收口。 */
83
+ async stop(): Promise<void> {
84
+ if (this.disposed || this.stopping) return;
85
+ this.stopping = true;
86
+ this.active_abort_controller?.abort(new GroupDispatchStoppedError());
87
+ await this.queue_tail;
88
+ this.active_abort_controller = undefined;
89
+ this.stopping = false;
90
+ }
91
+
92
+ /** 永久释放当前 Dispatch Session。 */
93
+ async dispose(): Promise<void> {
94
+ if (this.disposed) return;
95
+ await this.stop();
96
+ this.disposed = true;
97
+ this.store = undefined;
98
+ }
99
+
100
+ /** 执行队首调度 Turn,确保成功、失败和停止都形成持久化终态。 */
101
+ private async execute_turn(
102
+ input: GroupDispatchSessionInput,
103
+ queued_turn: GroupDispatchTurnRecord,
104
+ ): Promise<GroupDispatchSessionResult> {
105
+ const store = this.require_store();
106
+ if (this.stopping || this.disposed) {
107
+ await this.persist_terminal_turn(queued_turn, "stopped", "Group dispatch stopped");
108
+ throw new GroupDispatchStoppedError();
109
+ }
110
+ const abort_controller = new AbortController();
111
+ this.active_abort_controller = abort_controller;
112
+ await store.append_turn({
113
+ ...queued_turn,
114
+ status: "running",
115
+ updated_at: Date.now(),
116
+ });
117
+ try {
118
+ const decision = await this.dispatch_strategy.decide_dispatch({
119
+ ...input,
120
+ abort_signal: abort_controller.signal,
121
+ });
122
+ if (this.stopping || this.disposed || abort_controller.signal.aborted) {
123
+ throw new GroupDispatchStoppedError();
124
+ }
125
+ await store.append_turn({
126
+ ...queued_turn,
127
+ status: "completed",
128
+ decision,
129
+ updated_at: Date.now(),
130
+ });
131
+ return { dispatch_id: queued_turn.dispatch_id, decision };
132
+ } catch (error) {
133
+ if (
134
+ error instanceof GroupDispatchStoppedError ||
135
+ this.stopping ||
136
+ this.disposed ||
137
+ abort_controller.signal.aborted
138
+ ) {
139
+ await this.persist_terminal_turn(queued_turn, "stopped", "Group dispatch stopped");
140
+ throw new GroupDispatchStoppedError();
141
+ }
142
+ const detail = error instanceof Error ? error.message : String(error);
143
+ await this.persist_terminal_turn(queued_turn, "failed", detail);
144
+ throw error;
145
+ } finally {
146
+ if (this.active_abort_controller === abort_controller) {
147
+ this.active_abort_controller = undefined;
148
+ }
149
+ }
150
+ }
151
+
152
+ /** 提交没有 decision 的失败或停止终态。 */
153
+ private async persist_terminal_turn(
154
+ turn: GroupDispatchTurnRecord,
155
+ status: "failed" | "stopped",
156
+ error: string,
157
+ ): Promise<void> {
158
+ await this.require_store().append_turn({
159
+ ...turn,
160
+ status,
161
+ error,
162
+ updated_at: Date.now(),
163
+ });
164
+ }
165
+
166
+ private require_store(): GroupDispatchSessionDataStore {
167
+ if (!this.store) throw new Error("GroupDispatchSession is not initialized");
168
+ return this.store;
169
+ }
170
+
171
+ private assert_available(): void {
172
+ if (this.disposed) throw new Error("GroupDispatchSession is disposed");
173
+ if (this.stopping) throw new Error("GroupDispatchSession is stopping");
174
+ }
175
+ }
@@ -17,6 +17,11 @@ import type { DispatchDecision, DispatchNode, DispatchStrategy } from "@/types/g
17
17
  import type { WorkspaceBase } from "@downcity/workspace";
18
18
  import type { GroupSessionDataStore } from "@/types/group/GroupSessionStore.js";
19
19
  import type { RespondSessionInteractionInput } from "@/types/session/SessionInteraction.js";
20
+ import {
21
+ GroupDispatchSession,
22
+ GroupDispatchStoppedError,
23
+ } from "@/group/GroupDispatchSession.js";
24
+ import type { GroupDispatchSessionResult } from "@/types/group/GroupDispatchSession.js";
20
25
 
21
26
  const max_auto_dispatch_count = 32;
22
27
 
@@ -47,7 +52,7 @@ export class GroupSession implements GroupSessionContract {
47
52
  private readonly group_name: string;
48
53
  private readonly instruction?: string;
49
54
  private readonly members: readonly Agent[];
50
- private readonly dispatch_strategy: DispatchStrategy;
55
+ private readonly dispatch_session: GroupDispatchSession;
51
56
  private readonly workspace?: WorkspaceBase;
52
57
  private readonly messages_by_id: GroupMessage[] = [];
53
58
  private readonly subscribers = new Set<GroupEventSubscriber>();
@@ -77,7 +82,9 @@ export class GroupSession implements GroupSessionContract {
77
82
  this.group_name = options.group_name;
78
83
  this.instruction = options.instruction;
79
84
  this.members = options.members;
80
- this.dispatch_strategy = options.dispatch_strategy;
85
+ this.dispatch_session = new GroupDispatchSession({
86
+ dispatch_strategy: options.dispatch_strategy,
87
+ });
81
88
  this.workspace = options.workspace;
82
89
  this.workspace_id = options.workspace?.id;
83
90
  }
@@ -90,6 +97,7 @@ export class GroupSession implements GroupSessionContract {
90
97
  if (this.store === store) return this;
91
98
  this.store = store;
92
99
  await store.initialize();
100
+ await this.dispatch_session.initialize(store.dispatch_session);
93
101
  const metadata = await store.read_metadata();
94
102
  if (metadata.group_id !== this.group_id) {
95
103
  throw new Error(`GroupSession "${this.id}" belongs to another Group`);
@@ -112,7 +120,8 @@ export class GroupSession implements GroupSessionContract {
112
120
  turn_id: checkpoint.turn_id,
113
121
  root_message_id: checkpoint.root_message_id,
114
122
  context_message_ids: checkpoint.context_message_ids,
115
- auto_pending: true,
123
+ dispatch_stage: checkpoint.dispatch_stage,
124
+ auto_pending: checkpoint.dispatch_stage === "auto",
116
125
  stopped: false,
117
126
  recovered: true,
118
127
  });
@@ -151,7 +160,12 @@ export class GroupSession implements GroupSessionContract {
151
160
  if (Object.keys(valid_member_session_ids).length !== Object.keys(member_session_ids).length) {
152
161
  await store.update_metadata({ member_session_ids: valid_member_session_ids });
153
162
  }
154
- if (this.group_turns_by_id.size > 0 || this.auto_frontier_messages.length > 0) {
163
+ for (const group_turn of this.group_turns_by_id.values()) {
164
+ if (group_turn.dispatch_stage !== "user") continue;
165
+ const message = this.messages_by_id.find((item) => item.id === group_turn.root_message_id);
166
+ if (message) this.start_user_dispatch(message, group_turn);
167
+ }
168
+ if ([...this.group_turns_by_id.values()].some((turn) => turn.auto_pending) || this.auto_frontier_messages.length > 0) {
155
169
  this.request_auto_dispatch();
156
170
  }
157
171
  return this;
@@ -169,19 +183,14 @@ export class GroupSession implements GroupSessionContract {
169
183
  turn_id,
170
184
  root_message_id: message.id,
171
185
  context_message_ids: [...context_message_ids, message.id],
186
+ dispatch_stage: "user",
172
187
  auto_pending: false,
173
188
  stopped: false,
174
189
  };
175
190
  this.group_turns_by_id.set(turn_id, group_turn);
176
191
  await this.persist_dispatch_checkpoint();
177
192
  this.publish_status(turn_id, "dispatching", { message_id: message.id });
178
- const dispatch = this.run_user_dispatch(message, turn_id);
179
- group_turn.user_dispatch = dispatch;
180
- this.pending_deliveries.add(dispatch);
181
- void dispatch.finally(() => {
182
- this.pending_deliveries.delete(dispatch);
183
- if (group_turn.auto_pending) this.request_auto_dispatch();
184
- });
193
+ this.start_user_dispatch(message, group_turn);
185
194
  return { turn_id, success: true, message_count: this.messages_by_id.length };
186
195
  }
187
196
 
@@ -217,7 +226,10 @@ export class GroupSession implements GroupSessionContract {
217
226
  if (this.disposed) return;
218
227
  this.stop_requested = true;
219
228
  this.publish_status(undefined, "stopped");
220
- await Promise.allSettled([...this.member_sessions.values()].map((session) => session.stop()));
229
+ await Promise.allSettled([
230
+ this.dispatch_session.stop(),
231
+ ...[...this.member_sessions.values()].map((session) => session.stop()),
232
+ ]);
221
233
  await Promise.allSettled([...this.pending_deliveries]);
222
234
  if (this.auto_dispatch_promise) await this.auto_dispatch_promise;
223
235
  this.auto_dispatch_requested = false;
@@ -232,6 +244,7 @@ export class GroupSession implements GroupSessionContract {
232
244
  async dispose(): Promise<void> {
233
245
  if (this.disposed) return;
234
246
  await this.stop();
247
+ await this.dispatch_session.dispose();
235
248
  this.disposed = true;
236
249
  this.subscribers.clear();
237
250
  for (const unsubscribe of this.member_session_unsubscribes.values()) unsubscribe();
@@ -250,28 +263,53 @@ export class GroupSession implements GroupSessionContract {
250
263
  const group_turn = this.group_turns_by_id.get(turn_id);
251
264
  if (!group_turn) return;
252
265
  try {
253
- const decision = await this.decide_dispatch("user", message, [message]);
266
+ const dispatch = await this.decide_dispatch("user", message, [message]);
254
267
  const context_messages = group_turn.context_message_ids
255
268
  .map((message_id) => this.messages_by_id.find((item) => item.id === message_id))
256
269
  .filter((item): item is GroupMessage => Boolean(item));
257
- await this.run_dispatch_plan(decision, message, context_messages, turn_id);
270
+ await this.run_dispatch_plan(
271
+ dispatch.decision,
272
+ message,
273
+ context_messages,
274
+ turn_id,
275
+ dispatch.dispatch_id,
276
+ );
258
277
  if (this.stop_requested || this.disposed) {
259
278
  group_turn.stopped = true;
260
279
  this.group_turns_by_id.delete(turn_id);
261
280
  await this.persist_dispatch_checkpoint();
262
281
  return;
263
282
  }
264
- group_turn.auto_pending = true;
283
+ if (dispatch.decision.terminal) {
284
+ this.group_turns_by_id.delete(turn_id);
285
+ } else {
286
+ group_turn.dispatch_stage = "auto";
287
+ group_turn.auto_pending = true;
288
+ }
265
289
  await this.persist_dispatch_checkpoint();
290
+ if (dispatch.decision.terminal) this.publish_status(turn_id, "idle");
266
291
  } catch (error) {
267
292
  group_turn.stopped = this.stop_requested;
268
- await this.append_dispatch_failure(message, error);
293
+ if (!(error instanceof GroupDispatchStoppedError)) {
294
+ await this.append_dispatch_failure(message, error);
295
+ }
269
296
  this.group_turns_by_id.delete(turn_id);
270
297
  await this.persist_dispatch_checkpoint();
271
298
  this.publish_status(turn_id, this.stop_requested ? "stopped" : "failed");
272
299
  }
273
300
  }
274
301
 
302
+ /** 登记一次用户调度运行;新建和恢复必须经过同一个生命周期入口。 */
303
+ private start_user_dispatch(message: GroupMessage, group_turn: GroupTurnRuntime): void {
304
+ const dispatch = this.run_user_dispatch(message, group_turn.turn_id);
305
+ group_turn.user_dispatch = dispatch;
306
+ this.pending_deliveries.add(dispatch);
307
+ void dispatch.finally(() => {
308
+ this.pending_deliveries.delete(dispatch);
309
+ if (group_turn.auto_pending) this.request_auto_dispatch();
310
+ });
311
+ }
312
+
275
313
  /** GroupSession 唯一的自动调度循环,统一消费已完成成员产生的新消息。 */
276
314
  private request_auto_dispatch(): void {
277
315
  if (this.stop_requested || this.disposed) return;
@@ -299,8 +337,8 @@ export class GroupSession implements GroupSessionContract {
299
337
  const message = frontier_messages[frontier_messages.length - 1];
300
338
  this.publish_status(undefined, "dispatching", { message_id: message.id });
301
339
  try {
302
- const decision = await this.decide_dispatch("auto", message, frontier_messages);
303
- const dispatch_key = JSON.stringify(decision);
340
+ const dispatch = await this.decide_dispatch("auto", message, frontier_messages);
341
+ const dispatch_key = JSON.stringify(dispatch.decision);
304
342
  if (this.auto_dispatch_path_keys.has(dispatch_key)) {
305
343
  await this.append_message({ sender_type: "system", sender_id: "system", text: "Group auto dispatch detected a repeated path.", reply_to: message.id });
306
344
  frontier_messages = [];
@@ -311,8 +349,14 @@ export class GroupSession implements GroupSessionContract {
311
349
  continue;
312
350
  }
313
351
  this.auto_dispatch_path_keys.add(dispatch_key);
314
- frontier_messages = await this.run_dispatch_plan(decision, message, this.messages_by_id, undefined, `group-dispatch-${nanoid(12)}`);
315
- if (decision.terminal || frontier_messages.length === 0) {
352
+ frontier_messages = await this.run_dispatch_plan(
353
+ dispatch.decision,
354
+ message,
355
+ this.messages_by_id,
356
+ undefined,
357
+ dispatch.dispatch_id,
358
+ );
359
+ if (dispatch.decision.terminal || frontier_messages.length === 0) {
316
360
  frontier_messages = [];
317
361
  this.auto_frontier_messages = [];
318
362
  this.auto_dispatch_path_keys.clear();
@@ -324,6 +368,7 @@ export class GroupSession implements GroupSessionContract {
324
368
  this.auto_dispatch_requested = true;
325
369
  }
326
370
  } catch (error) {
371
+ if (error instanceof GroupDispatchStoppedError) return;
327
372
  await this.append_dispatch_failure(message, error);
328
373
  this.publish_status(undefined, "failed");
329
374
  frontier_messages = [];
@@ -361,16 +406,21 @@ export class GroupSession implements GroupSessionContract {
361
406
  batch_turns.forEach((group_turn) => { group_turn.auto_pending = false; });
362
407
  for (const message of candidate_messages) this.consumed_auto_message_ids.add(message.id);
363
408
  await this.persist_dispatch_checkpoint();
364
- const dispatch_id = `group-dispatch-${nanoid(12)}`;
365
409
  dispatch_count = 0;
366
410
  const message = candidate_messages[candidate_messages.length - 1];
367
411
  this.publish_status(undefined, "dispatching", { message_id: message.id });
368
412
  try {
369
- const decision = await this.decide_dispatch("auto", message, pending_messages);
413
+ const dispatch = await this.decide_dispatch("auto", message, pending_messages);
370
414
  const context_messages = this.messages_by_id.filter((item) => item.turn_id && batch_turn_ids.has(item.turn_id));
371
- const outputs = await this.run_dispatch_plan(decision, message, context_messages, undefined, dispatch_id);
415
+ const outputs = await this.run_dispatch_plan(
416
+ dispatch.decision,
417
+ message,
418
+ context_messages,
419
+ undefined,
420
+ dispatch.dispatch_id,
421
+ );
372
422
  for (const turn_id of batch_turn_ids) this.group_turns_by_id.delete(turn_id);
373
- if (decision.terminal || outputs.length === 0) {
423
+ if (dispatch.decision.terminal || outputs.length === 0) {
374
424
  this.auto_frontier_messages = [];
375
425
  await this.persist_dispatch_checkpoint();
376
426
  this.publish_status(undefined, "idle");
@@ -381,6 +431,7 @@ export class GroupSession implements GroupSessionContract {
381
431
  this.auto_dispatch_requested = true;
382
432
  }
383
433
  } catch (error) {
434
+ if (error instanceof GroupDispatchStoppedError) return;
384
435
  await this.append_dispatch_failure(message, error);
385
436
  this.publish_status(undefined, "failed");
386
437
  for (const turn_id of batch_turn_ids) this.group_turns_by_id.delete(turn_id);
@@ -394,8 +445,8 @@ export class GroupSession implements GroupSessionContract {
394
445
  trigger: "user" | "auto",
395
446
  message: GroupMessage,
396
447
  pending_messages: readonly GroupMessage[],
397
- ): Promise<DispatchDecision> {
398
- return await this.dispatch_strategy.decide_dispatch({
448
+ ): Promise<GroupDispatchSessionResult> {
449
+ return await this.dispatch_session.decide({
399
450
  trigger,
400
451
  message,
401
452
  pending_messages,
@@ -537,7 +588,10 @@ export class GroupSession implements GroupSessionContract {
537
588
  await this.append_member_failure(agent, message, result.error || "未知错误");
538
589
  return null;
539
590
  }
540
- if (!result.text?.trim()) return null;
591
+ if (!result.text?.trim()) {
592
+ await this.append_member_failure(agent, message, "未生成有效回复");
593
+ return null;
594
+ }
541
595
  const reply = await this.append_message({
542
596
  sender_type: "agent",
543
597
  sender_id: agent.id,
@@ -653,7 +707,7 @@ export class GroupSession implements GroupSessionContract {
653
707
  `Current message from ${message.sender_type === "agent" ? message.sender_id : "user"}: ${message.text}`,
654
708
  dispatch_instruction,
655
709
  `你只能代表成员 ${agent.id} 自己发言。不得代替、指挥、裁定、总结或转述其他成员;不得假装自己是其他成员;不得创建队长、裁判或协调者。`,
656
- "如果不需要你发言,返回空内容。",
710
+ "你已被 Group 调度选中,必须直接回复当前消息。",
657
711
  ].filter(Boolean).join("\n");
658
712
  }
659
713
 
@@ -695,6 +749,7 @@ export class GroupSession implements GroupSessionContract {
695
749
  turn_id: group_turn.turn_id,
696
750
  root_message_id: group_turn.root_message_id,
697
751
  context_message_ids: [...group_turn.context_message_ids],
752
+ dispatch_stage: group_turn.dispatch_stage,
698
753
  })),
699
754
  auto_frontier_message_ids: this.auto_frontier_messages.map((message) => message.id),
700
755
  });