@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
@@ -0,0 +1,118 @@
1
+ /**
2
+ * LocalGroupDispatchSessionDataStore:Group Dispatch Session 的追加式 Turn 日志。
3
+ *
4
+ * 每次状态迁移都追加完整快照;读取时按 dispatch_id 选择最后一条记录,因此中断前
5
+ * 已提交的状态不会被覆盖,文件尾部的非完整写入也可以安全修复。
6
+ */
7
+
8
+ import path from "node:path";
9
+ import type { FileSystem } from "@downcity/workspace";
10
+ import type {
11
+ GroupDispatchSessionDataStore,
12
+ GroupDispatchTurnRecord,
13
+ GroupDispatchTurnStatus,
14
+ } from "@/types/group/GroupDispatchSession.js";
15
+
16
+ const dispatch_turn_statuses = new Set<GroupDispatchTurnStatus>([
17
+ "queued",
18
+ "running",
19
+ "completed",
20
+ "failed",
21
+ "stopped",
22
+ ]);
23
+
24
+ /** 基于 GroupSession 私有目录的 Dispatch Session Store。 */
25
+ export class LocalGroupDispatchSessionDataStore implements GroupDispatchSessionDataStore {
26
+ private readonly files: FileSystem;
27
+ private readonly dispatch_root_path: string;
28
+ private readonly turns_file_path: string;
29
+ private readonly transaction_lock_path: string;
30
+
31
+ constructor(options: {
32
+ /** 当前 Group StorageScope 文件能力。 */
33
+ readonly files: FileSystem;
34
+ /** 当前 GroupSession 的私有存储根目录。 */
35
+ readonly session_root_path: string;
36
+ }) {
37
+ this.files = options.files;
38
+ this.dispatch_root_path = path.join(options.session_root_path, "dispatch");
39
+ this.turns_file_path = path.join(this.dispatch_root_path, "turns.jsonl");
40
+ this.transaction_lock_path = path.join(this.dispatch_root_path, ".lock");
41
+ }
42
+
43
+ /** 读取每个调度 Turn 最后一次成功提交的状态。 */
44
+ async list_turns(): Promise<GroupDispatchTurnRecord[]> {
45
+ if (!(await this.files.path_exists(this.turns_file_path))) return [];
46
+ await this.files.ensure_directory(this.dispatch_root_path);
47
+ return await this.files.with_file_lock(this.transaction_lock_path, async () => {
48
+ const records = await this.read_records(true);
49
+ const turns_by_id = new Map<string, GroupDispatchTurnRecord>();
50
+ for (const record of records) {
51
+ turns_by_id.delete(record.dispatch_id);
52
+ turns_by_id.set(record.dispatch_id, record);
53
+ }
54
+ return [...turns_by_id.values()];
55
+ });
56
+ }
57
+
58
+ /** 追加一个完整状态快照,状态提交与其它调度 Turn 串行。 */
59
+ async append_turn(turn: GroupDispatchTurnRecord): Promise<void> {
60
+ assert_dispatch_turn(turn);
61
+ await this.files.ensure_directory(this.dispatch_root_path);
62
+ await this.files.with_file_lock(this.transaction_lock_path, async () => {
63
+ await this.files.append_file(this.turns_file_path, `${JSON.stringify(turn)}\n`);
64
+ });
65
+ }
66
+
67
+ /** 读取状态日志;只允许修复进程中断产生的最后一条残缺记录。 */
68
+ private async read_records(repair_tail: boolean): Promise<GroupDispatchTurnRecord[]> {
69
+ const content = (await this.files.read_file(this.turns_file_path)).toString("utf8");
70
+ const lines = content.split("\n");
71
+ const records: GroupDispatchTurnRecord[] = [];
72
+ for (let line_index = 0; line_index < lines.length; line_index += 1) {
73
+ const line = lines[line_index];
74
+ if (!line.trim()) continue;
75
+ try {
76
+ const record = JSON.parse(line) as GroupDispatchTurnRecord;
77
+ assert_dispatch_turn(record);
78
+ records.push(record);
79
+ } catch (error) {
80
+ const is_tail = lines.slice(line_index + 1).every((item) => !item.trim());
81
+ if (!repair_tail || !is_tail) throw error;
82
+ const repaired = records.map((record) => JSON.stringify(record)).join("\n");
83
+ await this.files.write_file_atomically(
84
+ this.turns_file_path,
85
+ repaired ? `${repaired}\n` : "",
86
+ );
87
+ break;
88
+ }
89
+ }
90
+ return records;
91
+ }
92
+ }
93
+
94
+ /** 防止损坏的持久化记录进入 Dispatch Session 恢复流程。 */
95
+ function assert_dispatch_turn(value: GroupDispatchTurnRecord): void {
96
+ if (!value || typeof value !== "object") throw new Error("Invalid Group dispatch turn record");
97
+ if (typeof value.dispatch_id !== "string" || !value.dispatch_id.trim()) {
98
+ throw new Error("Invalid Group dispatch turn id");
99
+ }
100
+ if (value.trigger !== "user" && value.trigger !== "auto") {
101
+ throw new Error(`Invalid Group dispatch trigger: ${String(value.trigger)}`);
102
+ }
103
+ if (typeof value.message_id !== "string" || !value.message_id.trim()) {
104
+ throw new Error(`Invalid Group dispatch message id: ${value.dispatch_id}`);
105
+ }
106
+ if (!Array.isArray(value.pending_message_ids) || value.pending_message_ids.some((item) => typeof item !== "string" || !item.trim())) {
107
+ throw new Error(`Invalid Group dispatch pending messages: ${value.dispatch_id}`);
108
+ }
109
+ if (!dispatch_turn_statuses.has(value.status)) {
110
+ throw new Error(`Invalid Group dispatch status: ${String(value.status)}`);
111
+ }
112
+ if (!Number.isFinite(value.created_at) || !Number.isFinite(value.updated_at)) {
113
+ throw new Error(`Invalid Group dispatch timestamps: ${value.dispatch_id}`);
114
+ }
115
+ if (value.status === "completed" && !value.decision) {
116
+ throw new Error(`Completed Group dispatch is missing its decision: ${value.dispatch_id}`);
117
+ }
118
+ }
@@ -3,6 +3,8 @@
3
3
  import path from "node:path";
4
4
  import type { FileSystem } from "@downcity/workspace";
5
5
  import type { GroupMessage } from "@/types/group/Group.js";
6
+ import type { GroupDispatchSessionDataStore } from "@/types/group/GroupDispatchSession.js";
7
+ import { LocalGroupDispatchSessionDataStore } from "@/workspace/store/LocalGroupDispatchSessionDataStore.js";
6
8
  import type {
7
9
  GroupSessionDataStore,
8
10
  GroupSessionHistoryMeta,
@@ -12,6 +14,7 @@ import type {
12
14
  /** 单个 GroupSession 的本地文件存储。 */
13
15
  export class LocalGroupSessionDataStore implements GroupSessionDataStore {
14
16
  readonly session_id: string;
17
+ readonly dispatch_session: GroupDispatchSessionDataStore;
15
18
  private readonly files: FileSystem;
16
19
  private readonly group_id: string;
17
20
  private readonly session_root_path: string;
@@ -38,6 +41,10 @@ export class LocalGroupSessionDataStore implements GroupSessionDataStore {
38
41
  "sessions",
39
42
  encodeURIComponent(this.session_id),
40
43
  );
44
+ this.dispatch_session = new LocalGroupDispatchSessionDataStore({
45
+ files: this.files,
46
+ session_root_path: this.session_root_path,
47
+ });
41
48
  this.metadata_file_path = path.join(this.session_root_path, "meta.json");
42
49
  this.messages_file_path = path.join(this.session_root_path, "messages.jsonl");
43
50
  this.transaction_lock_path = path.join(this.session_root_path, ".lock");
@@ -82,7 +89,10 @@ export class LocalGroupSessionDataStore implements GroupSessionDataStore {
82
89
  if (!(await this.files.path_exists(this.metadata_file_path))) {
83
90
  return this.create_initial_metadata();
84
91
  }
85
- return await this.read_metadata_unlocked();
92
+ return await this.files.with_file_lock(
93
+ this.transaction_lock_path,
94
+ async () => await this.read_metadata_unlocked(),
95
+ );
86
96
  }
87
97
 
88
98
  /** 在调用方已经持有事务锁时读取 metadata。 */
@@ -92,15 +102,21 @@ export class LocalGroupSessionDataStore implements GroupSessionDataStore {
92
102
  }
93
103
  const raw = JSON.parse(
94
104
  (await this.files.read_file(this.metadata_file_path)).toString("utf8"),
95
- ) as Partial<GroupSessionHistoryMeta>;
105
+ ) as Omit<Partial<GroupSessionHistoryMeta>, "v" | "pending_turns"> & {
106
+ readonly v?: unknown;
107
+ readonly pending_turns?: unknown[];
108
+ };
109
+ if (raw.v !== 1 && raw.v !== 2) {
110
+ throw new Error(`Unsupported GroupSession metadata version: ${String(raw.v)}`);
111
+ }
96
112
  if (raw.session_id !== this.session_id) {
97
113
  throw new Error(`Invalid GroupSession ownership metadata: ${this.session_id}`);
98
114
  }
99
115
  if (raw.group_id && raw.group_id !== this.group_id) {
100
116
  throw new Error(`GroupSession "${this.session_id}" belongs to another Group`);
101
117
  }
102
- return {
103
- v: 1,
118
+ const metadata: GroupSessionHistoryMeta = {
119
+ v: 2,
104
120
  session_id: this.session_id,
105
121
  group_id: String(raw.group_id || this.group_id),
106
122
  ...(typeof raw.workspace_id === "string" && raw.workspace_id.trim() ? { workspace_id: raw.workspace_id.trim() } : {}),
@@ -112,12 +128,14 @@ export class LocalGroupSessionDataStore implements GroupSessionDataStore {
112
128
  ? { member_session_ids: normalize_member_session_ids(raw.member_session_ids) }
113
129
  : {}),
114
130
  ...(Array.isArray(raw.pending_turns)
115
- ? { pending_turns: normalize_pending_turns(raw.pending_turns) }
131
+ ? { pending_turns: normalize_pending_turns(raw.pending_turns, raw.v === 1 ? "auto" : undefined) }
116
132
  : {}),
117
133
  ...(Array.isArray(raw.auto_frontier_message_ids)
118
134
  ? { auto_frontier_message_ids: normalize_message_ids(raw.auto_frontier_message_ids) }
119
135
  : {}),
120
136
  };
137
+ if (raw.v === 1) await this.write_metadata_unlocked(metadata);
138
+ return metadata;
121
139
  }
122
140
 
123
141
  /** 原子写入当前 metadata。 */
@@ -148,7 +166,7 @@ export class LocalGroupSessionDataStore implements GroupSessionDataStore {
148
166
  ...patch,
149
167
  session_id: this.session_id,
150
168
  group_id: this.group_id,
151
- v: 1 as const,
169
+ v: 2 as const,
152
170
  };
153
171
  await this.write_metadata_unlocked(next_metadata);
154
172
  return next_metadata;
@@ -200,7 +218,7 @@ export class LocalGroupSessionDataStore implements GroupSessionDataStore {
200
218
  private create_initial_metadata(): GroupSessionHistoryMeta {
201
219
  const created_at = Date.now();
202
220
  return {
203
- v: 1,
221
+ v: 2,
204
222
  session_id: this.session_id,
205
223
  group_id: this.group_id,
206
224
  created_at,
@@ -214,7 +232,10 @@ function normalize_member_session_ids(input: object): Record<string, string> {
214
232
  return Object.fromEntries(Object.entries(input).filter(([agent_id, session_id]) => Boolean(agent_id.trim()) && typeof session_id === "string" && session_id.trim()));
215
233
  }
216
234
 
217
- function normalize_pending_turns(input: unknown[]): GroupSessionTurnCheckpoint[] {
235
+ function normalize_pending_turns(
236
+ input: unknown[],
237
+ default_dispatch_stage?: GroupSessionTurnCheckpoint["dispatch_stage"],
238
+ ): GroupSessionTurnCheckpoint[] {
218
239
  return input.flatMap((item) => {
219
240
  if (!item || typeof item !== "object") return [];
220
241
  const candidate = item as Partial<GroupSessionTurnCheckpoint>;
@@ -223,7 +244,12 @@ function normalize_pending_turns(input: unknown[]): GroupSessionTurnCheckpoint[]
223
244
  const context_message_ids = Array.isArray(candidate.context_message_ids)
224
245
  ? normalize_message_ids(candidate.context_message_ids)
225
246
  : [];
226
- return turn_id && root_message_id ? [{ turn_id, root_message_id, context_message_ids }] : [];
247
+ const dispatch_stage = candidate.dispatch_stage === "user" || candidate.dispatch_stage === "auto"
248
+ ? candidate.dispatch_stage
249
+ : default_dispatch_stage;
250
+ return turn_id && root_message_id && dispatch_stage
251
+ ? [{ turn_id, root_message_id, context_message_ids, dispatch_stage }]
252
+ : [];
227
253
  });
228
254
  }
229
255