@downcity/agent 1.1.401 → 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 (83) hide show
  1. package/README.md +4 -3
  2. package/bin/agent/WorkspaceEntry.js +1 -1
  3. package/bin/agent/WorkspaceEntry.js.map +1 -1
  4. package/bin/city/types/PluginHostContext.d.ts +3 -0
  5. package/bin/city/types/PluginHostContext.d.ts.map +1 -1
  6. package/bin/executor/core-engine/CoreEngineError.js +8 -2
  7. package/bin/executor/core-engine/CoreEngineError.js.map +1 -1
  8. package/bin/executor/core-engine/CoreEngineRunner.d.ts.map +1 -1
  9. package/bin/executor/core-engine/CoreEngineRunner.js +36 -0
  10. package/bin/executor/core-engine/CoreEngineRunner.js.map +1 -1
  11. package/bin/executor/model/ModelGenerate.d.ts.map +1 -1
  12. package/bin/executor/model/ModelGenerate.js +2 -1
  13. package/bin/executor/model/ModelGenerate.js.map +1 -1
  14. package/bin/executor/model/ModelStepRunner.d.ts +2 -0
  15. package/bin/executor/model/ModelStepRunner.d.ts.map +1 -1
  16. package/bin/executor/model/ModelStepRunner.js +33 -7
  17. package/bin/executor/model/ModelStepRunner.js.map +1 -1
  18. package/bin/executor/model/ModelStreamFailure.d.ts +22 -0
  19. package/bin/executor/model/ModelStreamFailure.d.ts.map +1 -0
  20. package/bin/executor/model/ModelStreamFailure.js +35 -0
  21. package/bin/executor/model/ModelStreamFailure.js.map +1 -0
  22. package/bin/group/GroupDispatchSession.d.ts +36 -0
  23. package/bin/group/GroupDispatchSession.d.ts.map +1 -0
  24. package/bin/group/GroupDispatchSession.js +154 -0
  25. package/bin/group/GroupDispatchSession.js.map +1 -0
  26. package/bin/group/GroupSession.d.ts +3 -1
  27. package/bin/group/GroupSession.d.ts.map +1 -1
  28. package/bin/group/GroupSession.js +65 -28
  29. package/bin/group/GroupSession.js.map +1 -1
  30. package/bin/index.d.ts +2 -0
  31. package/bin/index.d.ts.map +1 -1
  32. package/bin/index.js.map +1 -1
  33. package/bin/session/messages/SessionAssistantMessageWriter.d.ts.map +1 -1
  34. package/bin/session/messages/SessionAssistantMessageWriter.js +10 -6
  35. package/bin/session/messages/SessionAssistantMessageWriter.js.map +1 -1
  36. package/bin/types/group/DispatchStrategy.d.ts +3 -0
  37. package/bin/types/group/DispatchStrategy.d.ts.map +1 -1
  38. package/bin/types/group/DispatchStrategy.js +66 -18
  39. package/bin/types/group/DispatchStrategy.js.map +1 -1
  40. package/bin/types/group/GroupDispatchSession.d.ts +65 -0
  41. package/bin/types/group/GroupDispatchSession.d.ts.map +1 -0
  42. package/bin/types/group/GroupDispatchSession.js +8 -0
  43. package/bin/types/group/GroupDispatchSession.js.map +1 -0
  44. package/bin/types/group/GroupSession.d.ts +2 -0
  45. package/bin/types/group/GroupSession.d.ts.map +1 -1
  46. package/bin/types/group/GroupSessionStore.d.ts +6 -1
  47. package/bin/types/group/GroupSessionStore.d.ts.map +1 -1
  48. package/bin/types/plugin/PluginNotification.d.ts +31 -0
  49. package/bin/types/plugin/PluginNotification.d.ts.map +1 -0
  50. package/bin/types/plugin/PluginNotification.js +8 -0
  51. package/bin/types/plugin/PluginNotification.js.map +1 -0
  52. package/bin/workspace/store/LocalGroupDispatchSessionDataStore.d.ts +28 -0
  53. package/bin/workspace/store/LocalGroupDispatchSessionDataStore.d.ts.map +1 -0
  54. package/bin/workspace/store/LocalGroupDispatchSessionDataStore.js +102 -0
  55. package/bin/workspace/store/LocalGroupDispatchSessionDataStore.js.map +1 -0
  56. package/bin/workspace/store/LocalGroupSessionDataStore.d.ts +2 -0
  57. package/bin/workspace/store/LocalGroupSessionDataStore.d.ts.map +1 -1
  58. package/bin/workspace/store/LocalGroupSessionDataStore.js +25 -8
  59. package/bin/workspace/store/LocalGroupSessionDataStore.js.map +1 -1
  60. package/city-test/group.test.mjs +252 -1
  61. package/package.json +4 -4
  62. package/scripts/agent-multi-workspace.test.mjs +67 -0
  63. package/scripts/core-engine-context-compaction.test.mjs +49 -0
  64. package/scripts/core-engine-model-stream.test.mjs +69 -0
  65. package/src/agent/WorkspaceEntry.ts +1 -1
  66. package/src/city/types/PluginHostContext.ts +4 -0
  67. package/src/executor/core-engine/CoreEngineError.ts +8 -2
  68. package/src/executor/core-engine/CoreEngineRunner.ts +44 -0
  69. package/src/executor/model/ModelGenerate.ts +3 -1
  70. package/src/executor/model/ModelStepRunner.ts +36 -5
  71. package/src/executor/model/ModelStreamFailure.ts +44 -0
  72. package/src/group/GroupDispatchSession.ts +175 -0
  73. package/src/group/GroupSession.ts +83 -28
  74. package/src/index.ts +10 -0
  75. package/src/session/messages/SessionAssistantMessageWriter.ts +9 -5
  76. package/src/types/group/DispatchStrategy.ts +79 -19
  77. package/src/types/group/GroupDispatchSession.ts +80 -0
  78. package/src/types/group/GroupSession.ts +2 -0
  79. package/src/types/group/GroupSessionStore.ts +6 -1
  80. package/src/types/plugin/PluginNotification.ts +38 -0
  81. package/src/workspace/store/LocalGroupDispatchSessionDataStore.ts +118 -0
  82. package/src/workspace/store/LocalGroupSessionDataStore.ts +35 -9
  83. package/tsconfig.tsbuildinfo +1 -1
@@ -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
  });
package/src/index.ts CHANGED
@@ -26,6 +26,11 @@ export type {
26
26
  GroupSessionHistoryMeta,
27
27
  GroupSessionStore,
28
28
  } from "./types/group/GroupSessionStore.js";
29
+ export type {
30
+ GroupDispatchSessionDataStore,
31
+ GroupDispatchTurnRecord,
32
+ GroupDispatchTurnStatus,
33
+ } from "./types/group/GroupDispatchSession.js";
29
34
  export { GroupSession, GroupSessions } from "./group/index.js";
30
35
  export type {
31
36
  GroupMemberRuntime,
@@ -238,6 +243,11 @@ export type {
238
243
  } from "./types/sdk/AgentSessionCompact.js";
239
244
  export type { PluginContext } from "./types/plugin/PluginContext.js";
240
245
  export type { AgentPluginContext } from "./types/plugin/AgentPluginContext.js";
246
+ export type {
247
+ PluginNotificationInput,
248
+ PluginNotificationPublisher,
249
+ PluginNotificationTopicInput,
250
+ } from "./types/plugin/PluginNotification.js";
241
251
  export type {
242
252
  PluginWebServices,
243
253
  } from "./types/plugin/PluginServices.js";
@@ -215,7 +215,8 @@ export class SessionAssistantMessageWriter {
215
215
  /** 在单写队列中应用标准模型事件。 */
216
216
  private async apply_model_event_serialized(event: ModelStreamEvent): Promise<void> {
217
217
  if (this.closed) throw new Error("Assistant Message writer is closed");
218
- if (event.type === "model_error") throw new Error(event.error.message);
218
+ // model_error Executor 的恢复策略消费;Writer 保留已经写入的草稿事实。
219
+ if (event.type === "model_error") return;
219
220
  if (event.type === "text_start" || event.type === "reasoning_start") {
220
221
  const type = event.type === "text_start" ? "text" : "reasoning";
221
222
  const part_id = `${type}:${generate_id()}`;
@@ -283,8 +284,9 @@ export class SessionAssistantMessageWriter {
283
284
  const tool = this.require_tool(tool_call_id);
284
285
  await this.upsert_tool(tool_call_id, {
285
286
  tool_name: tool.tool_name,
286
- state: "ready",
287
+ state: event.input_error ? "failed" : "ready",
287
288
  input: event.input,
289
+ ...(event.input_error ? { error: event.input_error } : {}),
288
290
  });
289
291
  }
290
292
  }
@@ -435,9 +437,11 @@ export class SessionAssistantMessageWriter {
435
437
 
436
438
  /** 从 Tool 失败输出中提取稳定错误文本。 */
437
439
  function read_tool_error(output: unknown): string {
438
- if (output && typeof output === "object" && "error" in output) {
439
- const error = (output as { error?: unknown }).error;
440
- if (typeof error === "string" && error.trim()) return error;
440
+ if (output && typeof output === "object") {
441
+ const result = output as { error?: unknown; message?: unknown; output?: unknown };
442
+ for (const candidate of [result.error, result.message, result.output]) {
443
+ if (typeof candidate === "string" && candidate.trim()) return candidate;
444
+ }
441
445
  }
442
446
  return "Tool execution failed";
443
447
  }
@@ -3,7 +3,12 @@
3
3
  import type { GroupMessage } from "@/types/group/Group.js";
4
4
  import type { Agent } from "@/agent/Agent.js";
5
5
  import type { AgentModel } from "@/agent/AgentModel.js";
6
- import type { ModelClient, ModelJsonValue } from "@downcity/type";
6
+ import type {
7
+ ModelClient,
8
+ ModelContent,
9
+ ModelJsonValue,
10
+ ModelMessage,
11
+ } from "@downcity/type";
7
12
  import { generate_model } from "@executor/model/ModelGenerate.js";
8
13
  import { z } from "zod";
9
14
 
@@ -49,6 +54,8 @@ export interface DispatchStrategy {
49
54
  readonly messages: readonly GroupMessage[];
50
55
  /** 当前 Group 成员快照。 */
51
56
  readonly members: readonly Agent[];
57
+ /** 当前 Dispatch Session Turn 的取消信号。 */
58
+ readonly abort_signal: AbortSignal;
52
59
  }): Promise<DispatchDecision> | DispatchDecision;
53
60
  }
54
61
 
@@ -65,6 +72,8 @@ const dispatch_group_input_schema = z.object({
65
72
  next: z.enum(["stop", "continue"]),
66
73
  });
67
74
 
75
+ const max_dispatch_model_steps = 3;
76
+
68
77
  /** 使用 Group.model 理解群聊意图并通过内部 tool call 生成成员投递决定。 */
69
78
  export class AiDispatchStrategy implements DispatchStrategy {
70
79
  private readonly model?: ModelClient;
@@ -79,29 +88,45 @@ export class AiDispatchStrategy implements DispatchStrategy {
79
88
  readonly pending_messages: readonly GroupMessage[];
80
89
  readonly messages: readonly GroupMessage[];
81
90
  readonly members: readonly Agent[];
91
+ readonly abort_signal: AbortSignal;
82
92
  }): Promise<DispatchDecision> {
83
93
  if (!this.model) {
84
94
  throw new Error("Group requires a configured model for dispatch");
85
95
  }
86
96
  try {
87
- const result = await generate_model(this.model, {
88
- messages: [
89
- { role: "system", content: [{ type: "text", text: "你是群聊消息调度器。你只能调用 dispatch_group,不回答用户问题。普通任务默认只选择一个最合适的成员;只有用户明确要求多人分别回答,或确实存在并行的独立工作时,才把多个成员放在同一阶段。外层 steps 阶段按顺序执行,后一个阶段等待前一个阶段完成。只有真实存在的成员才能被选择。" }] },
90
- { role: "user", content: [{ type: "text", text: build_dispatch_prompt(input) }] },
91
- ],
92
- tools: [{
93
- name: "dispatch_group",
94
- description: "提交当前 Group 的成员投递路径。只调用一次;不要输出普通文本。没有成员需要回复时使用空 steps 和 next=stop。",
95
- input_schema: z.toJSONSchema(dispatch_group_input_schema) as Record<string, ModelJsonValue>,
96
- }],
97
- tool_choice: { type: "tool", tool_name: "dispatch_group" },
98
- max_output_tokens: 600,
99
- reasoning: { enabled: false },
100
- });
101
- if (result.tool_calls.length !== 1 || result.tool_calls[0]?.tool_name !== "dispatch_group") {
102
- throw new Error("Group dispatch model did not call dispatch_group");
97
+ const messages: ModelMessage[] = [
98
+ { role: "system", content: [{ type: "text", text: "你是群聊消息调度器。你只能调用 dispatch_group,不回答用户问题。普通任务默认只选择一个最合适的成员;只有用户明确要求多人分别回答,或确实存在并行的独立工作时,才把多个成员放在同一阶段。外层 steps 阶段按顺序执行,后一个阶段等待前一个阶段完成。只有真实存在的成员才能被选择。" }] },
99
+ { role: "user", content: [{ type: "text", text: build_dispatch_prompt(input) }] },
100
+ ];
101
+ let protocol_error = "Group dispatch model did not call dispatch_group";
102
+ for (let step_index = 0; step_index < max_dispatch_model_steps; step_index += 1) {
103
+ const result = await generate_model(this.model, {
104
+ messages,
105
+ tools: [{
106
+ name: "dispatch_group",
107
+ description: "提交当前 Group 的成员投递路径。只调用一次;不要输出普通文本。没有成员需要回复时使用空 steps 和 next=stop。",
108
+ input_schema: z.toJSONSchema(dispatch_group_input_schema) as Record<string, ModelJsonValue>,
109
+ }],
110
+ tool_choice: { type: "tool", tool_name: "dispatch_group" },
111
+ max_output_tokens: 600,
112
+ reasoning: { enabled: false },
113
+ }, input.abort_signal);
114
+ const dispatch_call = result.tool_calls.length === 1 && result.tool_calls[0]?.tool_name === "dispatch_group"
115
+ ? result.tool_calls[0]
116
+ : undefined;
117
+ if (dispatch_call) {
118
+ try {
119
+ return normalize_dispatch_tool_input(dispatch_call.input, input.members);
120
+ } catch (error) {
121
+ protocol_error = error instanceof Error ? error.message : String(error);
122
+ }
123
+ } else {
124
+ protocol_error = "Group dispatch model did not call dispatch_group exactly once";
125
+ }
126
+ if (step_index + 1 >= max_dispatch_model_steps) break;
127
+ append_dispatch_retry_messages(messages, result.text, result.tool_calls, protocol_error);
103
128
  }
104
- return normalize_dispatch_tool_input(result.tool_calls[0].input, input.members);
129
+ throw new Error(`${protocol_error} after ${max_dispatch_model_steps} attempts`);
105
130
  } catch (error) {
106
131
  const detail = error instanceof Error ? error.message : String(error);
107
132
  throw new Error(`Group dispatch model failed: ${detail}`, { cause: error });
@@ -109,6 +134,41 @@ export class AiDispatchStrategy implements DispatchStrategy {
109
134
  }
110
135
  }
111
136
 
137
+ /** 把协议错误作为下一模型 step 的显式上下文,允许模型在同一调度 Turn 内纠正。 */
138
+ function append_dispatch_retry_messages(
139
+ messages: ModelMessage[],
140
+ text: string,
141
+ tool_calls: readonly { type: "tool_call"; tool_call_id: string; tool_name: string; input: ModelJsonValue }[],
142
+ error: string,
143
+ ): void {
144
+ const assistant_content: ModelContent[] = [
145
+ ...(text.trim() ? [{ type: "text" as const, text }] : []),
146
+ ...tool_calls,
147
+ ];
148
+ if (assistant_content.length > 0) {
149
+ messages.push({ role: "assistant", content: assistant_content });
150
+ }
151
+ if (tool_calls.length > 0) {
152
+ messages.push({
153
+ role: "tool",
154
+ content: tool_calls.map((tool_call) => ({
155
+ type: "tool_result" as const,
156
+ tool_call_id: tool_call.tool_call_id,
157
+ tool_name: tool_call.tool_name,
158
+ outcome: "failed" as const,
159
+ content: [{ type: "text" as const, text: error }],
160
+ })),
161
+ });
162
+ }
163
+ messages.push({
164
+ role: "user",
165
+ content: [{
166
+ type: "text",
167
+ text: `上一响应不符合调度协议:${error}。请只调用一次 dispatch_group。`,
168
+ }],
169
+ });
170
+ }
171
+
112
172
  /** 为 AI 调度器构造稳定、有限长度的群聊上下文。 */
113
173
  function build_dispatch_prompt(input: {
114
174
  readonly trigger: DispatchTrigger;
@@ -152,7 +212,7 @@ function normalize_dispatch_tool_input(value: unknown, members: readonly Agent[]
152
212
  member_ids: unique_member_ids,
153
213
  response_mode: unique_member_ids.length > 1 ? "parallel" as const : "single" as const,
154
214
  depends_on_node_ids: index === 0 ? [] : [`step-${index - 1}`],
155
- instruction: "根据当前 Group 上下文判断是否需要回复,只代表自己发言;不需要时返回空内容。",
215
+ instruction: "你已被 Group 调度选中,请直接针对当前消息给出回复,并且只代表自己发言。",
156
216
  };
157
217
  });
158
218
  if (nodes.length === 0 && parsed.next === "continue") {
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Group Dispatch Session 领域类型。
3
+ *
4
+ * Dispatch Session 只记录群聊调度 Turn,不复制 GroupMessage 正文;消息正文仍由
5
+ * GroupSession 的消息日志作为唯一事实源,调度记录通过消息 ID 建立稳定引用。
6
+ */
7
+
8
+ import type { Agent } from "@/agent/Agent.js";
9
+ import type { GroupMessage } from "@/types/group/Group.js";
10
+ import type {
11
+ DispatchDecision,
12
+ DispatchStrategy,
13
+ DispatchTrigger,
14
+ } from "@/types/group/DispatchStrategy.js";
15
+
16
+ /** Dispatch Session 中一次调度 Turn 的持久化状态。 */
17
+ export type GroupDispatchTurnStatus =
18
+ | "queued"
19
+ | "running"
20
+ | "completed"
21
+ | "failed"
22
+ | "stopped";
23
+
24
+ /** Dispatch Session 中一次调度 Turn 的完整持久化快照。 */
25
+ export interface GroupDispatchTurnRecord {
26
+ /** 当前调度 Turn 的稳定标识。 */
27
+ readonly dispatch_id: string;
28
+ /** 本次调度由用户消息还是自动传播触发。 */
29
+ readonly trigger: DispatchTrigger;
30
+ /** 当前调度所围绕的 GroupMessage 标识。 */
31
+ readonly message_id: string;
32
+ /** 当前调度需要一起判断的新增 GroupMessage 标识。 */
33
+ readonly pending_message_ids: readonly string[];
34
+ /** 当前调度 Turn 的生命周期状态。 */
35
+ readonly status: GroupDispatchTurnStatus;
36
+ /** 调度成功后已经校验并持久化的成员投递决定。 */
37
+ readonly decision?: DispatchDecision;
38
+ /** 调度失败或停止时用于诊断的稳定错误文本。 */
39
+ readonly error?: string;
40
+ /** 当前调度 Turn 首次入队的 Unix 毫秒时间戳。 */
41
+ readonly created_at: number;
42
+ /** 当前状态最近一次提交的 Unix 毫秒时间戳。 */
43
+ readonly updated_at: number;
44
+ }
45
+
46
+ /** Dispatch Session 持久化所需的最小数据能力。 */
47
+ export interface GroupDispatchSessionDataStore {
48
+ /** 读取每个 dispatch_id 的最新调度 Turn 快照。 */
49
+ list_turns(): Promise<GroupDispatchTurnRecord[]>;
50
+ /** 以追加日志方式提交一个调度 Turn 的最新完整快照。 */
51
+ append_turn(turn: GroupDispatchTurnRecord): Promise<void>;
52
+ }
53
+
54
+ /** Dispatch Session 成功完成一次调度后的运行结果。 */
55
+ export interface GroupDispatchSessionResult {
56
+ /** 当前调度 Turn 的稳定标识。 */
57
+ readonly dispatch_id: string;
58
+ /** 已经校验并持久化的成员投递决定。 */
59
+ readonly decision: DispatchDecision;
60
+ }
61
+
62
+ /** 创建 Group Dispatch Session 的运行依赖。 */
63
+ export interface GroupDispatchSessionOptions {
64
+ /** 当前 GroupSession 使用的消息调度策略。 */
65
+ readonly dispatch_strategy: DispatchStrategy;
66
+ }
67
+
68
+ /** 向 Group Dispatch Session 提交一次调度判断的输入快照。 */
69
+ export interface GroupDispatchSessionInput {
70
+ /** 本次调度由用户消息还是自动传播触发。 */
71
+ readonly trigger: DispatchTrigger;
72
+ /** 当前调度所围绕的 GroupMessage。 */
73
+ readonly message: GroupMessage;
74
+ /** 当前调度需要一起判断的新增 GroupMessage。 */
75
+ readonly pending_messages: readonly GroupMessage[];
76
+ /** 当前 GroupSession 的完整共享消息快照。 */
77
+ readonly messages: readonly GroupMessage[];
78
+ /** 当前 Group 的成员快照。 */
79
+ readonly members: readonly Agent[];
80
+ }
@@ -143,6 +143,8 @@ export interface GroupTurnRuntime {
143
143
  readonly root_message_id: string;
144
144
  /** 用户消息写入时冻结的上下文消息标识。 */
145
145
  readonly context_message_ids: readonly string[];
146
+ /** 当前 Turn 位于初始用户调度还是自动传播阶段。 */
147
+ dispatch_stage: "user" | "auto";
146
148
  /** 当前 GroupTurn 的异步 user dispatch。 */
147
149
  user_dispatch?: Promise<void>;
148
150
  /** 当前 GroupTurn 是否等待 GroupSession 的 auto dispatch。 */
@@ -1,12 +1,13 @@
1
1
  /** GroupSession 持久化协议:只描述 GroupSession 所需的底层数据能力。 */
2
2
 
3
3
  import type { GroupMessage } from "@/types/group/Group.js";
4
+ import type { GroupDispatchSessionDataStore } from "@/types/group/GroupDispatchSession.js";
4
5
  import type { FileSystem } from "@downcity/workspace";
5
6
 
6
7
  /** GroupSession 的持久化元数据。 */
7
8
  export interface GroupSessionHistoryMeta {
8
9
  /** metadata schema 版本。 */
9
- readonly v: 1;
10
+ readonly v: 2;
10
11
  /** 当前 GroupSession 标识。 */
11
12
  readonly session_id: string;
12
13
  /** 所属 Group 标识。 */
@@ -37,12 +38,16 @@ export interface GroupSessionTurnCheckpoint {
37
38
  readonly root_message_id: string;
38
39
  /** 用户消息写入时冻结的上下文消息标识。 */
39
40
  readonly context_message_ids: readonly string[];
41
+ /** 恢复后应重新执行初始用户调度,还是继续等待自动传播收口。 */
42
+ readonly dispatch_stage: "user" | "auto";
40
43
  }
41
44
 
42
45
  /** 单个 GroupSession 的持久化数据视图。 */
43
46
  export interface GroupSessionDataStore {
44
47
  /** 当前 GroupSession 标识。 */
45
48
  readonly session_id: string;
49
+ /** 当前 GroupSession 独享的 Dispatch Session 持久化视图。 */
50
+ readonly dispatch_session: GroupDispatchSessionDataStore;
46
51
  /** 初始化当前 GroupSession 的存储布局。 */
47
52
  initialize(): Promise<void>;
48
53
  /** 读取完整共享消息历史。 */
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Agent Plugin 向宿主发布用户注意事项的通知协议。
3
+ *
4
+ * Plugin 只描述通知内容与自身工作区路由,不感知 Desktop、Dock、持久化或已读策略。
5
+ * 宿主负责绑定 Plugin 身份、聚合通知并决定具体呈现方式。
6
+ */
7
+
8
+ import type { JsonObject } from "@/types/common/Json.js";
9
+
10
+ /** Agent Plugin 发布的一条宿主通知。 */
11
+ export interface PluginNotificationInput {
12
+ /** Plugin 内稳定的聚合键;同一键的新通知替换旧的未读通知。 */
13
+ readonly topic_key: string;
14
+
15
+ /** 简短的用户可见标题。 */
16
+ readonly title: string;
17
+
18
+ /** 可选的用户可见补充说明。 */
19
+ readonly body?: string;
20
+
21
+ /** 打开通知时使用的当前 Plugin 工作区 JSON 路由。 */
22
+ readonly route?: JsonObject;
23
+ }
24
+
25
+ /** Plugin 要清除的一项本地通知主题。 */
26
+ export interface PluginNotificationTopicInput {
27
+ /** Plugin 内稳定的聚合键。 */
28
+ readonly topic_key: string;
29
+ }
30
+
31
+ /** 宿主注入给 Agent Plugin factory 的通知发布能力。 */
32
+ export interface PluginNotificationPublisher {
33
+ /** 发布一条由当前 Plugin 拥有的通知。 */
34
+ publish(input: PluginNotificationInput): Promise<void>;
35
+
36
+ /** 清除当前 Plugin 命名空间内一个主题的未读通知。 */
37
+ dismiss(input: PluginNotificationTopicInput): Promise<void>;
38
+ }