skyloom 1.14.8 → 1.15.0

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 (156) hide show
  1. package/.github/workflows/ci.yml +2 -2
  2. package/.github/workflows/publish.yml +51 -4
  3. package/CONVERSION_PLAN.md +191 -191
  4. package/config/default.yaml +46 -43
  5. package/config/models.yaml +928 -155
  6. package/config/providers.yaml +109 -6
  7. package/dist/agents/snow.d.ts +2 -0
  8. package/dist/agents/snow.d.ts.map +1 -1
  9. package/dist/agents/snow.js +36 -5
  10. package/dist/agents/snow.js.map +1 -1
  11. package/dist/cli/loom_chat.d.ts.map +1 -1
  12. package/dist/cli/loom_chat.js +207 -1
  13. package/dist/cli/loom_chat.js.map +1 -1
  14. package/dist/cli/main.js +190 -40
  15. package/dist/cli/main.js.map +1 -1
  16. package/dist/cli/tui.d.ts.map +1 -1
  17. package/dist/cli/tui.js +6 -31
  18. package/dist/cli/tui.js.map +1 -1
  19. package/dist/core/agent.d.ts +6 -4
  20. package/dist/core/agent.d.ts.map +1 -1
  21. package/dist/core/agent.js +61 -20
  22. package/dist/core/agent.js.map +1 -1
  23. package/dist/core/catalog.d.ts.map +1 -1
  24. package/dist/core/catalog.js +30 -9
  25. package/dist/core/catalog.js.map +1 -1
  26. package/dist/core/commands.d.ts +110 -0
  27. package/dist/core/commands.d.ts.map +1 -0
  28. package/dist/core/commands.js +633 -0
  29. package/dist/core/commands.js.map +1 -0
  30. package/dist/core/concurrency.d.ts +38 -0
  31. package/dist/core/concurrency.d.ts.map +1 -0
  32. package/dist/core/concurrency.js +65 -0
  33. package/dist/core/concurrency.js.map +1 -0
  34. package/dist/core/factory.js +16 -16
  35. package/dist/core/file_checkpoint.d.ts +9 -0
  36. package/dist/core/file_checkpoint.d.ts.map +1 -1
  37. package/dist/core/file_checkpoint.js +33 -1
  38. package/dist/core/file_checkpoint.js.map +1 -1
  39. package/dist/core/llm.d.ts.map +1 -1
  40. package/dist/core/llm.js +66 -13
  41. package/dist/core/llm.js.map +1 -1
  42. package/dist/core/memory.js +51 -51
  43. package/dist/core/schemas.d.ts +16 -0
  44. package/dist/core/schemas.d.ts.map +1 -1
  45. package/dist/core/schemas.js +32 -0
  46. package/dist/core/schemas.js.map +1 -1
  47. package/dist/core/security.d.ts.map +1 -1
  48. package/dist/core/security.js +27 -0
  49. package/dist/core/security.js.map +1 -1
  50. package/dist/core/skymd.js +14 -14
  51. package/dist/core/trace.d.ts +105 -0
  52. package/dist/core/trace.d.ts.map +1 -0
  53. package/dist/core/trace.js +213 -0
  54. package/dist/core/trace.js.map +1 -0
  55. package/dist/tools/builtin.d.ts +2 -6
  56. package/dist/tools/builtin.d.ts.map +1 -1
  57. package/dist/tools/builtin.js +18 -111
  58. package/dist/tools/builtin.js.map +1 -1
  59. package/dist/tools/extra.d.ts +13 -0
  60. package/dist/tools/extra.d.ts.map +1 -0
  61. package/dist/tools/extra.js +827 -0
  62. package/dist/tools/extra.js.map +1 -0
  63. package/dist/tools/guards.d.ts +12 -0
  64. package/dist/tools/guards.d.ts.map +1 -0
  65. package/dist/tools/guards.js +143 -0
  66. package/dist/tools/guards.js.map +1 -0
  67. package/dist/tools/model_tool.d.ts.map +1 -1
  68. package/dist/tools/model_tool.js +24 -4
  69. package/dist/tools/model_tool.js.map +1 -1
  70. package/dist/web/markdown.d.ts +32 -0
  71. package/dist/web/markdown.d.ts.map +1 -0
  72. package/dist/web/markdown.js +202 -0
  73. package/dist/web/markdown.js.map +1 -0
  74. package/dist/web/server.d.ts +4 -0
  75. package/dist/web/server.d.ts.map +1 -1
  76. package/dist/web/server.js +14 -582
  77. package/dist/web/server.js.map +1 -1
  78. package/dist/web/ui.d.ts +31 -0
  79. package/dist/web/ui.d.ts.map +1 -0
  80. package/dist/web/ui.js +1009 -0
  81. package/dist/web/ui.js.map +1 -0
  82. package/docs/AESTHETIC_DESIGN.md +152 -152
  83. package/docs/OPTIMIZATION_PLAN.md +178 -178
  84. package/package.json +1 -1
  85. package/src/agents/snow.ts +38 -5
  86. package/src/cli/commands_md.ts +112 -112
  87. package/src/cli/input_macros.ts +83 -83
  88. package/src/cli/loom.ts +1041 -1041
  89. package/src/cli/loom_chat.ts +772 -603
  90. package/src/cli/main.ts +853 -723
  91. package/src/cli/tui.ts +264 -289
  92. package/src/core/agent/guard.ts +133 -133
  93. package/src/core/agent/task.ts +100 -100
  94. package/src/core/agent.ts +1630 -1590
  95. package/src/core/agent_helpers.ts +500 -500
  96. package/src/core/bus.ts +221 -221
  97. package/src/core/cache.ts +153 -153
  98. package/src/core/catalog.ts +199 -178
  99. package/src/core/circuit_breaker.ts +119 -119
  100. package/src/core/commands.ts +704 -0
  101. package/src/core/concurrency.ts +73 -0
  102. package/src/core/config.ts +365 -365
  103. package/src/core/constants.ts +95 -95
  104. package/src/core/factory.ts +656 -656
  105. package/src/core/file_checkpoint.ts +163 -136
  106. package/src/core/hooks.ts +126 -126
  107. package/src/core/llm.ts +972 -915
  108. package/src/core/logger.ts +143 -143
  109. package/src/core/mcp.ts +1001 -1001
  110. package/src/core/memory.ts +1201 -1201
  111. package/src/core/middleware.ts +350 -350
  112. package/src/core/model_config.ts +159 -159
  113. package/src/core/pipelines.ts +424 -424
  114. package/src/core/schemas.ts +319 -282
  115. package/src/core/security.ts +27 -0
  116. package/src/core/semantic.ts +211 -211
  117. package/src/core/skill.ts +384 -384
  118. package/src/core/skymd.ts +143 -143
  119. package/src/core/theme.ts +65 -65
  120. package/src/core/tool.ts +457 -457
  121. package/src/core/trace.ts +236 -0
  122. package/src/core/verify.ts +71 -71
  123. package/src/plugins/loader.ts +91 -91
  124. package/src/skills/loader.ts +75 -75
  125. package/src/tools/builtin.ts +571 -642
  126. package/src/tools/computer.ts +279 -279
  127. package/src/tools/extra.ts +662 -0
  128. package/src/tools/guards.ts +82 -0
  129. package/src/tools/model_tool.ts +93 -74
  130. package/src/tools/todo.ts +76 -76
  131. package/src/web/markdown.ts +193 -0
  132. package/src/web/server.ts +117 -693
  133. package/src/web/ui.ts +949 -0
  134. package/tests/agent.test.ts +211 -159
  135. package/tests/agent_helpers.test.ts +48 -48
  136. package/tests/catalog.test.ts +86 -86
  137. package/tests/checkpoint_commands.test.ts +124 -124
  138. package/tests/claude_compat.test.ts +110 -110
  139. package/tests/commands.test.ts +103 -0
  140. package/tests/concurrency.test.ts +102 -0
  141. package/tests/config.test.ts +41 -41
  142. package/tests/extra_tools.test.ts +212 -0
  143. package/tests/fence_plugin.test.ts +52 -52
  144. package/tests/guard.test.ts +75 -75
  145. package/tests/loom.test.ts +337 -337
  146. package/tests/memory.test.ts +170 -170
  147. package/tests/model_config.test.ts +109 -109
  148. package/tests/skymd.test.ts +146 -146
  149. package/tests/ssrf.test.ts +38 -38
  150. package/tests/structured_retry.test.ts +87 -0
  151. package/tests/task.test.ts +60 -60
  152. package/tests/todo_toolstats.test.ts +94 -94
  153. package/tests/trace.test.ts +128 -0
  154. package/tests/tui.test.ts +67 -67
  155. package/tests/web.test.ts +169 -0
  156. package/tsconfig.json +38 -38
@@ -1,656 +1,656 @@
1
- /**
2
- * System factory — unified Agent creation and task orchestration.
3
- *
4
- * Avoids duplication between CLI and web entry points.
5
- */
6
-
7
- import { BaseAgent, Task as AgentTask, TaskState } from './agent';
8
- import { MessageBus } from './bus';
9
- import { loadConfig } from './config';
10
- import { LLMClient } from './llm';
11
- import { getLogger } from './logger';
12
- import { SkillRegistry } from './skill';
13
- import { ToolRegistry } from './tool';
14
-
15
- const log = getLogger('factory');
16
-
17
- export class SystemContext {
18
- config: ReturnType<typeof loadConfig>;
19
- bus: MessageBus;
20
- llm: LLMClient;
21
- agentMap: Map<string, BaseAgent>;
22
- toolRegistry: ToolRegistry;
23
- workspacePath: string = '';
24
- mcp: any = null;
25
- mcpStatus: string[] = [];
26
-
27
- constructor(opts: {
28
- config: ReturnType<typeof loadConfig>;
29
- bus: MessageBus;
30
- llm: LLMClient;
31
- agentMap: Map<string, BaseAgent>;
32
- toolRegistry: ToolRegistry;
33
- workspacePath?: string;
34
- mcp?: any;
35
- mcpStatus?: string[];
36
- }) {
37
- this.config = opts.config;
38
- this.bus = opts.bus;
39
- this.llm = opts.llm;
40
- this.agentMap = opts.agentMap;
41
- this.toolRegistry = opts.toolRegistry;
42
- this.workspacePath = opts.workspacePath || '';
43
- this.mcp = opts.mcp || null;
44
- this.mcpStatus = opts.mcpStatus || [];
45
- }
46
-
47
- async initAll(): Promise<void> {
48
- if (this.mcp) {
49
- try {
50
- this.mcpStatus = await this.mcp.connectAll();
51
- if (this.mcpStatus.length > 0) {
52
- log.info('mcp_connected', { servers: this.mcpStatus.join(', ') });
53
- }
54
- } catch (e) {
55
- log.warn('mcp_connect_all_failed', { error: String(e) });
56
- }
57
- }
58
- for (const agent of this.agentMap.values()) {
59
- await agent.init();
60
- }
61
- }
62
-
63
- async closeAll(): Promise<void> {
64
- for (const agent of this.agentMap.values()) {
65
- await agent.close();
66
- }
67
- if (this.mcp) {
68
- try { await this.mcp.closeAll(); } catch (e) { log.warn('mcp_close_failed', { error: String(e) }); }
69
- }
70
- }
71
- }
72
-
73
- /**
74
- * Bootstrap the full system: config, bus, LLM, tools, skills, plugins, agents.
75
- */
76
- export function createSystemContext(): SystemContext {
77
- const config = loadConfig();
78
-
79
- // session_start hooks — user-configured shell commands (see core/hooks)
80
- try {
81
- const { loadHooks, runSessionStartHooks } = require('./hooks');
82
- const hooks = loadHooks(config);
83
- if (hooks.sessionStart.length > 0) runSessionStartHooks(hooks);
84
- } catch { /* hooks must never block startup */ }
85
-
86
- let workspacePath = '';
87
- try {
88
- const { resolveWorkspacePath, initWorkspace } = require('./workspace');
89
- const wsRoot = resolveWorkspacePath((config as any).workspace?.path || 'auto');
90
- initWorkspace(wsRoot);
91
- workspacePath = wsRoot;
92
- log.info('workspace', { path: workspacePath });
93
- } catch { /* ignore */ }
94
-
95
- const bus = new MessageBus();
96
-
97
- // Shared registries
98
- const baseToolRegistry = new ToolRegistry();
99
- const baseSkillRegistry = new SkillRegistry();
100
-
101
- // Register builtin tools
102
- try {
103
- const { registerBuiltinTools } = require('../tools/builtin');
104
- registerBuiltinTools(baseToolRegistry);
105
- } catch (e) {
106
- log.warn('builtin_tools_not_available', { error: String(e) });
107
- }
108
-
109
- // Register all skills
110
- try {
111
- const { registerAllSkills } = require('../skills/loader');
112
- registerAllSkills(baseSkillRegistry);
113
- } catch (e) {
114
- log.warn('skills_not_available', { error: String(e) });
115
- }
116
-
117
- // Load plugins
118
- try {
119
- const { PluginLoader } = require('../plugins/loader');
120
- const pluginLoader = new PluginLoader(baseToolRegistry);
121
- const pluginConfig = (config as any).plugins;
122
- const pluginDirs = pluginConfig?.enabled ? (pluginConfig.directories || []) : [];
123
- pluginLoader.loadFromDirectories(pluginDirs);
124
- } catch (e) {
125
- log.warn('plugins_not_available', { error: String(e) });
126
- }
127
-
128
- // Configure MCP manager
129
- let mcpManager: any = null;
130
- try {
131
- const { MCPManager, loadPersistedServers, loadProjectMcpJson } = require('./mcp');
132
- mcpManager = new MCPManager(baseToolRegistry);
133
- const persisted = loadPersistedServers();
134
- const mcpServers = (config as any).mcp?.servers || [];
135
- const projectServers = loadProjectMcpJson(); // Claude Code 标准 .mcp.json
136
- // dedupe by name — project .mcp.json wins over runtime-added over config
137
- const byName = new Map<string, any>();
138
- for (const s of [...mcpServers, ...persisted, ...projectServers]) {
139
- if (s?.name) byName.set(s.name, s);
140
- }
141
- const allServers = [...byName.values()];
142
- if (allServers.length > 0) {
143
- mcpManager.configure(allServers);
144
- }
145
- } catch (e) {
146
- log.warn('mcp_not_available', { error: String(e) });
147
- }
148
-
149
- // Shared LLM client
150
- const llm = new LLMClient(config as any, baseToolRegistry);
151
-
152
- // Per-agent registries
153
- const agents = new Map<string, BaseAgent>();
154
-
155
- // Try to dynamically load agent classes
156
- const agentNames = ['fog', 'rain', 'frost', 'snow', 'dew', 'fair'];
157
-
158
- for (const name of agentNames) {
159
- const agentRegistry = new ToolRegistry();
160
- agentRegistry.merge(baseToolRegistry);
161
- const agentSkills = new SkillRegistry();
162
- agentSkills.merge(baseSkillRegistry);
163
-
164
- try {
165
- // Try dynamic import
166
- const clsName = name.charAt(0).toUpperCase() + name.slice(1) + 'Agent';
167
- // Use require for now since dynamic imports are async
168
- let AgentClass: any = null;
169
- try {
170
- const mod = require(`../agents/${name}`);
171
- AgentClass = mod[clsName];
172
- } catch {
173
- log.warn('agent_class_missing', { agent: name });
174
- continue;
175
- }
176
-
177
- if (!AgentClass) {
178
- log.warn('agent_class_not_found', { agent: name, class: clsName });
179
- continue;
180
- }
181
-
182
- const agent = new AgentClass(
183
- config,
184
- llm,
185
- bus,
186
- agentRegistry,
187
- agentSkills
188
- ) as BaseAgent;
189
-
190
- // Register delegate_to tool
191
- try {
192
- const { createDelegateTool } = require('../tools/delegate');
193
- agentRegistry.register(createDelegateTool(agents, agent));
194
- } catch (e) {
195
- log.warn('delegate_tool_not_available', { agent: name, error: String(e) });
196
- }
197
-
198
- // Register model self-service tools (list_models / set_my_model)
199
- try {
200
- const { createModelTools } = require('../tools/model_tool');
201
- for (const t of createModelTools(name, config)) agentRegistry.register(t);
202
- } catch (e) {
203
- log.warn('model_tools_not_available', { agent: name, error: String(e) });
204
- }
205
-
206
- // Register the task-checklist tool (todo_write)
207
- try {
208
- const { createTodoTool } = require('../tools/todo');
209
- agentRegistry.register(createTodoTool(agent));
210
- } catch (e) {
211
- log.warn('todo_tool_not_available', { agent: name, error: String(e) });
212
- }
213
-
214
- agents.set(name, agent);
215
- } catch (e) {
216
- log.warn('agent_creation_failed', { agent: name, error: String(e) });
217
- }
218
- }
219
-
220
- // Bind agents to MCP
221
- if (mcpManager) {
222
- try {
223
- mcpManager.bindAgents(agents);
224
- } catch (e) {
225
- log.warn('mcp_bind_failed', { error: String(e) });
226
- }
227
- }
228
-
229
- return new SystemContext({
230
- config,
231
- bus,
232
- llm,
233
- agentMap: agents,
234
- toolRegistry: baseToolRegistry,
235
- workspacePath,
236
- mcp: mcpManager,
237
- });
238
- }
239
-
240
- // ── Task orchestration ──
241
-
242
- export class TaskExecutionResult {
243
- id: string;
244
- agent: string;
245
- description: string;
246
- success: boolean;
247
- content: string;
248
-
249
- constructor(opts: {
250
- id: string;
251
- agent: string;
252
- description: string;
253
- success: boolean;
254
- content: string;
255
- }) {
256
- this.id = opts.id;
257
- this.agent = opts.agent;
258
- this.description = opts.description;
259
- this.success = opts.success;
260
- this.content = opts.content;
261
- }
262
- }
263
-
264
- // Placeholder phrases that mean the LLM gave up
265
- const PLACEHOLDER_PATTERNS = [
266
- 'done', 'ok', 'completed', 'task completed', 'task done', 'task finished',
267
- 'finished', '已完成', '完成了', '好的', '好了', 'ok!',
268
- ];
269
-
270
- const STATUS_REPORT_KEYWORDS = [
271
- '已完成', '完成了', '已成功', '已经完成', '工作已', '任务已',
272
- 'task complete', 'task is complete', 'i have completed', "i've completed",
273
- 'successfully completed', 'finished the', 'i finished',
274
- '已经写好', '已经写完', '已经做好', '写完了', '做完了',
275
- ];
276
-
277
- const DELIVERABLE_MARKERS = [
278
- '```', 'http://', 'https://', '|', '## ', '###', '- [ ]', '1.', '/', '\\',
279
- ];
280
-
281
- function isThinContent(content: string): boolean {
282
- if (!content) return true;
283
- const stripped = content.trim();
284
- if (!stripped) return true;
285
- const lowered = stripped.toLowerCase();
286
- if (lowered.startsWith('[truncated]') || lowered.startsWith('[error:')) return true;
287
- const bare = lowered.replace(/[.!?。!?\s]+$/, '').trim();
288
- if (PLACEHOLDER_PATTERNS.includes(bare)) return true;
289
- return (
290
- stripped.length <= 200 &&
291
- STATUS_REPORT_KEYWORDS.some(k => lowered.includes(k)) &&
292
- !DELIVERABLE_MARKERS.some(m => stripped.includes(m))
293
- );
294
- }
295
-
296
- const RESULT_FAILURE_MARKERS = [
297
- '[truncated]', '[stuck]', '[Error', 'Error:',
298
- '未能完成', '无法完成', '[cycle detected]', '[CircuitBreakerOpen]',
299
- ];
300
-
301
- function looksObviouslyComplete(results: TaskExecutionResult[]): boolean {
302
- if (!results.length) return false;
303
- return results.every(r => {
304
- if (!r.success) return false;
305
- const body = (r.content || '').trim();
306
- if (body.length < 400) return false;
307
- return !RESULT_FAILURE_MARKERS.some(m => body.includes(m));
308
- });
309
- }
310
-
311
- async function executeWithRetry(
312
- agent: BaseAgent,
313
- aTask: any,
314
- maxAttempts: number,
315
- onStatus?: ((status: string) => void) | null
316
- ): Promise<any> {
317
- let lastResult: any = null;
318
- const originalDescription = aTask.description;
319
-
320
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
321
- try {
322
- const result = await agent.executeTask(aTask, onStatus);
323
- const content = (result.content || '').trim();
324
- const truncated = (result as any).truncated === true;
325
- const ok = result.success && !isThinContent(content);
326
-
327
- if (ok && !truncated) return result;
328
- lastResult = result;
329
-
330
- if (attempt < maxAttempts) {
331
- const reason = truncated
332
- ? 'previous attempt was truncated'
333
- : 'previous attempt was empty or a placeholder ack';
334
- aTask.description = `${originalDescription}\n\n[retry ${attempt + 1}/${maxAttempts}] ${reason}. You MUST produce the actual deliverable.`;
335
- }
336
- } catch (e) {
337
- lastResult = { success: false, content: `Attempt ${attempt} threw: ${e}` };
338
- }
339
- if (attempt < maxAttempts) {
340
- await new Promise(r => setTimeout(r, Math.min(500 * Math.pow(2, attempt - 1), 2000)));
341
- }
342
- }
343
-
344
- aTask.description = originalDescription;
345
- return lastResult || { success: false, content: `All ${maxAttempts} attempts failed` };
346
- }
347
-
348
- async function executePending(
349
- pending: any[],
350
- agentMap: Map<string, BaseAgent>,
351
- results: TaskExecutionResult[],
352
- resultsById: Map<string, TaskExecutionResult>,
353
- fullContentsById: Map<string, string>,
354
- completed: Set<string>,
355
- options: {
356
- onTaskStart?: ((task: any) => Promise<void>) | null;
357
- onTaskDone?: ((task: any, result: TaskExecutionResult) => Promise<void>) | null;
358
- onToolStatus?: ((status: string) => void) | null;
359
- resultTruncate?: number | null;
360
- maxTaskRetries: number;
361
- }
362
- ): Promise<void> {
363
- while (pending.length > 0) {
364
- const ready = pending.filter(t => t.allDeps.every((dep: string) => completed.has(dep)));
365
- if (ready.length === 0) {
366
- for (const t of pending) {
367
- const missing = t.allDeps.filter((d: string) => !completed.has(d));
368
- t.transitionTo(TaskState.FAILED);
369
- const r = new TaskExecutionResult({
370
- id: t.id, agent: t.assignedTo || '',
371
- description: t.description, success: false,
372
- content: `[dependency missing] task ${t.id} requires ${missing.join(', ')} which never completed`,
373
- });
374
- results.push(r);
375
- resultsById.set(r.id, r);
376
- completed.add(r.id);
377
- if (options.onTaskDone) await options.onTaskDone(t, r);
378
- }
379
- pending.length = 0;
380
- return;
381
- }
382
-
383
- for (const t of ready) t.transitionTo(TaskState.RUNNING);
384
-
385
- const batchResults = await Promise.all(
386
- ready.map(async (t: any) => {
387
- const agent = agentMap.get(t.assignedTo);
388
- if (!agent) {
389
- return new TaskExecutionResult({
390
- id: t.id, agent: t.assignedTo || '',
391
- description: t.description, success: false,
392
- content: `Agent '${t.assignedTo}' not found`,
393
- });
394
- }
395
- if (options.onTaskStart) await options.onTaskStart(t);
396
-
397
- let description = t.description;
398
- const upstreamSections: string[] = [];
399
- const thinUpstreamIds: string[] = [];
400
-
401
- for (const depId of t.allDeps) {
402
- if (resultsById.has(depId)) {
403
- const parent = resultsById.get(depId)!;
404
- const fullContent = fullContentsById.get(depId) || parent.content || '';
405
- if (isThinContent(fullContent)) {
406
- thinUpstreamIds.push(parent.id);
407
- upstreamSections.push(
408
- `## 上游产出缺失 (task ${parent.id} · ${parent.agent})\n` +
409
- `⚠ 上游任务声称完成但未产出实际内容。\n` +
410
- `原始回复:\n${fullContent}`
411
- );
412
- } else {
413
- upstreamSections.push(
414
- `## 上游产出 (task ${parent.id} · ${parent.agent})\n${fullContent}`
415
- );
416
- }
417
- }
418
- }
419
- if (upstreamSections.length > 0) {
420
- description = `${t.description}\n\n${upstreamSections.join('\n\n')}`;
421
- }
422
- if (thinUpstreamIds.length > 0) {
423
- log.warn('thin_upstream', { task: t.id, thin_upstream: thinUpstreamIds });
424
- }
425
-
426
- const aTask = new AgentTask({
427
- id: t.id,
428
- description,
429
- assignedTo: t.assignedTo,
430
- parentId: t.parentId,
431
- metadata: t.metadata,
432
- });
433
-
434
- const result = await executeWithRetry(agent, aTask, options.maxTaskRetries, options.onToolStatus);
435
-
436
- if (result.success) t.transitionTo(TaskState.COMPLETED);
437
- else t.transitionTo(TaskState.FAILED);
438
-
439
- const full = result.content || '';
440
- fullContentsById.set(t.id, full);
441
- const tr = options.resultTruncate != null && full.length > options.resultTruncate
442
- ? full.slice(0, options.resultTruncate) : full;
443
-
444
- const r = new TaskExecutionResult({
445
- id: t.id, agent: t.assignedTo || '',
446
- description: t.description, success: result.success,
447
- content: tr,
448
- });
449
- if (options.onTaskDone) await options.onTaskDone(t, r);
450
- return r;
451
- })
452
- );
453
-
454
- for (const r of batchResults) {
455
- results.push(r);
456
- resultsById.set(r.id, r);
457
- completed.add(r.id);
458
- }
459
- for (const t of ready) {
460
- const idx = pending.indexOf(t);
461
- if (idx >= 0) pending.splice(idx, 1);
462
- }
463
- }
464
- }
465
-
466
- async function judgeGoalAchievement(
467
- snow: BaseAgent,
468
- goal: string,
469
- results: TaskExecutionResult[]
470
- ): Promise<[boolean, string]> {
471
- const bullets = results.map(r => {
472
- const status = r.success ? '成功' : '失败';
473
- const excerpt = (r.content || '').slice(0, 800);
474
- return `- [task ${r.id} · agent=${r.agent} · status=${status}] len=${(r.content || '').length}chars\n ${excerpt}`;
475
- }).join('\n');
476
-
477
- const prompt = `你是一名极严格的项目验收员。验证 sub-task 真的产出了可验证的交付物。
478
-
479
- ## 验收规则(严格执行)
480
-
481
- 1. **「调研/搜集/查询」类**:必须列出具体对象名称和数据/特性/链接。
482
- 2. **「撰写/生成/写作」类**:必须包含实际的文本/代码/markdown。
483
- 3. **「审查/审计/对比」类**:必须列出具体问题点。
484
- 4. **核心交付物缺失绝不容忍**。
485
-
486
- ## 用户原目标
487
- ${goal}
488
-
489
- ## 子任务执行结果
490
- ${bullets}
491
-
492
- 严格按下列 JSON 格式输出(除 JSON 之外不要任何其他字符):
493
- {"achieved": true/false, "missing": "若未达成,逐项列出缺什么"}`;
494
-
495
- try {
496
- const raw = await snow.chatOneshot(prompt);
497
- let text = raw.trim();
498
- if (text.startsWith('```')) {
499
- text = text.replace(/```/g, '').replace(/^json/i, '').trim();
500
- }
501
- const start = text.indexOf('{');
502
- const end = text.lastIndexOf('}');
503
- if (start < 0 || end <= start) return [true, ''];
504
- const parsed = JSON.parse(text.slice(start, end + 1));
505
- return [!!parsed.achieved, (parsed.missing || '').trim()];
506
- } catch {
507
- return [true, ''];
508
- }
509
- }
510
-
511
- export async function orchestrateTask(
512
- goal: string,
513
- agentMap: Map<string, BaseAgent>,
514
- snow: BaseAgent | null = null,
515
- options?: {
516
- onTaskStart?: ((task: any) => Promise<void>) | null;
517
- onTaskDone?: ((task: any, result: TaskExecutionResult) => Promise<void>) | null;
518
- onPlanned?: ((tasks: any[]) => Promise<boolean | null>) | null;
519
- onToolStatus?: ((status: string) => void) | null;
520
- resultTruncate?: number | null;
521
- summaryPromptTemplate?: string;
522
- maxTaskRetries?: number;
523
- maxReplanRounds?: number;
524
- maxTotalTasks?: number;
525
- resume?: boolean;
526
- }
527
- ): Promise<[any[], TaskExecutionResult[], string]> {
528
- const snowAgent = snow || agentMap.get('snow') || null;
529
- if (!snowAgent) {
530
- return [[], [], 'Snow agent not available'];
531
- }
532
-
533
- const maxTaskRetries = options?.maxTaskRetries ?? 3;
534
- const maxReplanRounds = options?.maxReplanRounds ?? 1;
535
- const maxTotalTasks = options?.maxTotalTasks ?? 6;
536
- const resultTruncate = options?.resultTruncate ?? 500;
537
-
538
- // Try pipeline match first
539
- let tasks: any[];
540
- try {
541
- const { matchPipeline, buildTasksFromPipeline } = require('./pipelines');
542
- const matched = matchPipeline(goal);
543
- if (matched) {
544
- tasks = buildTasksFromPipeline(matched, goal);
545
- } else {
546
- tasks = await (snowAgent as any).orchestrate(goal);
547
- }
548
- } catch {
549
- tasks = await (snowAgent as any).orchestrate(goal);
550
- }
551
-
552
- if (!tasks || tasks.length === 0) {
553
- return [[], [], 'No tasks were planned'];
554
- }
555
-
556
- // Notify caller of the plan
557
- if (options?.onPlanned) {
558
- const proceed = await options.onPlanned(tasks);
559
- if (proceed === false) {
560
- return [tasks, [], '[CANCELLED] plan rejected before execution'];
561
- }
562
- }
563
-
564
- const completed = new Set<string>();
565
- const results: TaskExecutionResult[] = [];
566
- const resultsById = new Map<string, TaskExecutionResult>();
567
- const fullContentsById = new Map<string, string>();
568
- let pending = tasks.filter((t: any) => t.assignedTo && t.assignedTo !== 'snow');
569
- let replanRound = 0;
570
-
571
- // Cycle detection
572
- function hasCycle(t: any, path: Set<string>): boolean {
573
- if (path.has(t.id)) return true;
574
- path.add(t.id);
575
- for (const depId of t.allDeps || []) {
576
- const dep = tasks.find((x: any) => x.id === depId);
577
- if (dep && hasCycle(dep, new Set(path))) return true;
578
- }
579
- return false;
580
- }
581
-
582
- pending = pending.filter((t: any) => {
583
- if (hasCycle(t, new Set())) {
584
- results.push(new TaskExecutionResult({
585
- id: t.id, agent: t.assignedTo || '',
586
- description: t.description, success: false,
587
- content: `[cycle detected] task ${t.id} has circular dependency`,
588
- }));
589
- completed.add(t.id);
590
- return false;
591
- }
592
- return true;
593
- });
594
-
595
- while (true) {
596
- await executePending(pending, agentMap, results, resultsById, fullContentsById, completed, {
597
- onTaskStart: options?.onTaskStart || null,
598
- onTaskDone: options?.onTaskDone || null,
599
- onToolStatus: options?.onToolStatus || null,
600
- resultTruncate,
601
- maxTaskRetries,
602
- });
603
- pending = [];
604
-
605
- if (!results.length || replanRound >= maxReplanRounds) break;
606
- if (results.length === 1 && results[0].success) break;
607
- if (looksObviouslyComplete(results)) break;
608
-
609
- const [achieved, missing] = await judgeGoalAchievement(snowAgent, goal, results);
610
- if (achieved) break;
611
-
612
- replanRound++;
613
- try {
614
- const extraTasks = await (snowAgent as any).replanForMissing(goal, results, missing,
615
- new Set(tasks.map((t: any) => t.id)));
616
- if (!extraTasks || extraTasks.length === 0) break;
617
- if (tasks.length + extraTasks.length > maxTotalTasks) break;
618
- tasks.push(...extraTasks);
619
- if (options?.onPlanned) {
620
- const proceed = await options.onPlanned(tasks);
621
- if (proceed === false) break;
622
- }
623
- pending = extraTasks.filter((t: any) => t.assignedTo && t.assignedTo !== 'snow');
624
- } catch {
625
- break;
626
- }
627
- }
628
-
629
- // Generate summary
630
- let summary: string;
631
- if (!results.length) {
632
- summary = '没有需要执行的任务。';
633
- } else if (results.length === 1) {
634
- summary = results[0].content;
635
- } else {
636
- const tpl = options?.summaryPromptTemplate || '请汇总以下所有子任务的执行结果:\n\n';
637
- let summaryPrompt = tpl;
638
- for (const r of results) {
639
- const status = r.success ? '成功' : '失败';
640
- summaryPrompt += `### 任务 ${r.id} (${r.agent}) - ${status}\n${(r.content || '').slice(0, 300)}\n\n`;
641
- }
642
- summary = await snowAgent.chatOneshot(summaryPrompt);
643
- }
644
-
645
- // Check if we hit the replan budget without full completion
646
- if (replanRound >= maxReplanRounds && results.length > 1) {
647
- try {
648
- const [achieved, missing] = await judgeGoalAchievement(snowAgent, goal, results);
649
- if (!achieved && missing) {
650
- summary = `[INCOMPLETE] 经过 ${maxReplanRounds + 1} 轮规划仍未完全达成目标。\n剩余缺口:${missing}\n\n${summary}`;
651
- }
652
- } catch { /* ignore */ }
653
- }
654
-
655
- return [tasks, results, summary];
656
- }
1
+ /**
2
+ * System factory — unified Agent creation and task orchestration.
3
+ *
4
+ * Avoids duplication between CLI and web entry points.
5
+ */
6
+
7
+ import { BaseAgent, Task as AgentTask, TaskState } from './agent';
8
+ import { MessageBus } from './bus';
9
+ import { loadConfig } from './config';
10
+ import { LLMClient } from './llm';
11
+ import { getLogger } from './logger';
12
+ import { SkillRegistry } from './skill';
13
+ import { ToolRegistry } from './tool';
14
+
15
+ const log = getLogger('factory');
16
+
17
+ export class SystemContext {
18
+ config: ReturnType<typeof loadConfig>;
19
+ bus: MessageBus;
20
+ llm: LLMClient;
21
+ agentMap: Map<string, BaseAgent>;
22
+ toolRegistry: ToolRegistry;
23
+ workspacePath: string = '';
24
+ mcp: any = null;
25
+ mcpStatus: string[] = [];
26
+
27
+ constructor(opts: {
28
+ config: ReturnType<typeof loadConfig>;
29
+ bus: MessageBus;
30
+ llm: LLMClient;
31
+ agentMap: Map<string, BaseAgent>;
32
+ toolRegistry: ToolRegistry;
33
+ workspacePath?: string;
34
+ mcp?: any;
35
+ mcpStatus?: string[];
36
+ }) {
37
+ this.config = opts.config;
38
+ this.bus = opts.bus;
39
+ this.llm = opts.llm;
40
+ this.agentMap = opts.agentMap;
41
+ this.toolRegistry = opts.toolRegistry;
42
+ this.workspacePath = opts.workspacePath || '';
43
+ this.mcp = opts.mcp || null;
44
+ this.mcpStatus = opts.mcpStatus || [];
45
+ }
46
+
47
+ async initAll(): Promise<void> {
48
+ if (this.mcp) {
49
+ try {
50
+ this.mcpStatus = await this.mcp.connectAll();
51
+ if (this.mcpStatus.length > 0) {
52
+ log.info('mcp_connected', { servers: this.mcpStatus.join(', ') });
53
+ }
54
+ } catch (e) {
55
+ log.warn('mcp_connect_all_failed', { error: String(e) });
56
+ }
57
+ }
58
+ for (const agent of this.agentMap.values()) {
59
+ await agent.init();
60
+ }
61
+ }
62
+
63
+ async closeAll(): Promise<void> {
64
+ for (const agent of this.agentMap.values()) {
65
+ await agent.close();
66
+ }
67
+ if (this.mcp) {
68
+ try { await this.mcp.closeAll(); } catch (e) { log.warn('mcp_close_failed', { error: String(e) }); }
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Bootstrap the full system: config, bus, LLM, tools, skills, plugins, agents.
75
+ */
76
+ export function createSystemContext(): SystemContext {
77
+ const config = loadConfig();
78
+
79
+ // session_start hooks — user-configured shell commands (see core/hooks)
80
+ try {
81
+ const { loadHooks, runSessionStartHooks } = require('./hooks');
82
+ const hooks = loadHooks(config);
83
+ if (hooks.sessionStart.length > 0) runSessionStartHooks(hooks);
84
+ } catch { /* hooks must never block startup */ }
85
+
86
+ let workspacePath = '';
87
+ try {
88
+ const { resolveWorkspacePath, initWorkspace } = require('./workspace');
89
+ const wsRoot = resolveWorkspacePath((config as any).workspace?.path || 'auto');
90
+ initWorkspace(wsRoot);
91
+ workspacePath = wsRoot;
92
+ log.info('workspace', { path: workspacePath });
93
+ } catch { /* ignore */ }
94
+
95
+ const bus = new MessageBus();
96
+
97
+ // Shared registries
98
+ const baseToolRegistry = new ToolRegistry();
99
+ const baseSkillRegistry = new SkillRegistry();
100
+
101
+ // Register builtin tools
102
+ try {
103
+ const { registerBuiltinTools } = require('../tools/builtin');
104
+ registerBuiltinTools(baseToolRegistry);
105
+ } catch (e) {
106
+ log.warn('builtin_tools_not_available', { error: String(e) });
107
+ }
108
+
109
+ // Register all skills
110
+ try {
111
+ const { registerAllSkills } = require('../skills/loader');
112
+ registerAllSkills(baseSkillRegistry);
113
+ } catch (e) {
114
+ log.warn('skills_not_available', { error: String(e) });
115
+ }
116
+
117
+ // Load plugins
118
+ try {
119
+ const { PluginLoader } = require('../plugins/loader');
120
+ const pluginLoader = new PluginLoader(baseToolRegistry);
121
+ const pluginConfig = (config as any).plugins;
122
+ const pluginDirs = pluginConfig?.enabled ? (pluginConfig.directories || []) : [];
123
+ pluginLoader.loadFromDirectories(pluginDirs);
124
+ } catch (e) {
125
+ log.warn('plugins_not_available', { error: String(e) });
126
+ }
127
+
128
+ // Configure MCP manager
129
+ let mcpManager: any = null;
130
+ try {
131
+ const { MCPManager, loadPersistedServers, loadProjectMcpJson } = require('./mcp');
132
+ mcpManager = new MCPManager(baseToolRegistry);
133
+ const persisted = loadPersistedServers();
134
+ const mcpServers = (config as any).mcp?.servers || [];
135
+ const projectServers = loadProjectMcpJson(); // Claude Code 标准 .mcp.json
136
+ // dedupe by name — project .mcp.json wins over runtime-added over config
137
+ const byName = new Map<string, any>();
138
+ for (const s of [...mcpServers, ...persisted, ...projectServers]) {
139
+ if (s?.name) byName.set(s.name, s);
140
+ }
141
+ const allServers = [...byName.values()];
142
+ if (allServers.length > 0) {
143
+ mcpManager.configure(allServers);
144
+ }
145
+ } catch (e) {
146
+ log.warn('mcp_not_available', { error: String(e) });
147
+ }
148
+
149
+ // Shared LLM client
150
+ const llm = new LLMClient(config as any, baseToolRegistry);
151
+
152
+ // Per-agent registries
153
+ const agents = new Map<string, BaseAgent>();
154
+
155
+ // Try to dynamically load agent classes
156
+ const agentNames = ['fog', 'rain', 'frost', 'snow', 'dew', 'fair'];
157
+
158
+ for (const name of agentNames) {
159
+ const agentRegistry = new ToolRegistry();
160
+ agentRegistry.merge(baseToolRegistry);
161
+ const agentSkills = new SkillRegistry();
162
+ agentSkills.merge(baseSkillRegistry);
163
+
164
+ try {
165
+ // Try dynamic import
166
+ const clsName = name.charAt(0).toUpperCase() + name.slice(1) + 'Agent';
167
+ // Use require for now since dynamic imports are async
168
+ let AgentClass: any = null;
169
+ try {
170
+ const mod = require(`../agents/${name}`);
171
+ AgentClass = mod[clsName];
172
+ } catch {
173
+ log.warn('agent_class_missing', { agent: name });
174
+ continue;
175
+ }
176
+
177
+ if (!AgentClass) {
178
+ log.warn('agent_class_not_found', { agent: name, class: clsName });
179
+ continue;
180
+ }
181
+
182
+ const agent = new AgentClass(
183
+ config,
184
+ llm,
185
+ bus,
186
+ agentRegistry,
187
+ agentSkills
188
+ ) as BaseAgent;
189
+
190
+ // Register delegate_to tool
191
+ try {
192
+ const { createDelegateTool } = require('../tools/delegate');
193
+ agentRegistry.register(createDelegateTool(agents, agent));
194
+ } catch (e) {
195
+ log.warn('delegate_tool_not_available', { agent: name, error: String(e) });
196
+ }
197
+
198
+ // Register model self-service tools (list_models / set_my_model)
199
+ try {
200
+ const { createModelTools } = require('../tools/model_tool');
201
+ for (const t of createModelTools(name, config)) agentRegistry.register(t);
202
+ } catch (e) {
203
+ log.warn('model_tools_not_available', { agent: name, error: String(e) });
204
+ }
205
+
206
+ // Register the task-checklist tool (todo_write)
207
+ try {
208
+ const { createTodoTool } = require('../tools/todo');
209
+ agentRegistry.register(createTodoTool(agent));
210
+ } catch (e) {
211
+ log.warn('todo_tool_not_available', { agent: name, error: String(e) });
212
+ }
213
+
214
+ agents.set(name, agent);
215
+ } catch (e) {
216
+ log.warn('agent_creation_failed', { agent: name, error: String(e) });
217
+ }
218
+ }
219
+
220
+ // Bind agents to MCP
221
+ if (mcpManager) {
222
+ try {
223
+ mcpManager.bindAgents(agents);
224
+ } catch (e) {
225
+ log.warn('mcp_bind_failed', { error: String(e) });
226
+ }
227
+ }
228
+
229
+ return new SystemContext({
230
+ config,
231
+ bus,
232
+ llm,
233
+ agentMap: agents,
234
+ toolRegistry: baseToolRegistry,
235
+ workspacePath,
236
+ mcp: mcpManager,
237
+ });
238
+ }
239
+
240
+ // ── Task orchestration ──
241
+
242
+ export class TaskExecutionResult {
243
+ id: string;
244
+ agent: string;
245
+ description: string;
246
+ success: boolean;
247
+ content: string;
248
+
249
+ constructor(opts: {
250
+ id: string;
251
+ agent: string;
252
+ description: string;
253
+ success: boolean;
254
+ content: string;
255
+ }) {
256
+ this.id = opts.id;
257
+ this.agent = opts.agent;
258
+ this.description = opts.description;
259
+ this.success = opts.success;
260
+ this.content = opts.content;
261
+ }
262
+ }
263
+
264
+ // Placeholder phrases that mean the LLM gave up
265
+ const PLACEHOLDER_PATTERNS = [
266
+ 'done', 'ok', 'completed', 'task completed', 'task done', 'task finished',
267
+ 'finished', '已完成', '完成了', '好的', '好了', 'ok!',
268
+ ];
269
+
270
+ const STATUS_REPORT_KEYWORDS = [
271
+ '已完成', '完成了', '已成功', '已经完成', '工作已', '任务已',
272
+ 'task complete', 'task is complete', 'i have completed', "i've completed",
273
+ 'successfully completed', 'finished the', 'i finished',
274
+ '已经写好', '已经写完', '已经做好', '写完了', '做完了',
275
+ ];
276
+
277
+ const DELIVERABLE_MARKERS = [
278
+ '```', 'http://', 'https://', '|', '## ', '###', '- [ ]', '1.', '/', '\\',
279
+ ];
280
+
281
+ function isThinContent(content: string): boolean {
282
+ if (!content) return true;
283
+ const stripped = content.trim();
284
+ if (!stripped) return true;
285
+ const lowered = stripped.toLowerCase();
286
+ if (lowered.startsWith('[truncated]') || lowered.startsWith('[error:')) return true;
287
+ const bare = lowered.replace(/[.!?。!?\s]+$/, '').trim();
288
+ if (PLACEHOLDER_PATTERNS.includes(bare)) return true;
289
+ return (
290
+ stripped.length <= 200 &&
291
+ STATUS_REPORT_KEYWORDS.some(k => lowered.includes(k)) &&
292
+ !DELIVERABLE_MARKERS.some(m => stripped.includes(m))
293
+ );
294
+ }
295
+
296
+ const RESULT_FAILURE_MARKERS = [
297
+ '[truncated]', '[stuck]', '[Error', 'Error:',
298
+ '未能完成', '无法完成', '[cycle detected]', '[CircuitBreakerOpen]',
299
+ ];
300
+
301
+ function looksObviouslyComplete(results: TaskExecutionResult[]): boolean {
302
+ if (!results.length) return false;
303
+ return results.every(r => {
304
+ if (!r.success) return false;
305
+ const body = (r.content || '').trim();
306
+ if (body.length < 400) return false;
307
+ return !RESULT_FAILURE_MARKERS.some(m => body.includes(m));
308
+ });
309
+ }
310
+
311
+ async function executeWithRetry(
312
+ agent: BaseAgent,
313
+ aTask: any,
314
+ maxAttempts: number,
315
+ onStatus?: ((status: string) => void) | null
316
+ ): Promise<any> {
317
+ let lastResult: any = null;
318
+ const originalDescription = aTask.description;
319
+
320
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
321
+ try {
322
+ const result = await agent.executeTask(aTask, onStatus);
323
+ const content = (result.content || '').trim();
324
+ const truncated = (result as any).truncated === true;
325
+ const ok = result.success && !isThinContent(content);
326
+
327
+ if (ok && !truncated) return result;
328
+ lastResult = result;
329
+
330
+ if (attempt < maxAttempts) {
331
+ const reason = truncated
332
+ ? 'previous attempt was truncated'
333
+ : 'previous attempt was empty or a placeholder ack';
334
+ aTask.description = `${originalDescription}\n\n[retry ${attempt + 1}/${maxAttempts}] ${reason}. You MUST produce the actual deliverable.`;
335
+ }
336
+ } catch (e) {
337
+ lastResult = { success: false, content: `Attempt ${attempt} threw: ${e}` };
338
+ }
339
+ if (attempt < maxAttempts) {
340
+ await new Promise(r => setTimeout(r, Math.min(500 * Math.pow(2, attempt - 1), 2000)));
341
+ }
342
+ }
343
+
344
+ aTask.description = originalDescription;
345
+ return lastResult || { success: false, content: `All ${maxAttempts} attempts failed` };
346
+ }
347
+
348
+ async function executePending(
349
+ pending: any[],
350
+ agentMap: Map<string, BaseAgent>,
351
+ results: TaskExecutionResult[],
352
+ resultsById: Map<string, TaskExecutionResult>,
353
+ fullContentsById: Map<string, string>,
354
+ completed: Set<string>,
355
+ options: {
356
+ onTaskStart?: ((task: any) => Promise<void>) | null;
357
+ onTaskDone?: ((task: any, result: TaskExecutionResult) => Promise<void>) | null;
358
+ onToolStatus?: ((status: string) => void) | null;
359
+ resultTruncate?: number | null;
360
+ maxTaskRetries: number;
361
+ }
362
+ ): Promise<void> {
363
+ while (pending.length > 0) {
364
+ const ready = pending.filter(t => t.allDeps.every((dep: string) => completed.has(dep)));
365
+ if (ready.length === 0) {
366
+ for (const t of pending) {
367
+ const missing = t.allDeps.filter((d: string) => !completed.has(d));
368
+ t.transitionTo(TaskState.FAILED);
369
+ const r = new TaskExecutionResult({
370
+ id: t.id, agent: t.assignedTo || '',
371
+ description: t.description, success: false,
372
+ content: `[dependency missing] task ${t.id} requires ${missing.join(', ')} which never completed`,
373
+ });
374
+ results.push(r);
375
+ resultsById.set(r.id, r);
376
+ completed.add(r.id);
377
+ if (options.onTaskDone) await options.onTaskDone(t, r);
378
+ }
379
+ pending.length = 0;
380
+ return;
381
+ }
382
+
383
+ for (const t of ready) t.transitionTo(TaskState.RUNNING);
384
+
385
+ const batchResults = await Promise.all(
386
+ ready.map(async (t: any) => {
387
+ const agent = agentMap.get(t.assignedTo);
388
+ if (!agent) {
389
+ return new TaskExecutionResult({
390
+ id: t.id, agent: t.assignedTo || '',
391
+ description: t.description, success: false,
392
+ content: `Agent '${t.assignedTo}' not found`,
393
+ });
394
+ }
395
+ if (options.onTaskStart) await options.onTaskStart(t);
396
+
397
+ let description = t.description;
398
+ const upstreamSections: string[] = [];
399
+ const thinUpstreamIds: string[] = [];
400
+
401
+ for (const depId of t.allDeps) {
402
+ if (resultsById.has(depId)) {
403
+ const parent = resultsById.get(depId)!;
404
+ const fullContent = fullContentsById.get(depId) || parent.content || '';
405
+ if (isThinContent(fullContent)) {
406
+ thinUpstreamIds.push(parent.id);
407
+ upstreamSections.push(
408
+ `## 上游产出缺失 (task ${parent.id} · ${parent.agent})\n` +
409
+ `⚠ 上游任务声称完成但未产出实际内容。\n` +
410
+ `原始回复:\n${fullContent}`
411
+ );
412
+ } else {
413
+ upstreamSections.push(
414
+ `## 上游产出 (task ${parent.id} · ${parent.agent})\n${fullContent}`
415
+ );
416
+ }
417
+ }
418
+ }
419
+ if (upstreamSections.length > 0) {
420
+ description = `${t.description}\n\n${upstreamSections.join('\n\n')}`;
421
+ }
422
+ if (thinUpstreamIds.length > 0) {
423
+ log.warn('thin_upstream', { task: t.id, thin_upstream: thinUpstreamIds });
424
+ }
425
+
426
+ const aTask = new AgentTask({
427
+ id: t.id,
428
+ description,
429
+ assignedTo: t.assignedTo,
430
+ parentId: t.parentId,
431
+ metadata: t.metadata,
432
+ });
433
+
434
+ const result = await executeWithRetry(agent, aTask, options.maxTaskRetries, options.onToolStatus);
435
+
436
+ if (result.success) t.transitionTo(TaskState.COMPLETED);
437
+ else t.transitionTo(TaskState.FAILED);
438
+
439
+ const full = result.content || '';
440
+ fullContentsById.set(t.id, full);
441
+ const tr = options.resultTruncate != null && full.length > options.resultTruncate
442
+ ? full.slice(0, options.resultTruncate) : full;
443
+
444
+ const r = new TaskExecutionResult({
445
+ id: t.id, agent: t.assignedTo || '',
446
+ description: t.description, success: result.success,
447
+ content: tr,
448
+ });
449
+ if (options.onTaskDone) await options.onTaskDone(t, r);
450
+ return r;
451
+ })
452
+ );
453
+
454
+ for (const r of batchResults) {
455
+ results.push(r);
456
+ resultsById.set(r.id, r);
457
+ completed.add(r.id);
458
+ }
459
+ for (const t of ready) {
460
+ const idx = pending.indexOf(t);
461
+ if (idx >= 0) pending.splice(idx, 1);
462
+ }
463
+ }
464
+ }
465
+
466
+ async function judgeGoalAchievement(
467
+ snow: BaseAgent,
468
+ goal: string,
469
+ results: TaskExecutionResult[]
470
+ ): Promise<[boolean, string]> {
471
+ const bullets = results.map(r => {
472
+ const status = r.success ? '成功' : '失败';
473
+ const excerpt = (r.content || '').slice(0, 800);
474
+ return `- [task ${r.id} · agent=${r.agent} · status=${status}] len=${(r.content || '').length}chars\n ${excerpt}`;
475
+ }).join('\n');
476
+
477
+ const prompt = `你是一名极严格的项目验收员。验证 sub-task 真的产出了可验证的交付物。
478
+
479
+ ## 验收规则(严格执行)
480
+
481
+ 1. **「调研/搜集/查询」类**:必须列出具体对象名称和数据/特性/链接。
482
+ 2. **「撰写/生成/写作」类**:必须包含实际的文本/代码/markdown。
483
+ 3. **「审查/审计/对比」类**:必须列出具体问题点。
484
+ 4. **核心交付物缺失绝不容忍**。
485
+
486
+ ## 用户原目标
487
+ ${goal}
488
+
489
+ ## 子任务执行结果
490
+ ${bullets}
491
+
492
+ 严格按下列 JSON 格式输出(除 JSON 之外不要任何其他字符):
493
+ {"achieved": true/false, "missing": "若未达成,逐项列出缺什么"}`;
494
+
495
+ try {
496
+ const raw = await snow.chatOneshot(prompt);
497
+ let text = raw.trim();
498
+ if (text.startsWith('```')) {
499
+ text = text.replace(/```/g, '').replace(/^json/i, '').trim();
500
+ }
501
+ const start = text.indexOf('{');
502
+ const end = text.lastIndexOf('}');
503
+ if (start < 0 || end <= start) return [true, ''];
504
+ const parsed = JSON.parse(text.slice(start, end + 1));
505
+ return [!!parsed.achieved, (parsed.missing || '').trim()];
506
+ } catch {
507
+ return [true, ''];
508
+ }
509
+ }
510
+
511
+ export async function orchestrateTask(
512
+ goal: string,
513
+ agentMap: Map<string, BaseAgent>,
514
+ snow: BaseAgent | null = null,
515
+ options?: {
516
+ onTaskStart?: ((task: any) => Promise<void>) | null;
517
+ onTaskDone?: ((task: any, result: TaskExecutionResult) => Promise<void>) | null;
518
+ onPlanned?: ((tasks: any[]) => Promise<boolean | null>) | null;
519
+ onToolStatus?: ((status: string) => void) | null;
520
+ resultTruncate?: number | null;
521
+ summaryPromptTemplate?: string;
522
+ maxTaskRetries?: number;
523
+ maxReplanRounds?: number;
524
+ maxTotalTasks?: number;
525
+ resume?: boolean;
526
+ }
527
+ ): Promise<[any[], TaskExecutionResult[], string]> {
528
+ const snowAgent = snow || agentMap.get('snow') || null;
529
+ if (!snowAgent) {
530
+ return [[], [], 'Snow agent not available'];
531
+ }
532
+
533
+ const maxTaskRetries = options?.maxTaskRetries ?? 3;
534
+ const maxReplanRounds = options?.maxReplanRounds ?? 1;
535
+ const maxTotalTasks = options?.maxTotalTasks ?? 6;
536
+ const resultTruncate = options?.resultTruncate ?? 500;
537
+
538
+ // Try pipeline match first
539
+ let tasks: any[];
540
+ try {
541
+ const { matchPipeline, buildTasksFromPipeline } = require('./pipelines');
542
+ const matched = matchPipeline(goal);
543
+ if (matched) {
544
+ tasks = buildTasksFromPipeline(matched, goal);
545
+ } else {
546
+ tasks = await (snowAgent as any).orchestrate(goal);
547
+ }
548
+ } catch {
549
+ tasks = await (snowAgent as any).orchestrate(goal);
550
+ }
551
+
552
+ if (!tasks || tasks.length === 0) {
553
+ return [[], [], 'No tasks were planned'];
554
+ }
555
+
556
+ // Notify caller of the plan
557
+ if (options?.onPlanned) {
558
+ const proceed = await options.onPlanned(tasks);
559
+ if (proceed === false) {
560
+ return [tasks, [], '[CANCELLED] plan rejected before execution'];
561
+ }
562
+ }
563
+
564
+ const completed = new Set<string>();
565
+ const results: TaskExecutionResult[] = [];
566
+ const resultsById = new Map<string, TaskExecutionResult>();
567
+ const fullContentsById = new Map<string, string>();
568
+ let pending = tasks.filter((t: any) => t.assignedTo && t.assignedTo !== 'snow');
569
+ let replanRound = 0;
570
+
571
+ // Cycle detection
572
+ function hasCycle(t: any, path: Set<string>): boolean {
573
+ if (path.has(t.id)) return true;
574
+ path.add(t.id);
575
+ for (const depId of t.allDeps || []) {
576
+ const dep = tasks.find((x: any) => x.id === depId);
577
+ if (dep && hasCycle(dep, new Set(path))) return true;
578
+ }
579
+ return false;
580
+ }
581
+
582
+ pending = pending.filter((t: any) => {
583
+ if (hasCycle(t, new Set())) {
584
+ results.push(new TaskExecutionResult({
585
+ id: t.id, agent: t.assignedTo || '',
586
+ description: t.description, success: false,
587
+ content: `[cycle detected] task ${t.id} has circular dependency`,
588
+ }));
589
+ completed.add(t.id);
590
+ return false;
591
+ }
592
+ return true;
593
+ });
594
+
595
+ while (true) {
596
+ await executePending(pending, agentMap, results, resultsById, fullContentsById, completed, {
597
+ onTaskStart: options?.onTaskStart || null,
598
+ onTaskDone: options?.onTaskDone || null,
599
+ onToolStatus: options?.onToolStatus || null,
600
+ resultTruncate,
601
+ maxTaskRetries,
602
+ });
603
+ pending = [];
604
+
605
+ if (!results.length || replanRound >= maxReplanRounds) break;
606
+ if (results.length === 1 && results[0].success) break;
607
+ if (looksObviouslyComplete(results)) break;
608
+
609
+ const [achieved, missing] = await judgeGoalAchievement(snowAgent, goal, results);
610
+ if (achieved) break;
611
+
612
+ replanRound++;
613
+ try {
614
+ const extraTasks = await (snowAgent as any).replanForMissing(goal, results, missing,
615
+ new Set(tasks.map((t: any) => t.id)));
616
+ if (!extraTasks || extraTasks.length === 0) break;
617
+ if (tasks.length + extraTasks.length > maxTotalTasks) break;
618
+ tasks.push(...extraTasks);
619
+ if (options?.onPlanned) {
620
+ const proceed = await options.onPlanned(tasks);
621
+ if (proceed === false) break;
622
+ }
623
+ pending = extraTasks.filter((t: any) => t.assignedTo && t.assignedTo !== 'snow');
624
+ } catch {
625
+ break;
626
+ }
627
+ }
628
+
629
+ // Generate summary
630
+ let summary: string;
631
+ if (!results.length) {
632
+ summary = '没有需要执行的任务。';
633
+ } else if (results.length === 1) {
634
+ summary = results[0].content;
635
+ } else {
636
+ const tpl = options?.summaryPromptTemplate || '请汇总以下所有子任务的执行结果:\n\n';
637
+ let summaryPrompt = tpl;
638
+ for (const r of results) {
639
+ const status = r.success ? '成功' : '失败';
640
+ summaryPrompt += `### 任务 ${r.id} (${r.agent}) - ${status}\n${(r.content || '').slice(0, 300)}\n\n`;
641
+ }
642
+ summary = await snowAgent.chatOneshot(summaryPrompt);
643
+ }
644
+
645
+ // Check if we hit the replan budget without full completion
646
+ if (replanRound >= maxReplanRounds && results.length > 1) {
647
+ try {
648
+ const [achieved, missing] = await judgeGoalAchievement(snowAgent, goal, results);
649
+ if (!achieved && missing) {
650
+ summary = `[INCOMPLETE] 经过 ${maxReplanRounds + 1} 轮规划仍未完全达成目标。\n剩余缺口:${missing}\n\n${summary}`;
651
+ }
652
+ } catch { /* ignore */ }
653
+ }
654
+
655
+ return [tasks, results, summary];
656
+ }