skyloom 1.12.0 → 1.13.1
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.
- package/.github/workflows/ci.yml +36 -36
- package/README.md +137 -46
- package/config/default.yaml +43 -47
- package/config/models.yaml +155 -155
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/main.js +127 -74
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts +52 -19
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +198 -265
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent/task.d.ts +58 -0
- package/dist/core/agent/task.d.ts.map +1 -0
- package/dist/core/agent/task.js +83 -0
- package/dist/core/agent/task.js.map +1 -0
- package/dist/core/agent.d.ts +2 -45
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +61 -145
- package/dist/core/agent.js.map +1 -1
- package/dist/core/agent_helpers.d.ts +10 -0
- package/dist/core/agent_helpers.d.ts.map +1 -1
- package/dist/core/agent_helpers.js +39 -0
- package/dist/core/agent_helpers.js.map +1 -1
- package/dist/core/catalog.d.ts +71 -0
- package/dist/core/catalog.d.ts.map +1 -0
- package/dist/core/catalog.js +176 -0
- package/dist/core/catalog.js.map +1 -0
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +12 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/factory.js +16 -16
- package/dist/core/llm.d.ts +7 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +139 -7
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/memory.d.ts.map +1 -1
- package/dist/core/memory.js +69 -62
- package/dist/core/memory.js.map +1 -1
- package/dist/core/theme.d.ts +46 -0
- package/dist/core/theme.d.ts.map +1 -0
- package/dist/core/theme.js +42 -0
- package/dist/core/theme.js.map +1 -0
- package/dist/web/server.js +542 -519
- package/dist/web/server.js.map +1 -1
- package/docs/AESTHETIC_DESIGN.md +144 -0
- package/docs/OPTIMIZATION_PLAN.md +178 -0
- package/package.json +60 -60
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/main.ts +417 -372
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +174 -223
- package/src/core/agent/task.ts +100 -0
- package/src/core/agent.ts +1446 -1549
- package/src/core/agent_helpers.ts +496 -461
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -0
- package/src/core/checkpoint.ts +94 -94
- package/src/core/config.ts +20 -4
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +627 -627
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +108 -5
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp_server.ts +176 -176
- package/src/core/memory.ts +1178 -1171
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +342 -342
- package/src/core/theme.ts +65 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +46 -46
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +380 -380
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/web/server.ts +660 -634
- package/src/web/tts.ts +93 -93
- package/tests/agent_helpers.test.ts +48 -0
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -0
- package/tests/config.test.ts +41 -0
- package/tests/icons.test.ts +45 -45
- package/tests/memory.test.ts +147 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/task.test.ts +60 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/tests/tui.test.ts +67 -0
- package/vitest.config.ts +17 -17
package/src/core/checkpoint.ts
CHANGED
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Orchestration checkpoint — save/restore task state.
|
|
3
|
-
*
|
|
4
|
-
* Writes ~/.skyloom/task_checkpoint.json so a long-running orchestration
|
|
5
|
-
* interrupted by Ctrl-C can be resumed.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as fs from 'fs';
|
|
9
|
-
import * as path from 'path';
|
|
10
|
-
import { USER_CONFIG_DIR } from './config';
|
|
11
|
-
|
|
12
|
-
function checkpointPath(): string {
|
|
13
|
-
return path.join(USER_CONFIG_DIR, 'task_checkpoint.json');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Save current orchestration state so it can be resumed later.
|
|
18
|
-
*/
|
|
19
|
-
export function save(
|
|
20
|
-
goal: string,
|
|
21
|
-
tasks: any[],
|
|
22
|
-
results: any[],
|
|
23
|
-
completedIds?: Set<string>
|
|
24
|
-
): void {
|
|
25
|
-
const cids = completedIds || new Set(results.map((r: any) => r.id));
|
|
26
|
-
const payload = {
|
|
27
|
-
goal,
|
|
28
|
-
tasks: tasks.map(serializeTask),
|
|
29
|
-
results: results.map(serializeResult),
|
|
30
|
-
completed_ids: Array.from(cids).sort(),
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const p = checkpointPath();
|
|
34
|
-
const dir = path.dirname(p);
|
|
35
|
-
if (!fs.existsSync(dir)) {
|
|
36
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const tmp = p + '.tmp';
|
|
40
|
-
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
|
|
41
|
-
fs.renameSync(tmp, p);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Return the last saved checkpoint dict, or null if none / unreadable.
|
|
46
|
-
*/
|
|
47
|
-
export function load(): Record<string, any> | null {
|
|
48
|
-
const p = checkpointPath();
|
|
49
|
-
if (!fs.existsSync(p)) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
try {
|
|
53
|
-
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
54
|
-
return typeof data === 'object' && data !== null ? data : null;
|
|
55
|
-
} catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Delete the checkpoint file.
|
|
62
|
-
*/
|
|
63
|
-
export function clear(): void {
|
|
64
|
-
try {
|
|
65
|
-
const p = checkpointPath();
|
|
66
|
-
if (fs.existsSync(p)) {
|
|
67
|
-
fs.unlinkSync(p);
|
|
68
|
-
}
|
|
69
|
-
} catch {
|
|
70
|
-
// Ignore cleanup errors
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ── Serialization helpers ──
|
|
75
|
-
|
|
76
|
-
function serializeTask(t: any): Record<string, any> {
|
|
77
|
-
return {
|
|
78
|
-
id: t.id,
|
|
79
|
-
description: t.description,
|
|
80
|
-
assigned_to: t.assignedTo ?? t.assigned_to,
|
|
81
|
-
all_deps: t.allDeps ?? t.all_deps ?? [],
|
|
82
|
-
status: t.status?.value ?? t.status ?? 'unknown',
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function serializeResult(r: any): Record<string, any> {
|
|
87
|
-
return {
|
|
88
|
-
id: r.id,
|
|
89
|
-
agent: r.agent,
|
|
90
|
-
description: r.description,
|
|
91
|
-
success: r.success,
|
|
92
|
-
content: r.content,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration checkpoint — save/restore task state.
|
|
3
|
+
*
|
|
4
|
+
* Writes ~/.skyloom/task_checkpoint.json so a long-running orchestration
|
|
5
|
+
* interrupted by Ctrl-C can be resumed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { USER_CONFIG_DIR } from './config';
|
|
11
|
+
|
|
12
|
+
function checkpointPath(): string {
|
|
13
|
+
return path.join(USER_CONFIG_DIR, 'task_checkpoint.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Save current orchestration state so it can be resumed later.
|
|
18
|
+
*/
|
|
19
|
+
export function save(
|
|
20
|
+
goal: string,
|
|
21
|
+
tasks: any[],
|
|
22
|
+
results: any[],
|
|
23
|
+
completedIds?: Set<string>
|
|
24
|
+
): void {
|
|
25
|
+
const cids = completedIds || new Set(results.map((r: any) => r.id));
|
|
26
|
+
const payload = {
|
|
27
|
+
goal,
|
|
28
|
+
tasks: tasks.map(serializeTask),
|
|
29
|
+
results: results.map(serializeResult),
|
|
30
|
+
completed_ids: Array.from(cids).sort(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const p = checkpointPath();
|
|
34
|
+
const dir = path.dirname(p);
|
|
35
|
+
if (!fs.existsSync(dir)) {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const tmp = p + '.tmp';
|
|
40
|
+
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
|
|
41
|
+
fs.renameSync(tmp, p);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Return the last saved checkpoint dict, or null if none / unreadable.
|
|
46
|
+
*/
|
|
47
|
+
export function load(): Record<string, any> | null {
|
|
48
|
+
const p = checkpointPath();
|
|
49
|
+
if (!fs.existsSync(p)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
54
|
+
return typeof data === 'object' && data !== null ? data : null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Delete the checkpoint file.
|
|
62
|
+
*/
|
|
63
|
+
export function clear(): void {
|
|
64
|
+
try {
|
|
65
|
+
const p = checkpointPath();
|
|
66
|
+
if (fs.existsSync(p)) {
|
|
67
|
+
fs.unlinkSync(p);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Ignore cleanup errors
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Serialization helpers ──
|
|
75
|
+
|
|
76
|
+
function serializeTask(t: any): Record<string, any> {
|
|
77
|
+
return {
|
|
78
|
+
id: t.id,
|
|
79
|
+
description: t.description,
|
|
80
|
+
assigned_to: t.assignedTo ?? t.assigned_to,
|
|
81
|
+
all_deps: t.allDeps ?? t.all_deps ?? [],
|
|
82
|
+
status: t.status?.value ?? t.status ?? 'unknown',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function serializeResult(r: any): Record<string, any> {
|
|
87
|
+
return {
|
|
88
|
+
id: r.id,
|
|
89
|
+
agent: r.agent,
|
|
90
|
+
description: r.description,
|
|
91
|
+
success: r.success,
|
|
92
|
+
content: r.content,
|
|
93
|
+
};
|
|
94
|
+
}
|
package/src/core/config.ts
CHANGED
|
@@ -223,6 +223,14 @@ export interface SkyloomConfig {
|
|
|
223
223
|
agents: Record<string, AgentConfig>;
|
|
224
224
|
providers?: Record<string, ProviderEntry>;
|
|
225
225
|
models?: Record<string, ModelEntry[]>;
|
|
226
|
+
/** Top-level default model chosen by the setup wizard (e.g. "deepseek-v4-flash"). */
|
|
227
|
+
default_model?: string;
|
|
228
|
+
/** Top-level default provider chosen by the setup wizard. */
|
|
229
|
+
default_provider?: string;
|
|
230
|
+
/** LLM defaults block from default.yaml / user config (snake_case keys). */
|
|
231
|
+
llm?: Record<string, any>;
|
|
232
|
+
/** Other passthrough top-level config (memory, workspace, cli, mcp, plugins, tts…). */
|
|
233
|
+
[key: string]: any;
|
|
226
234
|
}
|
|
227
235
|
|
|
228
236
|
/**
|
|
@@ -261,18 +269,26 @@ export function mergeConfigs(defaultCfg: SkyloomConfig, userCfg: SkyloomConfig |
|
|
|
261
269
|
return defaultCfg;
|
|
262
270
|
}
|
|
263
271
|
|
|
272
|
+
// Preserve all top-level keys (default_model, default_provider, llm, memory,
|
|
273
|
+
// workspace, …) with the user winning, then deep-merge the known sub-objects.
|
|
264
274
|
return {
|
|
275
|
+
...defaultCfg,
|
|
276
|
+
...userCfg,
|
|
265
277
|
agents: {
|
|
266
278
|
...defaultCfg.agents,
|
|
267
279
|
...userCfg.agents,
|
|
268
280
|
},
|
|
269
281
|
providers: {
|
|
270
|
-
...defaultCfg.providers,
|
|
271
|
-
...userCfg.providers,
|
|
282
|
+
...(defaultCfg.providers || {}),
|
|
283
|
+
...(userCfg.providers || {}),
|
|
272
284
|
},
|
|
273
285
|
models: {
|
|
274
|
-
...defaultCfg.models,
|
|
275
|
-
...userCfg.models,
|
|
286
|
+
...(defaultCfg.models || {}),
|
|
287
|
+
...(userCfg.models || {}),
|
|
288
|
+
},
|
|
289
|
+
llm: {
|
|
290
|
+
...(defaultCfg.llm || {}),
|
|
291
|
+
...(userCfg.llm || {}),
|
|
276
292
|
},
|
|
277
293
|
};
|
|
278
294
|
}
|
package/src/core/estimate.ts
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 资源估算模块 — Token & time budget estimation for task planning.
|
|
3
|
-
*
|
|
4
|
-
* Helps Snow and other planning agents estimate the cost of
|
|
5
|
-
* proposed sub-tasks before committing to execution.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { Task } from "./agent";
|
|
9
|
-
|
|
10
|
-
/* ═══════════════════════════════════════
|
|
11
|
-
Token estimation
|
|
12
|
-
═══════════════════════════════════════ */
|
|
13
|
-
const CJK_REGEX = /[一-鿿-ゟ가-㐀-䶿]/g;
|
|
14
|
-
|
|
15
|
-
/** Estimate tokens for a given text (CJK ~2 each, ASCII ~4 chars each). */
|
|
16
|
-
export function estimateTokens(text: string): number {
|
|
17
|
-
const cjk = (text.match(CJK_REGEX) || []).length;
|
|
18
|
-
return cjk * 2 + Math.ceil((text.length - cjk) / 4);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/* ═══════════════════════════════════════
|
|
22
|
-
Per-task-type cost estimates
|
|
23
|
-
═══════════════════════════════════════ */
|
|
24
|
-
const TASK_TYPE_PATTERNS: Array<[RegExp, number, number]> = [
|
|
25
|
-
// [pattern, estimated tokens, estimated tools]
|
|
26
|
-
[/read|read_file|grep|search|查|搜索|list/i, 2000, 2],
|
|
27
|
-
[/write|write_file|生成|写|create|implement/i, 4000, 5],
|
|
28
|
-
[/edit|edit_file|改|修改|fix|修复/i, 3000, 3],
|
|
29
|
-
[/delete|delete_file|删|rm/i, 1500, 2],
|
|
30
|
-
[/deploy|部署|publish|发布|release/i, 8000, 8],
|
|
31
|
-
[/review|审查|audit|审计|scan|扫描/i, 5000, 4],
|
|
32
|
-
[/test|测试|run_test|coverage/i, 3000, 3],
|
|
33
|
-
[/research|研究|调研|analyze|分析/i, 6000, 4],
|
|
34
|
-
[/orchestrate|编排|multi-step|多步/i, 12000, 10],
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
/** Estimate cost for a single task description. */
|
|
38
|
-
export function estimateTaskCost(description: string): { tokens: number; tools: number; timeSeconds: number } {
|
|
39
|
-
let tokens = 2000; // base
|
|
40
|
-
let tools = 2; // base
|
|
41
|
-
for (const [pattern, t, tc] of TASK_TYPE_PATTERNS) {
|
|
42
|
-
if (pattern.test(description)) { tokens = Math.max(tokens, t); tools = Math.max(tools, tc); }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Time estimate: ~0.5s per tool call + 2s per 1k tokens
|
|
46
|
-
const timeSeconds = (tokens / 1000) * 2 + tools * 0.5 + 2;
|
|
47
|
-
return { tokens, tools, timeSeconds };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/* ═══════════════════════════════════════
|
|
51
|
-
Task plan cost summary
|
|
52
|
-
═══════════════════════════════════════ */
|
|
53
|
-
export interface PlanEstimate {
|
|
54
|
-
totalTokens: number;
|
|
55
|
-
totalTools: number;
|
|
56
|
-
totalTimeSeconds: number;
|
|
57
|
-
perTask: Array<{ id: string; tokens: number; tools: number; time: number }>;
|
|
58
|
-
warnings: string[];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function estimateTaskPlan(tasks: Task[]): PlanEstimate {
|
|
62
|
-
const perTask: PlanEstimate["perTask"] = [];
|
|
63
|
-
let totalTokens = 500; // system prompt overhead
|
|
64
|
-
let totalTools = 0;
|
|
65
|
-
let totalTime = 5; // init overhead
|
|
66
|
-
const warnings: string[] = [];
|
|
67
|
-
|
|
68
|
-
for (const t of tasks) {
|
|
69
|
-
const est = estimateTaskCost(t.description);
|
|
70
|
-
perTask.push({ id: t.id, tokens: est.tokens, tools: est.tools, time: est.timeSeconds });
|
|
71
|
-
totalTokens += est.tokens;
|
|
72
|
-
totalTools += est.tools;
|
|
73
|
-
totalTime += est.timeSeconds;
|
|
74
|
-
|
|
75
|
-
if (est.timeSeconds > 60) warnings.push(`Task ${t.id} may take >${Math.round(est.timeSeconds)}s`);
|
|
76
|
-
if (est.tools > 10) warnings.push(`Task ${t.id} uses many tool calls (${est.tools})`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (totalTokens > 64000) warnings.push(`Total token estimate (${totalTokens}) exceeds typical context window`);
|
|
80
|
-
if (totalTime > 120) warnings.push(`Estimated total time (${Math.round(totalTime)}s) is significant`);
|
|
81
|
-
if (tasks.length > 6) warnings.push(`Large number of sub-tasks (${tasks.length}) — consider merging simpler ones`);
|
|
82
|
-
|
|
83
|
-
return { totalTokens, totalTools, totalTimeSeconds: Math.round(totalTime), perTask, warnings };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/* ═══════════════════════════════════════
|
|
87
|
-
Format estimate for display
|
|
88
|
-
═══════════════════════════════════════ */
|
|
89
|
-
export function formatPlanEstimate(est: PlanEstimate): string {
|
|
90
|
-
const lines: string[] = [
|
|
91
|
-
`## Plan Estimate`,
|
|
92
|
-
`| Task | Tokens | Tools | Time |`,
|
|
93
|
-
`|------|--------|-------|------|`,
|
|
94
|
-
...est.perTask.map(t => `| ${t.id} | ${t.tokens} | ${t.tools} | ${t.time.toFixed(0)}s |`),
|
|
95
|
-
`| **Total** | **${est.totalTokens}** | **${est.totalTools}** | **${est.totalTimeSeconds}s** |`,
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
if (est.warnings.length > 0) {
|
|
99
|
-
lines.push("", "### Warnings");
|
|
100
|
-
for (const w of est.warnings) lines.push(`- ⚠ ${w}`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return lines.join("\n");
|
|
104
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 资源估算模块 — Token & time budget estimation for task planning.
|
|
3
|
+
*
|
|
4
|
+
* Helps Snow and other planning agents estimate the cost of
|
|
5
|
+
* proposed sub-tasks before committing to execution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Task } from "./agent";
|
|
9
|
+
|
|
10
|
+
/* ═══════════════════════════════════════
|
|
11
|
+
Token estimation
|
|
12
|
+
═══════════════════════════════════════ */
|
|
13
|
+
const CJK_REGEX = /[一-鿿-ゟ가-㐀-䶿]/g;
|
|
14
|
+
|
|
15
|
+
/** Estimate tokens for a given text (CJK ~2 each, ASCII ~4 chars each). */
|
|
16
|
+
export function estimateTokens(text: string): number {
|
|
17
|
+
const cjk = (text.match(CJK_REGEX) || []).length;
|
|
18
|
+
return cjk * 2 + Math.ceil((text.length - cjk) / 4);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* ═══════════════════════════════════════
|
|
22
|
+
Per-task-type cost estimates
|
|
23
|
+
═══════════════════════════════════════ */
|
|
24
|
+
const TASK_TYPE_PATTERNS: Array<[RegExp, number, number]> = [
|
|
25
|
+
// [pattern, estimated tokens, estimated tools]
|
|
26
|
+
[/read|read_file|grep|search|查|搜索|list/i, 2000, 2],
|
|
27
|
+
[/write|write_file|生成|写|create|implement/i, 4000, 5],
|
|
28
|
+
[/edit|edit_file|改|修改|fix|修复/i, 3000, 3],
|
|
29
|
+
[/delete|delete_file|删|rm/i, 1500, 2],
|
|
30
|
+
[/deploy|部署|publish|发布|release/i, 8000, 8],
|
|
31
|
+
[/review|审查|audit|审计|scan|扫描/i, 5000, 4],
|
|
32
|
+
[/test|测试|run_test|coverage/i, 3000, 3],
|
|
33
|
+
[/research|研究|调研|analyze|分析/i, 6000, 4],
|
|
34
|
+
[/orchestrate|编排|multi-step|多步/i, 12000, 10],
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** Estimate cost for a single task description. */
|
|
38
|
+
export function estimateTaskCost(description: string): { tokens: number; tools: number; timeSeconds: number } {
|
|
39
|
+
let tokens = 2000; // base
|
|
40
|
+
let tools = 2; // base
|
|
41
|
+
for (const [pattern, t, tc] of TASK_TYPE_PATTERNS) {
|
|
42
|
+
if (pattern.test(description)) { tokens = Math.max(tokens, t); tools = Math.max(tools, tc); }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Time estimate: ~0.5s per tool call + 2s per 1k tokens
|
|
46
|
+
const timeSeconds = (tokens / 1000) * 2 + tools * 0.5 + 2;
|
|
47
|
+
return { tokens, tools, timeSeconds };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ═══════════════════════════════════════
|
|
51
|
+
Task plan cost summary
|
|
52
|
+
═══════════════════════════════════════ */
|
|
53
|
+
export interface PlanEstimate {
|
|
54
|
+
totalTokens: number;
|
|
55
|
+
totalTools: number;
|
|
56
|
+
totalTimeSeconds: number;
|
|
57
|
+
perTask: Array<{ id: string; tokens: number; tools: number; time: number }>;
|
|
58
|
+
warnings: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function estimateTaskPlan(tasks: Task[]): PlanEstimate {
|
|
62
|
+
const perTask: PlanEstimate["perTask"] = [];
|
|
63
|
+
let totalTokens = 500; // system prompt overhead
|
|
64
|
+
let totalTools = 0;
|
|
65
|
+
let totalTime = 5; // init overhead
|
|
66
|
+
const warnings: string[] = [];
|
|
67
|
+
|
|
68
|
+
for (const t of tasks) {
|
|
69
|
+
const est = estimateTaskCost(t.description);
|
|
70
|
+
perTask.push({ id: t.id, tokens: est.tokens, tools: est.tools, time: est.timeSeconds });
|
|
71
|
+
totalTokens += est.tokens;
|
|
72
|
+
totalTools += est.tools;
|
|
73
|
+
totalTime += est.timeSeconds;
|
|
74
|
+
|
|
75
|
+
if (est.timeSeconds > 60) warnings.push(`Task ${t.id} may take >${Math.round(est.timeSeconds)}s`);
|
|
76
|
+
if (est.tools > 10) warnings.push(`Task ${t.id} uses many tool calls (${est.tools})`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (totalTokens > 64000) warnings.push(`Total token estimate (${totalTokens}) exceeds typical context window`);
|
|
80
|
+
if (totalTime > 120) warnings.push(`Estimated total time (${Math.round(totalTime)}s) is significant`);
|
|
81
|
+
if (tasks.length > 6) warnings.push(`Large number of sub-tasks (${tasks.length}) — consider merging simpler ones`);
|
|
82
|
+
|
|
83
|
+
return { totalTokens, totalTools, totalTimeSeconds: Math.round(totalTime), perTask, warnings };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ═══════════════════════════════════════
|
|
87
|
+
Format estimate for display
|
|
88
|
+
═══════════════════════════════════════ */
|
|
89
|
+
export function formatPlanEstimate(est: PlanEstimate): string {
|
|
90
|
+
const lines: string[] = [
|
|
91
|
+
`## Plan Estimate`,
|
|
92
|
+
`| Task | Tokens | Tools | Time |`,
|
|
93
|
+
`|------|--------|-------|------|`,
|
|
94
|
+
...est.perTask.map(t => `| ${t.id} | ${t.tokens} | ${t.tools} | ${t.time.toFixed(0)}s |`),
|
|
95
|
+
`| **Total** | **${est.totalTokens}** | **${est.totalTools}** | **${est.totalTimeSeconds}s** |`,
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
if (est.warnings.length > 0) {
|
|
99
|
+
lines.push("", "### Warnings");
|
|
100
|
+
for (const w of est.warnings) lines.push(`- ⚠ ${w}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|