@co0ontty/wand 4.76.0 → 4.77.0-beta.g7fb0b70

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 (71) hide show
  1. package/dist/agent-dispatch.d.ts +46 -0
  2. package/dist/agent-dispatch.js +84 -0
  3. package/dist/ai-team-availability.d.ts +69 -0
  4. package/dist/ai-team-availability.js +100 -0
  5. package/dist/ai-team-prompts.d.ts +113 -0
  6. package/dist/ai-team-prompts.js +435 -0
  7. package/dist/ai-team-runner.d.ts +241 -0
  8. package/dist/ai-team-runner.js +1370 -0
  9. package/dist/ai-team-types.d.ts +147 -0
  10. package/dist/ai-team-types.js +32 -0
  11. package/dist/attention.d.ts +40 -0
  12. package/dist/attention.js +86 -0
  13. package/dist/build-info.json +4 -4
  14. package/dist/claude-sdk-runner.d.ts +5 -0
  15. package/dist/claude-sdk-runner.js +2 -1
  16. package/dist/commit-task-archive.d.ts +3 -0
  17. package/dist/commit-task-archive.js +43 -0
  18. package/dist/config.js +0 -2
  19. package/dist/git-quick-commit.d.ts +2 -1
  20. package/dist/git-quick-commit.js +72 -60
  21. package/dist/message-truncator.d.ts +41 -5
  22. package/dist/message-truncator.js +255 -38
  23. package/dist/missions.d.ts +4 -2
  24. package/dist/missions.js +2 -2
  25. package/dist/process-manager.d.ts +1 -0
  26. package/dist/process-manager.js +17 -8
  27. package/dist/prompt-optimizer.js +13 -13
  28. package/dist/server-ai-team-routes.d.ts +11 -0
  29. package/dist/server-ai-team-routes.js +283 -0
  30. package/dist/server-attention-routes.d.ts +9 -0
  31. package/dist/server-attention-routes.js +26 -0
  32. package/dist/server-file-routes.js +1 -1
  33. package/dist/server-session-routes.js +41 -5
  34. package/dist/server-settings-routes.js +1 -1
  35. package/dist/server-task-routes.d.ts +2 -0
  36. package/dist/server-task-routes.js +6 -40
  37. package/dist/server-workspace-routes.js +13 -5
  38. package/dist/server.js +43 -0
  39. package/dist/session-topic.js +13 -13
  40. package/dist/session-transport.d.ts +3 -0
  41. package/dist/session-transport.js +1 -0
  42. package/dist/storage.d.ts +54 -1
  43. package/dist/storage.js +413 -2
  44. package/dist/structured-claude-adapter.d.ts +5 -0
  45. package/dist/structured-claude-adapter.js +13 -2
  46. package/dist/structured-claude-protocol.d.ts +2 -0
  47. package/dist/structured-claude-protocol.js +4 -0
  48. package/dist/structured-codex-adapter.js +2 -2
  49. package/dist/structured-grok-adapter.js +2 -1
  50. package/dist/structured-opencode-adapter.js +2 -2
  51. package/dist/structured-pi-adapter.js +2 -1
  52. package/dist/structured-provider-common.d.ts +17 -0
  53. package/dist/structured-provider-common.js +52 -0
  54. package/dist/structured-qoder-adapter.js +2 -1
  55. package/dist/structured-session-manager.d.ts +18 -1
  56. package/dist/structured-session-manager.js +67 -10
  57. package/dist/system-ai.d.ts +13 -2
  58. package/dist/system-ai.js +9 -5
  59. package/dist/task-title.js +10 -10
  60. package/dist/types.d.ts +25 -2
  61. package/dist/wand-task-sync.d.ts +1 -0
  62. package/dist/wand-task-sync.js +1 -0
  63. package/dist/web-ui/content/ai-teams.js +12 -0
  64. package/dist/web-ui/content/scripts.js +86 -5301
  65. package/dist/web-ui/content/styles.css +1 -1
  66. package/dist/web-ui/embedded-assets.d.ts +1 -0
  67. package/dist/web-ui/embedded-assets.js +3 -2
  68. package/dist/web-ui/scripts.d.ts +8 -0
  69. package/dist/web-ui/scripts.js +28 -2
  70. package/dist/ws-broadcast.js +52 -19
  71. package/package.json +1 -1
@@ -0,0 +1,435 @@
1
+ import { memberAgents } from "./ai-team-types.js";
2
+ export const AI_TEAM_REPORT_DIR = ".wand-team";
3
+ export const AI_TEAM_MAX_ASSIGN_STEPS = 5;
4
+ const MESSAGE_MAX = 2000;
5
+ const TITLE_MAX = 80;
6
+ const INSTRUCTIONS_MAX = 8000;
7
+ /**
8
+ * 团队这一轮要做的事。
9
+ * 指派框 / 群聊里刚输入的文字就是本轮任务,不再把任务卡上的旧描述静默拼进去。
10
+ * 没有单独输入时(创建任务后立刻交给团队)才用标题 + 描述。
11
+ */
12
+ export function buildAiTeamObjective(task, note) {
13
+ const assignment = note?.trim() ?? "";
14
+ if (assignment) {
15
+ const title = task.title.trim();
16
+ return title && !assignment.includes(title) ? `任务:${title}\n\n${assignment}` : assignment;
17
+ }
18
+ return [task.title.trim(), task.description.trim()].filter(Boolean).join("\n\n") || "完成这张任务";
19
+ }
20
+ export function leaderOf(team) {
21
+ return team.members.find((member) => member.isLeader) ?? team.members[0];
22
+ }
23
+ export function workersOf(team) {
24
+ return team.members.filter((member) => !member.isLeader);
25
+ }
26
+ /** 每步的约定报告文件(相对 cwd)。所有 CLI 都能写文件,因此是通用的交接方式。 */
27
+ export function aiTeamReportPath(runId, seq, kind, memberId) {
28
+ const name = kind === "leader" ? `${seq}-leader.json` : `${seq}-${memberId}.md`;
29
+ return `${AI_TEAM_REPORT_DIR}/${runId}/${name}`;
30
+ }
31
+ /**
32
+ * 交接文件(相对 cwd):上一批步骤的报告全文汇总,服务端在派发前写好,
33
+ * 下一步的提示词只给这条路径,让执行者自己去读,不再把报告贴进提示词。
34
+ */
35
+ export function aiTeamHandoffPath(runId, seq, kind) {
36
+ return `${AI_TEAM_REPORT_DIR}/${runId}/handoff-${seq}-${kind}.md`;
37
+ }
38
+ /**
39
+ * 群聊续跑的上下文文件(相对 cwd):用户在同一个群聊里接着说话时,服务端把之前几轮的
40
+ * 步骤摘要与群聊原文写在这里,新一轮的负责人与成员先读它再动手,不从零重来。
41
+ * 文件名由运行 id 决定,所以在库里不用额外记一列;「文件在不在」就是「是不是续跑」。
42
+ */
43
+ export function aiTeamChatHistoryPath(runId) {
44
+ return `${AI_TEAM_REPORT_DIR}/${runId}/chat-history.md`;
45
+ }
46
+ const STEP_STATUS_LABEL = {
47
+ queued: "排队",
48
+ running: "进行中",
49
+ done: "完成",
50
+ failed: "失败",
51
+ skipped: "已跳过",
52
+ };
53
+ const RUN_STATUS_LABEL = {
54
+ running: "进行中",
55
+ awaiting_approval: "等你批准计划",
56
+ waiting_user: "等你回复",
57
+ done: "已完成",
58
+ failed: "失败",
59
+ stopped: "已停止",
60
+ };
61
+ /** 交接文件正文(Markdown)。纯函数,便于单测。 */
62
+ export function renderHandoffFile(entries, generatedAt) {
63
+ const lines = [
64
+ "# 上游交接",
65
+ "",
66
+ `> 生成时间:${generatedAt}。下面是上游步骤的原始报告,动手前请读完与本次相关的部分。`,
67
+ "",
68
+ ];
69
+ for (const entry of entries) {
70
+ lines.push(`## 第${entry.seq}步 · ${entry.memberName} · ${entry.title} · ${STEP_STATUS_LABEL[entry.status]}`);
71
+ lines.push(`报告文件:${entry.reportPath}`, "", entry.report.trim() || "(没有报告)", "");
72
+ }
73
+ return lines.join("\n");
74
+ }
75
+ /** 提示词里只列一行元信息;正文在交接文件里,避免把长报告整段塞进上下文。 */
76
+ function handoffStepLine(step) {
77
+ return `- 第${step.seq}步 · ${step.memberName} · ${step.title} · ${STEP_STATUS_LABEL[step.status]} · 报告文件:${step.reportPath}`;
78
+ }
79
+ /** 上下文文件里单条发言最多占的字节;整个文件另有总预算。 */
80
+ const CHAT_HISTORY_MAX_BYTES = 64 * 1024;
81
+ function byteLength(text) {
82
+ return Buffer.byteLength(text, "utf8");
83
+ }
84
+ function historyTurnSpeaker(turn) {
85
+ if (turn.role === "user")
86
+ return "用户";
87
+ if (!turn.author)
88
+ return "团队";
89
+ return turn.author.leader ? `${turn.author.name}(负责人)` : turn.author.name;
90
+ }
91
+ /** 一条群聊发言;提示行(notice)压成引用行,没有文字内容返回 null。 */
92
+ function renderHistoryTurn(turn) {
93
+ const text = turn.content.map((block) => (block.type === "text" ? block.text : "")).join("\n").trim();
94
+ if (!text)
95
+ return null;
96
+ return turn.notice ? `> ${historyTurnSpeaker(turn)}:${text}` : `**${historyTurnSpeaker(turn)}**\n${text}`;
97
+ }
98
+ /** 从最新往回取到预算上限;一条都放不下时至少留最后一条,否则续跑就没有上下文可读。 */
99
+ function tailWithinBudget(blocks, budget) {
100
+ let used = 0;
101
+ let start = blocks.length;
102
+ while (start > 0) {
103
+ const size = byteLength(blocks[start - 1]) + 2;
104
+ if (used + size > budget)
105
+ break;
106
+ used += size;
107
+ start -= 1;
108
+ }
109
+ if (start === blocks.length && blocks.length > 0)
110
+ start = blocks.length - 1;
111
+ return blocks.slice(start);
112
+ }
113
+ /**
114
+ * 群聊续跑的交接文件(Markdown,纯函数便于单测):先列之前几轮的步骤摘要(含上一轮没做完的),
115
+ * 再给群聊原文;原文只留最近的这一段,超预算时从最早处丢并注明。
116
+ */
117
+ export function renderChatHistoryFile(input) {
118
+ const lines = [
119
+ "# 本群聊之前的记录",
120
+ "",
121
+ `> 生成时间:${input.generatedAt}。用户在这个群聊里接着提了新要求,动手前先读完这份记录。`,
122
+ "> 已经完成的部分不要重做;上一轮没做完的步骤见下面的运行摘要,报告正文见群聊原文。",
123
+ "",
124
+ ];
125
+ if (input.runs.length > 0) {
126
+ lines.push("## 之前几轮运行", "");
127
+ for (const run of input.runs) {
128
+ lines.push(`- 状态:${RUN_STATUS_LABEL[run.status]}`);
129
+ for (const step of run.steps) {
130
+ lines.push(` - 第${step.seq}步 · ${step.memberName} · ${step.title} · ${STEP_STATUS_LABEL[step.status]}`);
131
+ }
132
+ }
133
+ lines.push("");
134
+ }
135
+ const head = `${lines.join("\n")}\n## 群聊原文\n\n`;
136
+ // 预算只看群聊原文这一段;摘要与截断说明不参与取舍。
137
+ const budget = Math.max(0, (input.maxBytes ?? CHAT_HISTORY_MAX_BYTES) - byteLength(head) - 64);
138
+ const blocks = input.turns.map(renderHistoryTurn).filter((block) => block !== null);
139
+ const kept = tailWithinBudget(blocks, budget);
140
+ const dropped = blocks.length - kept.length;
141
+ return [
142
+ head.trimEnd(),
143
+ dropped > 0 ? `> (更早的 ${dropped} 条发言已省略)` : "",
144
+ ...kept,
145
+ "",
146
+ ].filter((part) => part !== "").join("\n\n");
147
+ }
148
+ /** 角色中文标签;any / 缺省不显示。 */
149
+ const TEAM_ROLE_LABELS = {
150
+ plan: "制定计划",
151
+ work: "干活实现",
152
+ verify: "验收测试",
153
+ };
154
+ function memberLine(member) {
155
+ // 只渲染首选候选(§3.6);备用候选不进提示词,降级由系统处理。
156
+ const preferred = memberAgents(member)[0] ?? member.agent;
157
+ const model = preferred.model === "default" ? "默认模型" : preferred.model;
158
+ const role = member.role ? TEAM_ROLE_LABELS[member.role] : undefined;
159
+ const duty = member.duty.trim().replace(/\s+/g, " ") || "(未填写)";
160
+ const parts = [`id: ${member.id}`, `名字: ${member.name}`];
161
+ if (role)
162
+ parts.push(`角色: ${role}`);
163
+ parts.push(`CLI: ${preferred.provider}/${model}/${preferred.thinkingEffort}`, `职责: ${duty}`);
164
+ return `- ${parts.join(" | ")}`;
165
+ }
166
+ function leaderReplyFormat() {
167
+ return [
168
+ "## 回复方式(必须遵守)",
169
+ "每轮消息都会给出本次的报告文件路径;把决定写成 JSON 写入该文件,写完简短回复「已写入」。",
170
+ "格式三选一:",
171
+ '{"action":"assign","message":"给用户看的一句话说明","steps":[{"member":"成员 id","title":"简短标题","instructions":"具体要做什么、做到什么程度"}]}',
172
+ '{"action":"ask","message":"要问用户的问题"}',
173
+ '{"action":"finish","message":"最终总结:完成了什么、改了哪些文件、验收结论、遗留问题"}',
174
+ "",
175
+ "steps 里 `after` 写这一步要等哪几步完成(写本次 steps 内的序号,从 1 开始):",
176
+ "- 省略 after:等上一步完成后开始(顺序执行);",
177
+ "- \"after\": []:立刻开始,可以和别的成员并行;",
178
+ "- \"after\": [1, 2]:等第 1、2 步都做完再开始。",
179
+ "",
180
+ "instructions 的写法要求:",
181
+ "- 实现步骤的 instructions 必须列出允许改动的文件/目录范围;",
182
+ "- 验收步骤的 instructions 必须以「验收标准」清单收尾。",
183
+ ];
184
+ }
185
+ function teamInstructions(team) {
186
+ const text = team.instructions?.trim();
187
+ return text ? ["", "## 协作指令", text] : [];
188
+ }
189
+ /** 目标与用户补充都是用户原话(常带口述笔误),别按字面较真。 */
190
+ const OBJECTIVE_INTENT_NOTE = "目标与用户补充都是用户原话,可能带口述笔误(例如把 CLI 写成 ci),按用户意图理解,不要照字面复述。";
191
+ /**
192
+ * 接着同一个群聊跑的那一轮:负责人与成员先读上下文文件再动手。
193
+ * 负责人还要据此避免重做已经完成的步骤;成员只需在已有改动上继续。
194
+ */
195
+ function chatHistoryBlock(path, audience) {
196
+ if (!path)
197
+ return [];
198
+ const shared = `用户是在这个群聊里接着提要求,之前几轮的步骤摘要与群聊原文在:${path}`;
199
+ return audience === "leader"
200
+ ? [
201
+ "## 接着这个群聊之前的几轮(动手前先读文件)",
202
+ shared,
203
+ "先读这个文件再安排:已经做完的不要重做,上一轮没做完的接着做;不要凭标题猜成员做过什么,也不要让他们复述历史。",
204
+ "",
205
+ ]
206
+ : [
207
+ "## 本群聊历史(动手前先读文件)",
208
+ shared,
209
+ "先读这个文件,在已有改动的基础上继续;已经完成的部分不要重做。",
210
+ "",
211
+ ];
212
+ }
213
+ function leaderSystemPrompt(run) {
214
+ const leader = leaderOf(run.team);
215
+ return [
216
+ `你是 AI 团队「${run.team.name}」的负责人(${leader.name})。`,
217
+ `你的职责:${leader.duty.trim() || "拆分任务、派工、根据报告决定下一步。"}`,
218
+ "",
219
+ "## 成员(只能把工作派给下列成员;id 必须原样使用)",
220
+ "成员的名单行只列首选执行配置;执行配置由系统按候选顺序自动降级,你只按成员能力分派,不操心模型可用性。",
221
+ ...workersOf(run.team).map(memberLine),
222
+ ...teamInstructions(run.team),
223
+ "",
224
+ "## 工作方式",
225
+ "- 你不直接改代码:你负责拆分任务、派工,并根据成员报告决定下一步。",
226
+ `- 每次只安排接下来要做的几步(1–${AI_TEAM_MAX_ASSIGN_STEPS} 步)。互不依赖的工作派给不同成员可以并行;所有在跑的步骤结束后,我会把报告交回给你。`,
227
+ "- 并行成员共用同一个工作目录,不要让两个人同时改同一批文件。",
228
+ "- 实现与验收交给不同成员,不要让同一个成员验收自己的工作。",
229
+ "- 派工优先按角色:制定计划给 plan 成员,实现给 work 成员,验收给 verify 成员;没标角色的成员视为 any。",
230
+ "- 同一成员同一时间只有一个步骤;互不相干的步骤并行派出,有先后关系的用 after 声明。",
231
+ "- 需要用户拍板时就提问,不要猜。",
232
+ `- ${OBJECTIVE_INTENT_NOTE}`,
233
+ "",
234
+ ...leaderReplyFormat(),
235
+ ].join("\n");
236
+ }
237
+ function memberSystemPrompt(run, member) {
238
+ return [
239
+ `你是 AI 团队「${run.team.name}」的成员(${member.name})。`,
240
+ `你的职责:${member.duty.trim() || "(未填写)"}`,
241
+ ...teamInstructions(run.team),
242
+ "",
243
+ "## 工作方式",
244
+ "- 只做本轮消息里指派给你的这一步,不要顺手改别的。",
245
+ "- 不要 git commit / push。",
246
+ "- 你的发言会以群聊气泡出现,署名是你的名字;报告仍写进报告文件。",
247
+ "",
248
+ "## 报告约定",
249
+ "每轮消息都会给出本次的报告文件路径;完成后把报告写进该文件(Markdown):",
250
+ "第一行写 `状态: 完成`、`状态: 受阻` 或 `状态: 失败`,然后写做了什么、改了哪些文件、怎么验证的、还有什么问题。",
251
+ ].join("\n");
252
+ }
253
+ /** 会话是新建的时候才需要补目标:老会话的上下文里已经有目标了。 */
254
+ function objectiveBlock(run, fresh) {
255
+ // 标签内联而不是单独的 `## 团队目标` 行:团队消息也会被会话标题 / 迭代提示词记录取首行,
256
+ // 单独一行的话标题会退化成「团队目标」这个空壳。
257
+ return fresh ? [`团队目标:${run.objective}`, ""] : [];
258
+ }
259
+ /** `chatHistoryPath` 有值时说明这一轮是同一个群聊的续跑(见 `aiTeamChatHistoryPath`)。 */
260
+ export function buildLeaderKickoffPrompt(run, reportPath, fresh, chatHistoryPath) {
261
+ return {
262
+ system: leaderSystemPrompt(run),
263
+ message: [
264
+ ...objectiveBlock(run, fresh),
265
+ ...chatHistoryBlock(chatHistoryPath, "leader"),
266
+ "## 本轮要求",
267
+ `把计划决策写成 JSON 写入:${reportPath}`,
268
+ ].join("\n"),
269
+ };
270
+ }
271
+ export function buildLeaderFollowupPrompt(run, finished, reportPath, userNote, fresh, handoffPath, chatHistoryPath) {
272
+ // 续跑上下文只在新建的负责人会话里补一次:会话复用的时候,kickoff 那一轮已经给过路径。
273
+ const lines = [...objectiveBlock(run, fresh), ...chatHistoryBlock(fresh ? chatHistoryPath : null, "leader")];
274
+ if (finished.length > 0) {
275
+ lines.push("## 已结束的步骤", "");
276
+ for (const { step, memberName } of finished) {
277
+ lines.push(handoffStepLine({ ...step, memberName }));
278
+ }
279
+ if (handoffPath) {
280
+ lines.push("", `这些步骤的报告全文汇总在交接文件:${handoffPath}`, "先读这个文件再决定下一步;不要凭标题猜成员做了什么,也不要让他们把报告复述进消息里。");
281
+ }
282
+ lines.push("");
283
+ }
284
+ if (userNote?.trim())
285
+ lines.push("## 用户补充", userNote.trim(), "");
286
+ lines.push("## 本轮要求", `把下一步决定写成 JSON 写入:${reportPath}`);
287
+ return { system: leaderSystemPrompt(run), message: lines.join("\n") };
288
+ }
289
+ export function buildLeaderFormatRetryPrompt(run, error, reportPath, fresh, chatHistoryPath) {
290
+ return {
291
+ system: leaderSystemPrompt(run),
292
+ message: [
293
+ ...objectiveBlock(run, fresh),
294
+ ...chatHistoryBlock(fresh ? chatHistoryPath : null, "leader"),
295
+ `上次的回复没能解析:${error}`,
296
+ "",
297
+ "## 本轮要求",
298
+ `请严格按上面的 JSON 格式重新写入:${reportPath}`,
299
+ ].join("\n"),
300
+ };
301
+ }
302
+ /**
303
+ * fresh=false 时成员会话已有上下文,省掉目标段落。`upstream` 有值时只给交接文件路径与
304
+ * 各步报告文件,不在提示词里复述上游报告正文(上一个成员的信息走文件交接)。
305
+ */
306
+ export function buildMemberPrompt(run, step, member, fresh, upstream, chatHistoryPath) {
307
+ const lines = [
308
+ ...objectiveBlock(run, fresh),
309
+ // 续跑上下文只在新建的成员会话里补一次:成员这一轮以前的信息都在文件里。
310
+ ...chatHistoryBlock(fresh ? chatHistoryPath : null, "member"),
311
+ `## 本步骤:${step.title}`,
312
+ step.instructions,
313
+ "",
314
+ ];
315
+ if (upstream && upstream.steps.length > 0) {
316
+ lines.push("## 上游交接(动手前先读文件)", "");
317
+ for (const item of upstream.steps)
318
+ lines.push(handoffStepLine(item));
319
+ lines.push("", `上游报告全文:${upstream.handoffPath}`, "先读这个文件,再开始本步骤;不要猜上游做了什么,也不要重复劳动。", "");
320
+ }
321
+ lines.push("## 报告", `写入文件:${step.reportPath}`);
322
+ return { system: memberSystemPrompt(run, member), message: lines.join("\n") };
323
+ }
324
+ /** 从 start 处的 `{` 找到配对的 `}`(跳过字符串里的括号)。 */
325
+ function balancedObjectEnd(text, start) {
326
+ let depth = 0;
327
+ let inString = false;
328
+ let escaped = false;
329
+ for (let index = start; index < text.length; index += 1) {
330
+ const char = text[index];
331
+ if (inString) {
332
+ if (escaped)
333
+ escaped = false;
334
+ else if (char === "\\")
335
+ escaped = true;
336
+ else if (char === "\"")
337
+ inString = false;
338
+ continue;
339
+ }
340
+ if (char === "\"")
341
+ inString = true;
342
+ else if (char === "{")
343
+ depth += 1;
344
+ else if (char === "}") {
345
+ depth -= 1;
346
+ if (depth === 0)
347
+ return index;
348
+ }
349
+ }
350
+ return -1;
351
+ }
352
+ function extractDecisionObject(text) {
353
+ for (let start = text.indexOf("{"); start >= 0; start = text.indexOf("{", start + 1)) {
354
+ const end = balancedObjectEnd(text, start);
355
+ if (end < 0)
356
+ return null;
357
+ try {
358
+ const value = JSON.parse(text.slice(start, end + 1));
359
+ if (value && typeof value === "object" && !Array.isArray(value) && "action" in value) {
360
+ return value;
361
+ }
362
+ }
363
+ catch {
364
+ // 继续尝试下一个 `{`:前面可能是正文里的花括号。
365
+ }
366
+ }
367
+ return null;
368
+ }
369
+ function resolveMember(team, ref) {
370
+ if (typeof ref !== "string")
371
+ return null;
372
+ const key = ref.trim().toLowerCase();
373
+ if (!key)
374
+ return null;
375
+ const workers = workersOf(team);
376
+ return workers.find((member) => member.id.toLowerCase() === key)
377
+ ?? workers.find((member) => member.name.trim().toLowerCase() === key)
378
+ ?? null;
379
+ }
380
+ export function parseLeaderDecision(text, team) {
381
+ const raw = extractDecisionObject(text);
382
+ if (!raw)
383
+ return { ok: false, error: "没有找到包含 action 字段的 JSON 对象。" };
384
+ const action = raw.action;
385
+ if (typeof raw.message !== "string")
386
+ return { ok: false, error: "message 必须是字符串。" };
387
+ const message = raw.message.trim().slice(0, MESSAGE_MAX);
388
+ if (action === "ask" || action === "finish") {
389
+ if (!message)
390
+ return { ok: false, error: "message 不能为空。" };
391
+ return { ok: true, decision: { action, message } };
392
+ }
393
+ if (action !== "assign")
394
+ return { ok: false, error: "action 只能是 assign / ask / finish。" };
395
+ if (!Array.isArray(raw.steps) || raw.steps.length === 0) {
396
+ return { ok: false, error: "assign 需要至少一个 steps 项。" };
397
+ }
398
+ if (raw.steps.length > AI_TEAM_MAX_ASSIGN_STEPS) {
399
+ return { ok: false, error: `一次最多安排 ${AI_TEAM_MAX_ASSIGN_STEPS} 步。` };
400
+ }
401
+ const steps = [];
402
+ for (const [index, item] of raw.steps.entries()) {
403
+ const entry = item && typeof item === "object" ? item : {};
404
+ const member = resolveMember(team, entry.member);
405
+ if (!member) {
406
+ return { ok: false, error: `第 ${index + 1} 步的 member「${String(entry.member ?? "")}」不是可派工的成员 id。` };
407
+ }
408
+ const title = typeof entry.title === "string" ? entry.title.trim().slice(0, TITLE_MAX) : "";
409
+ const instructions = typeof entry.instructions === "string" ? entry.instructions.trim().slice(0, INSTRUCTIONS_MAX) : "";
410
+ if (!title)
411
+ return { ok: false, error: `第 ${index + 1} 步缺少 title。` };
412
+ if (!instructions)
413
+ return { ok: false, error: `第 ${index + 1} 步缺少 instructions。` };
414
+ let after;
415
+ if (entry.after === undefined || entry.after === null) {
416
+ after = index > 0 ? [index - 1] : [];
417
+ }
418
+ else if (Array.isArray(entry.after)) {
419
+ after = [];
420
+ for (const ref of entry.after) {
421
+ const position = Number(ref);
422
+ if (!Number.isInteger(position) || position < 1 || position > index) {
423
+ return { ok: false, error: `第 ${index + 1} 步的 after 只能引用它前面的步骤序号(1–${index})。` };
424
+ }
425
+ if (!after.includes(position - 1))
426
+ after.push(position - 1);
427
+ }
428
+ }
429
+ else {
430
+ return { ok: false, error: `第 ${index + 1} 步的 after 必须是数组。` };
431
+ }
432
+ steps.push({ memberId: member.id, title, instructions, after });
433
+ }
434
+ return { ok: true, decision: { action: "assign", message, steps } };
435
+ }
@@ -0,0 +1,241 @@
1
+ import { type AgentDispatchDeps } from "./agent-dispatch.js";
2
+ import { type ModelCatalogView } from "./ai-team-availability.js";
3
+ import { type AiTeamRun, type AiTeamRunDetail, type AiTeamRunSummary } from "./ai-team-types.js";
4
+ import type { SessionRegistry } from "./session-registry.js";
5
+ import type { WandStorage } from "./storage.js";
6
+ import type { WandTask, WandTaskAgent } from "./task-types.js";
7
+ import type { ConversationTurn, ProcessEvent, SessionSnapshot } from "./types.js";
8
+ /** 群聊会话的 automationId 前缀;后面接运行 id。 */
9
+ export declare const AI_TEAM_CHAT_PREFIX = "ai-team-chat:";
10
+ /** 用户可见的冲突错误(路由转 409)。 */
11
+ export declare class AiTeamConflictError extends Error {
12
+ }
13
+ /** 团队调度需要的会话操作。默认实现走 agent-dispatch;测试可整体替换。 */
14
+ export interface AiTeamSessionOps {
15
+ open(input: {
16
+ task: WandTask;
17
+ agent: WandTaskAgent;
18
+ prompt: string;
19
+ automationId: string;
20
+ /** 角色与规则;走 provider 的系统提示通道,不拼进 prompt。 */
21
+ systemPrompt?: string;
22
+ }): Promise<string>;
23
+ send(sessionId: string, text: string): Promise<void>;
24
+ stop(sessionId: string): void;
25
+ snapshot(sessionId: string): SessionSnapshot | null;
26
+ ownerOf(sessionId: string): "structured" | "pty" | "storage" | null;
27
+ }
28
+ /** 群聊:一次运行一个转发会话,团队的决定、派工和报告都以成员身份发在里面。 */
29
+ export interface AiTeamChatOps {
30
+ open(input: {
31
+ task: WandTask;
32
+ run: AiTeamRun;
33
+ title: string;
34
+ }): string;
35
+ post(sessionId: string, turns: ConversationTurn[]): void;
36
+ }
37
+ export interface AiTeamRunnerOptions {
38
+ storage: WandStorage;
39
+ chat?: AiTeamChatOps;
40
+ /** 解析任务卡的执行目录;默认走 agent-dispatch 的规则。 */
41
+ resolveCwd: (task: WandTask) => string;
42
+ ops: AiTeamSessionOps;
43
+ notify?: (run: AiTeamRun) => void;
44
+ now?: () => number;
45
+ /**
46
+ * 某 provider 已发现的模型清单(§3.4 model-unknown 事前比对)。目录未就绪 / 清单为空时
47
+ * 返回空值即可,判定方向是「拿不准就放行」。
48
+ */
49
+ models?: (provider: WandTaskAgent["provider"]) => ModelCatalogView;
50
+ }
51
+ export declare function createAiTeamSessionOps(deps: AgentDispatchDeps & {
52
+ sessions: SessionRegistry;
53
+ }): AiTeamSessionOps;
54
+ export declare function createAiTeamChatOps(deps: AgentDispatchDeps): AiTeamChatOps | undefined;
55
+ export declare function createAiTeamRunner(deps: AgentDispatchDeps & {
56
+ sessions: SessionRegistry;
57
+ notify?: (run: AiTeamRun) => void;
58
+ models?: (provider: WandTaskAgent["provider"]) => ModelCatalogView;
59
+ }): AiTeamRunner;
60
+ /** 遮蔽参照目录:家目录与激活配置目录(含 `/tmp/wand-dev` 这类隔离路径,按传入处理)。 */
61
+ export interface RedactionDirs {
62
+ homeDir?: string;
63
+ configDir?: string;
64
+ }
65
+ /**
66
+ * 落库 / 进群聊前的脱敏(§4.5「全链路不写连接码 / token / home 路径」)。
67
+ * 纯函数、无 IO:目录由调用方传进来,同一个输入永远得到同一个输出,重复清洗结果不变。
68
+ * 顺序很关键:先把配置目录(在家目录下)整段换掉,再换家目录,否则 `~/.wand` 会漏半截。
69
+ * 方向是宁多勿漏:认不准的超长串一律隐藏,代价只是文案少一段可调试信息。
70
+ */
71
+ export declare function redactAiTeamErrorText(text: string, dirs?: RedactionDirs): string;
72
+ /**
73
+ * AI 团队调度器。状态全部在数据库里,会话事件驱动推进;同一运行的所有操作串行执行。
74
+ * 成员会话是普通 Wand 会话,服务端只负责传话:Leader 决定派谁做什么,成员交报告。
75
+ */
76
+ export declare class AiTeamRunner {
77
+ private readonly storage;
78
+ private readonly ops;
79
+ private readonly chat?;
80
+ private readonly resolveCwd;
81
+ private readonly notifyListener?;
82
+ private readonly modelsOf?;
83
+ private readonly now;
84
+ private readonly chains;
85
+ private readonly outputTimers;
86
+ private readonly recheckTimers;
87
+ /** 已经确定「不降级、按既有路径记 failed」的步骤,防止 finishStep 再判一次形成回环。 */
88
+ private readonly noDegrade;
89
+ private sweepTimer;
90
+ /** 脱敏参照目录只算一次:os.homedir() 读 env,configDir 随 storage 定,运行期不变。 */
91
+ private redactionDirsValue;
92
+ constructor(options: AiTeamRunnerOptions);
93
+ /** 本 runner 的遮蔽参照目录(§4.5)。 */
94
+ private get redactionDirs();
95
+ /** 只脱敏、不改长度语义(用于本来就不截断的文案)。 */
96
+ private redact;
97
+ /** 先脱敏再头尾截断:截断会把中间的敏感串切出来,顺序不能反。 */
98
+ private cleanError;
99
+ start(input: {
100
+ teamId: string;
101
+ taskId: string;
102
+ note?: string;
103
+ chatSessionId?: string;
104
+ }): Promise<AiTeamRunDetail>;
105
+ approve(runId: string, options?: {
106
+ echo?: boolean;
107
+ }): Promise<AiTeamRunDetail>;
108
+ reject(runId: string, feedback: string, options?: {
109
+ echo?: boolean;
110
+ }): Promise<AiTeamRunDetail>;
111
+ reply(runId: string, text: string, options?: {
112
+ echo?: boolean;
113
+ }): Promise<AiTeamRunDetail>;
114
+ continueRun(runId: string, extraSteps: number): Promise<AiTeamRunDetail>;
115
+ completeStep(runId: string, stepId: string, report: string): Promise<AiTeamRunDetail>;
116
+ stop(runId: string): Promise<AiTeamRunDetail>;
117
+ /**
118
+ * 用户在群聊里说的话。等回复时转给负责人;等批准时「批准」类短语直接批准,其余当修改意见;
119
+ * 成员干活时先记下,负责人下一轮一起看到;运行已结束则接着开一轮新的。
120
+ */
121
+ chatInput(sessionId: string, text: string): Promise<void>;
122
+ detail(runId: string): AiTeamRunDetail;
123
+ listForTask(taskId: string): AiTeamRun[];
124
+ /** 团队页的运行记录;teamId 为空时列出所有团队(侧边栏计数用)。 */
125
+ listRuns(filter: {
126
+ teamId?: string;
127
+ activeOnly?: boolean;
128
+ limit?: number;
129
+ }): AiTeamRunSummary[];
130
+ /** 与 missions.ingest 并列调用。绝大多数事件与团队无关,必须廉价返回。 */
131
+ ingest(event: ProcessEvent): void;
132
+ /** 服务启动时调用:把重启前未结束的运行接回来,并开始兜底巡检。 */
133
+ reconcile(): void;
134
+ /** 兜底巡检:PTY 成员写完报告后可能不再产生事件。 */
135
+ startSweep(): void;
136
+ dispose(): void;
137
+ /** 等当前排队的所有操作跑完(测试用)。 */
138
+ idle(): Promise<void>;
139
+ private enqueue;
140
+ private act;
141
+ private requireStatus;
142
+ private evaluate;
143
+ private scheduleRecheck;
144
+ private stepOutcome;
145
+ /** proceed=false 时只记结果不推进,由调用方在记完一批之后统一 advance。 */
146
+ private finishStep;
147
+ /**
148
+ * 派出所有已就绪的成员步骤:依赖都已完成、且该成员手上没有活。不同成员并行,同一成员串行。
149
+ * 有成员失败、队列清空或依赖再也满足不了时,等在跑的都结束后交回 Leader。
150
+ */
151
+ private advance;
152
+ /** 开一轮 Leader:把上一轮 Leader 之后结束的成员报告交给他。 */
153
+ private startLeaderRound;
154
+ /** 一条交接条目:元信息 + 报告全文(全文只进交接文件,不进提示词)。 */
155
+ private handoffEntry;
156
+ /**
157
+ * 成员步骤的上游交接:把依赖步骤的报告写成一个交接文件,提示词只给路径。
158
+ * 没有依赖、或写文件失败时返回 null(提示词仍逐行列出上游报告文件)。
159
+ */
160
+ private memberUpstream;
161
+ /**
162
+ * 同一个群聊里接着开新运行时的续跑交接(§4.4 同一套文件交接):把之前几轮的步骤摘要
163
+ * 与群聊原文写进 `.wand-team/<runId>/chat-history.md`,提示词只给路径。
164
+ * 不是续跑(没有群聊会话、之前没有运行、群聊也还是空的)返回 null,提示词就不带这一段。
165
+ */
166
+ private writeChatHistory;
167
+ /**
168
+ * 这次运行要接着的群聊上下文文件;文件不在(不是续跑、或写失败)就给 null。
169
+ * 续跑标记就落在文件本身,所以服务重启后同一路径依旧成立,不用额外存一列。
170
+ */
171
+ private chatHistoryFor;
172
+ /** 交接文件写到报告目录;失败只记日志,不阻断派发。 */
173
+ private writeHandoff;
174
+ private hasBudget;
175
+ private createLeaderStep;
176
+ /**
177
+ * 派出一个步骤:先把步骤标成 running 再发送;同一成员的活会话复用(上下文延续)。
178
+ * 提示词分两段:`system`(角色与规则)只在新建会话时交给系统提示通道,`message` 是本轮用户消息。
179
+ */
180
+ private dispatchStep;
181
+ /**
182
+ * 换候选 = 开新步(§3.4 五步),与 Leader 的格式重试同构。全程在调用方的 enqueue 链里;
183
+ * 只直接派出 A′,**不整轮 advance**(N1:advance 的 queued/byId/busyMembers 是进循环时的快照,
184
+ * 整轮调整会在同一栈里对同一步二次派发)。`proceed` 原样透传给 A′ 的 dispatchStep。
185
+ */
186
+ private degradeWorkStep;
187
+ /**
188
+ * 会话「起来又倒下」的启动期归因(§3.4 触发点 2)。判定核在 ai-team-availability,
189
+ * 这里只负责喂时钟、快照与报告产出;不满足降级条件返回 null,按既有路径记 failed。
190
+ */
191
+ private asyncStartupFailure;
192
+ /** run 级黑名单里这一层是谁封的、为什么封(§3.4 修正 B12 两层键值 + host-disabled 进程级)。 */
193
+ private blockedCandidate;
194
+ /** 黑名单从库里读(§3.4):run 不存在或列里是脏值时退化成空黑名单。 */
195
+ private stateOf;
196
+ /** 每次黑名单变更立即落库,重启后已拉黑的候选与 strikes 计数都还在。 */
197
+ private saveState;
198
+ /** host-disabled 是进程级信号:本次运行该 kind 的候选全部不再尝试。 */
199
+ private disableHostKind;
200
+ /**
201
+ * 失败候选的黑名单记账(§3.4):spawn-missing 封 provider、model-unknown 直接封五元组、
202
+ * startup-timeout 累计到 STARTUP_TIMEOUT_STRIKES 才封。runtime-failure 这类不计入,
203
+ * 一次写完一次落库。
204
+ */
205
+ private accountCandidateFailure;
206
+ /**
207
+ * 本步实际用的候选与累计跳过的候选(`dispatch_info_json`);没有记录 = 首选、没跳过过任何候选。
208
+ * 读的是步骤行自己,重启后同一条恢复链继续从库里接着走。
209
+ */
210
+ private infoOf;
211
+ /** 该步实际使用的候选(§3.6 署名口径):候选一律经 memberAgents 读,越界回退首选。 */
212
+ private actualAgent;
213
+ /**
214
+ * 运行停下来等用户时,用户可能刚在团队页换了成员的 CLI / 模型(比如额度用尽换一个)。
215
+ * 接着跑之前换成最新配置;成员被删了或没有负责人时保留原快照。
216
+ */
217
+ private refreshTeam;
218
+ /**
219
+ * 同一成员复用还活着的会话。比对基准是该成员**最近一步实际使用的候选**(§3.6),
220
+ * 降级之后不会因为「和首选不一致」而每步都另开会话;候选换了五元组就不复用。
221
+ */
222
+ private liveSessionFor;
223
+ private openChat;
224
+ private postTurn;
225
+ private postUser;
226
+ private postNotice;
227
+ /** 负责人的决定:说明 + 派工清单;等批准 / 等回复时告诉用户在群里怎么接话。 */
228
+ private postLeaderDecision;
229
+ /**
230
+ * 职责交叉只提示、不拦截(§3.3 修正 B13):同一个成员既实现过又验收时,在群里提一句,
231
+ * 由用户决定是否让负责人调整。服务端不做任何职责硬校验,步骤照常派发。
232
+ */
233
+ private postVerifyOverlapNotices;
234
+ private member;
235
+ private nextSeq;
236
+ private setStatus;
237
+ private saveRun;
238
+ private saveStep;
239
+ private notify;
240
+ private iso;
241
+ }