skyloom 1.4.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.
- package/.github/workflows/ci.yml +36 -0
- package/CONVERSION_PLAN.md +191 -0
- package/README.md +67 -0
- package/dist/agents/dew.d.ts +15 -0
- package/dist/agents/dew.d.ts.map +1 -0
- package/dist/agents/dew.js +74 -0
- package/dist/agents/dew.js.map +1 -0
- package/dist/agents/fair.d.ts +15 -0
- package/dist/agents/fair.d.ts.map +1 -0
- package/dist/agents/fair.js +106 -0
- package/dist/agents/fair.js.map +1 -0
- package/dist/agents/fog.d.ts +15 -0
- package/dist/agents/fog.d.ts.map +1 -0
- package/dist/agents/fog.js +52 -0
- package/dist/agents/fog.js.map +1 -0
- package/dist/agents/frost.d.ts +15 -0
- package/dist/agents/frost.d.ts.map +1 -0
- package/dist/agents/frost.js +54 -0
- package/dist/agents/frost.js.map +1 -0
- package/dist/agents/rain.d.ts +15 -0
- package/dist/agents/rain.d.ts.map +1 -0
- package/dist/agents/rain.js +54 -0
- package/dist/agents/rain.js.map +1 -0
- package/dist/agents/snow.d.ts +27 -0
- package/dist/agents/snow.d.ts.map +1 -0
- package/dist/agents/snow.js +226 -0
- package/dist/agents/snow.js.map +1 -0
- package/dist/cli/main.d.ts +7 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +402 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/mode.d.ts +17 -0
- package/dist/cli/mode.d.ts.map +1 -0
- package/dist/cli/mode.js +56 -0
- package/dist/cli/mode.js.map +1 -0
- package/dist/core/agent.d.ts +174 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +1332 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/agent_helpers.d.ts +51 -0
- package/dist/core/agent_helpers.d.ts.map +1 -0
- package/dist/core/agent_helpers.js +477 -0
- package/dist/core/agent_helpers.js.map +1 -0
- package/dist/core/bus.d.ts +99 -0
- package/dist/core/bus.d.ts.map +1 -0
- package/dist/core/bus.js +191 -0
- package/dist/core/bus.js.map +1 -0
- package/dist/core/cache.d.ts +63 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +121 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/checkpoint.d.ts +19 -0
- package/dist/core/checkpoint.d.ts.map +1 -0
- package/dist/core/checkpoint.js +120 -0
- package/dist/core/checkpoint.js.map +1 -0
- package/dist/core/circuit_breaker.d.ts +46 -0
- package/dist/core/circuit_breaker.d.ts.map +1 -0
- package/dist/core/circuit_breaker.js +99 -0
- package/dist/core/circuit_breaker.js.map +1 -0
- package/dist/core/config.d.ts +97 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +281 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/constants.d.ts +78 -0
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/constants.js +84 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/factory.d.ts +63 -0
- package/dist/core/factory.d.ts.map +1 -0
- package/dist/core/factory.js +537 -0
- package/dist/core/factory.js.map +1 -0
- package/dist/core/icons.d.ts +28 -0
- package/dist/core/icons.d.ts.map +1 -0
- package/dist/core/icons.js +86 -0
- package/dist/core/icons.js.map +1 -0
- package/dist/core/index.d.ts +29 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +54 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/llm.d.ts +121 -0
- package/dist/core/llm.d.ts.map +1 -0
- package/dist/core/llm.js +532 -0
- package/dist/core/llm.js.map +1 -0
- package/dist/core/logger.d.ts +57 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +122 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/mcp.d.ts +190 -0
- package/dist/core/mcp.d.ts.map +1 -0
- package/dist/core/mcp.js +822 -0
- package/dist/core/mcp.js.map +1 -0
- package/dist/core/mcp_server.d.ts +26 -0
- package/dist/core/mcp_server.d.ts.map +1 -0
- package/dist/core/mcp_server.js +211 -0
- package/dist/core/mcp_server.js.map +1 -0
- package/dist/core/memory.d.ts +190 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +988 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/middleware.d.ts +114 -0
- package/dist/core/middleware.d.ts.map +1 -0
- package/dist/core/middleware.js +248 -0
- package/dist/core/middleware.js.map +1 -0
- package/dist/core/pipelines.d.ts +87 -0
- package/dist/core/pipelines.d.ts.map +1 -0
- package/dist/core/pipelines.js +301 -0
- package/dist/core/pipelines.js.map +1 -0
- package/dist/core/profile.d.ts +23 -0
- package/dist/core/profile.d.ts.map +1 -0
- package/dist/core/profile.js +289 -0
- package/dist/core/profile.js.map +1 -0
- package/dist/core/router.d.ts +24 -0
- package/dist/core/router.d.ts.map +1 -0
- package/dist/core/router.js +111 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/schemas.d.ts +82 -0
- package/dist/core/schemas.d.ts.map +1 -0
- package/dist/core/schemas.js +200 -0
- package/dist/core/schemas.js.map +1 -0
- package/dist/core/semantic.d.ts +92 -0
- package/dist/core/semantic.d.ts.map +1 -0
- package/dist/core/semantic.js +175 -0
- package/dist/core/semantic.js.map +1 -0
- package/dist/core/skill.d.ts +68 -0
- package/dist/core/skill.d.ts.map +1 -0
- package/dist/core/skill.js +350 -0
- package/dist/core/skill.js.map +1 -0
- package/dist/core/tool.d.ts +99 -0
- package/dist/core/tool.d.ts.map +1 -0
- package/dist/core/tool.js +341 -0
- package/dist/core/tool.js.map +1 -0
- package/dist/core/tool_router.d.ts +29 -0
- package/dist/core/tool_router.d.ts.map +1 -0
- package/dist/core/tool_router.js +172 -0
- package/dist/core/tool_router.js.map +1 -0
- package/dist/core/workspace.d.ts +48 -0
- package/dist/core/workspace.d.ts.map +1 -0
- package/dist/core/workspace.js +179 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/plugins/loader.d.ts +17 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +96 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/skills/loader.d.ts +9 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +78 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/tools/builtin.d.ts +10 -0
- package/dist/tools/builtin.d.ts.map +1 -0
- package/dist/tools/builtin.js +414 -0
- package/dist/tools/builtin.js.map +1 -0
- package/dist/tools/computer.d.ts +12 -0
- package/dist/tools/computer.d.ts.map +1 -0
- package/dist/tools/computer.js +326 -0
- package/dist/tools/computer.js.map +1 -0
- package/dist/tools/delegate.d.ts +10 -0
- package/dist/tools/delegate.d.ts.map +1 -0
- package/dist/tools/delegate.js +45 -0
- package/dist/tools/delegate.js.map +1 -0
- package/dist/web/server.d.ts +5 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +647 -0
- package/dist/web/server.js.map +1 -0
- package/dist/web/tts.d.ts +33 -0
- package/dist/web/tts.d.ts.map +1 -0
- package/dist/web/tts.js +69 -0
- package/dist/web/tts.js.map +1 -0
- package/package.json +60 -0
- package/scripts/install.js +48 -0
- package/scripts/link.js +10 -0
- package/setup.bat +79 -0
- package/skill-test-ty2fOA/test.md +10 -0
- package/src/agents/dew.ts +70 -0
- package/src/agents/fair.ts +102 -0
- package/src/agents/fog.ts +48 -0
- package/src/agents/frost.ts +50 -0
- package/src/agents/rain.ts +50 -0
- package/src/agents/snow.ts +239 -0
- package/src/cli/main.ts +405 -0
- package/src/cli/mode.ts +58 -0
- package/src/core/agent.ts +1506 -0
- package/src/core/agent_helpers.ts +461 -0
- package/src/core/bus.ts +221 -0
- package/src/core/cache.ts +153 -0
- package/src/core/checkpoint.ts +94 -0
- package/src/core/circuit_breaker.ts +119 -0
- package/src/core/config.ts +341 -0
- package/src/core/constants.ts +95 -0
- package/src/core/factory.ts +627 -0
- package/src/core/icons.ts +53 -0
- package/src/core/index.ts +31 -0
- package/src/core/llm.ts +724 -0
- package/src/core/logger.ts +144 -0
- package/src/core/mcp.ts +953 -0
- package/src/core/mcp_server.ts +176 -0
- package/src/core/memory.ts +1169 -0
- package/src/core/middleware.ts +350 -0
- package/src/core/pipelines.ts +424 -0
- package/src/core/profile.ts +255 -0
- package/src/core/router.ts +124 -0
- package/src/core/schemas.ts +282 -0
- package/src/core/semantic.ts +211 -0
- package/src/core/skill.ts +342 -0
- package/src/core/tool.ts +427 -0
- package/src/core/tool_router.ts +193 -0
- package/src/core/workspace.ts +150 -0
- package/src/plugins/loader.ts +66 -0
- package/src/skills/loader.ts +46 -0
- package/src/sql.js.d.ts +29 -0
- package/src/tools/builtin.ts +382 -0
- package/src/tools/computer.ts +269 -0
- package/src/tools/delegate.ts +49 -0
- package/src/web/server.ts +634 -0
- package/src/web/tts.ts +93 -0
- package/tests/bus.test.ts +121 -0
- package/tests/icons.test.ts +45 -0
- package/tests/router.test.ts +86 -0
- package/tests/schemas.test.ts +51 -0
- package/tests/semantic.test.ts +83 -0
- package/tests/setup.ts +10 -0
- package/tests/skill.test.ts +172 -0
- package/tests/tool.test.ts +108 -0
- package/tests/tool_router.test.ts +71 -0
- package/tsconfig.json +37 -0
- package/vitest.config.ts +17 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level helpers for core/agent — parsing, signatures, similarity, labels.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions / constants only: no state, no agent reference, safe to import anywhere.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
import type { Message } from './memory';
|
|
9
|
+
import type { ToolRegistry } from './tool';
|
|
10
|
+
|
|
11
|
+
// ── Tool labels ──
|
|
12
|
+
|
|
13
|
+
const TOOL_LABELS: Record<string, string> = {
|
|
14
|
+
read_file: 'Reading {path}',
|
|
15
|
+
write_file: 'Writing {path}',
|
|
16
|
+
edit_file: 'Editing {path}',
|
|
17
|
+
list_directory: 'Listing {path}',
|
|
18
|
+
file_search: 'Searching {directory}/{pattern}',
|
|
19
|
+
code_search: "Searching for '{query}'",
|
|
20
|
+
grep: "Grepping '{pattern}'",
|
|
21
|
+
shell_exec: 'Running: {command}',
|
|
22
|
+
http_get: 'GET {url}',
|
|
23
|
+
http_post: 'POST {url}',
|
|
24
|
+
web_search: 'Searching: {query}',
|
|
25
|
+
move_file: 'Moving {src}',
|
|
26
|
+
copy_file: 'Copying {src}',
|
|
27
|
+
delete_file: 'Deleting {path}',
|
|
28
|
+
get_cwd: 'Getting working directory',
|
|
29
|
+
tree: 'Tree {directory}',
|
|
30
|
+
lint_file: 'Linting {path}',
|
|
31
|
+
scan_deps: 'Scanning {directory}',
|
|
32
|
+
fetch_page: 'Fetching {url}',
|
|
33
|
+
delegate_to: 'Delegating to {agent}: {task}',
|
|
34
|
+
use_skill: 'Activating {name}',
|
|
35
|
+
list_skills: 'Listing available skills',
|
|
36
|
+
git_status: 'Git status',
|
|
37
|
+
git_diff: 'Git diff',
|
|
38
|
+
git_log: 'Git log',
|
|
39
|
+
git_add: 'Git add {files}',
|
|
40
|
+
git_commit: 'Git commit',
|
|
41
|
+
git_checkout: 'Git checkout {branch}',
|
|
42
|
+
launch_app: 'Launching {name}',
|
|
43
|
+
open_path: 'Opening {target}',
|
|
44
|
+
browser_open: 'Opening {url} in browser',
|
|
45
|
+
list_installed_apps: 'Listing installed apps',
|
|
46
|
+
system_info: 'System info',
|
|
47
|
+
system_diagnose: 'System diagnosis',
|
|
48
|
+
list_processes: 'Listing processes',
|
|
49
|
+
kill_process: 'Killing {target}',
|
|
50
|
+
package_manager: 'Package {action} {name}',
|
|
51
|
+
service_control: 'Service {action} {name}',
|
|
52
|
+
mcp_list_servers: 'Listing MCP servers',
|
|
53
|
+
mcp_add_server: 'Adding MCP server {name}',
|
|
54
|
+
mcp_remove_server: 'Removing MCP server {name}',
|
|
55
|
+
mcp_scaffold_server: 'Scaffolding MCP server {name}',
|
|
56
|
+
remember: 'Remembering: {note}',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ── Regex patterns ──
|
|
60
|
+
|
|
61
|
+
const RE_OBJ_OR_ARRAY = /(\{.*\}|\[.*\])/s;
|
|
62
|
+
const RE_KV_DETECT = /\b\w[\w\d_]*\s*=/;
|
|
63
|
+
const RE_KV_PAIRS = /(\w[\w\d_]*)\s*=\s*("[^"]*"|'[^']*'|[\w\d_.+-]+)/g;
|
|
64
|
+
const RE_NONE_LITERAL = /:\s*None\s*([,}])/g;
|
|
65
|
+
const RE_TRUE_LITERAL = /:\s*True\s*([,}])/g;
|
|
66
|
+
const RE_FALSE_LITERAL = /:\s*False\s*([,}])/g;
|
|
67
|
+
const RE_PY_NONE = /\bNone\b/g;
|
|
68
|
+
const RE_PY_TRUE = /\bTrue\b/g;
|
|
69
|
+
const RE_PY_FALSE = /\bFalse\b/g;
|
|
70
|
+
const RE_UNQUOTED_KEY = /([{,]\s*)(\w[\w\d_]*)(\s*:)/g;
|
|
71
|
+
const RE_TRAILING_COMMA = /,\s*([}\]])/g;
|
|
72
|
+
const RE_UNQUOTED_STRING = /(:\s*)([a-zA-Z_.][a-zA-Z0-9_ ./\\@.\-+#~$]*?)(\s*[,}\]])/g;
|
|
73
|
+
|
|
74
|
+
// ── Tool-signature loop detector tuning ──
|
|
75
|
+
export const SIG_WINDOW = 8;
|
|
76
|
+
export const SIG_LOOP_HINT = 4;
|
|
77
|
+
export const SIG_LOOP_HARDSTOP = 6;
|
|
78
|
+
|
|
79
|
+
// ── Tool-failure markers ──
|
|
80
|
+
const TOOL_FAILURE_MARKERS = [
|
|
81
|
+
'no results found',
|
|
82
|
+
'no matches for',
|
|
83
|
+
'file not found',
|
|
84
|
+
'directory not found',
|
|
85
|
+
'permission denied',
|
|
86
|
+
'status: 4',
|
|
87
|
+
'status: 5',
|
|
88
|
+
'request timed out',
|
|
89
|
+
'timed out',
|
|
90
|
+
'connection refused',
|
|
91
|
+
'name or service not known',
|
|
92
|
+
'ssl',
|
|
93
|
+
'[error',
|
|
94
|
+
'error: tool',
|
|
95
|
+
'error: file',
|
|
96
|
+
'error: directory',
|
|
97
|
+
'circuitbreakeropen',
|
|
98
|
+
'execution failed:',
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
// ── File-producing tools ──
|
|
102
|
+
const FILE_PRODUCING_TOOLS: Record<string, string[]> = {
|
|
103
|
+
write_file: ['path'],
|
|
104
|
+
edit_file: ['path'],
|
|
105
|
+
copy_file: ['dst', 'destination'],
|
|
106
|
+
move_file: ['dst', 'destination'],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// ── Functions ──
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Parse tool call JSON with multi-stage repair for LLM output quirks.
|
|
113
|
+
*/
|
|
114
|
+
export function parseToolArgs(raw: string): Record<string, any> | null {
|
|
115
|
+
if (!raw || !raw.trim()) return null;
|
|
116
|
+
|
|
117
|
+
let cleaned = raw.trim();
|
|
118
|
+
|
|
119
|
+
// 1. Direct parse
|
|
120
|
+
try { return JSON.parse(cleaned); } catch { /* continue */ }
|
|
121
|
+
|
|
122
|
+
// 2. Strip markdown code fences
|
|
123
|
+
if (cleaned.startsWith('```')) {
|
|
124
|
+
const nl = cleaned.indexOf('\n');
|
|
125
|
+
if (nl >= 0) cleaned = cleaned.slice(nl + 1);
|
|
126
|
+
const end = cleaned.lastIndexOf('```');
|
|
127
|
+
if (end >= 0) cleaned = cleaned.slice(0, end);
|
|
128
|
+
cleaned = cleaned.trim();
|
|
129
|
+
try { return JSON.parse(cleaned); } catch { /* continue */ }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 3. Extract first JSON object/array from surrounding text
|
|
133
|
+
const objMatch = RE_OBJ_OR_ARRAY.exec(cleaned);
|
|
134
|
+
if (objMatch) {
|
|
135
|
+
cleaned = objMatch[1];
|
|
136
|
+
try { return JSON.parse(cleaned); } catch { /* continue */ }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 4. Key=value format: query="weather", count=5 -> {"query": "weather", "count": 5}
|
|
140
|
+
if (!cleaned.startsWith('{') && RE_KV_DETECT.test(cleaned)) {
|
|
141
|
+
const kvPairs: string[] = [];
|
|
142
|
+
let m: RegExpExecArray | null;
|
|
143
|
+
while ((m = RE_KV_PAIRS.exec(cleaned)) !== null) {
|
|
144
|
+
const key = m[1];
|
|
145
|
+
let val = m[2];
|
|
146
|
+
if (val.startsWith("'") && val.endsWith("'")) {
|
|
147
|
+
val = '"' + val.slice(1, -1) + '"';
|
|
148
|
+
}
|
|
149
|
+
kvPairs.push(`"${key}": ${val}`);
|
|
150
|
+
}
|
|
151
|
+
if (kvPairs.length > 0) {
|
|
152
|
+
let jsonStr = '{' + kvPairs.join(', ') + '}';
|
|
153
|
+
jsonStr = jsonStr.replace(RE_NONE_LITERAL, ': null$1');
|
|
154
|
+
jsonStr = jsonStr.replace(RE_TRUE_LITERAL, ': true$1');
|
|
155
|
+
jsonStr = jsonStr.replace(RE_FALSE_LITERAL, ': false$1');
|
|
156
|
+
try { return JSON.parse(jsonStr); } catch { /* continue */ }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 5. Python -> JSON literals
|
|
161
|
+
cleaned = cleaned.replace(RE_PY_NONE, 'null');
|
|
162
|
+
cleaned = cleaned.replace(RE_PY_TRUE, 'true');
|
|
163
|
+
cleaned = cleaned.replace(RE_PY_FALSE, 'false');
|
|
164
|
+
|
|
165
|
+
// 6. Backtick -> double quote
|
|
166
|
+
cleaned = cleaned.replace(/`/g, '"');
|
|
167
|
+
|
|
168
|
+
// 7. Fix single-quote strings
|
|
169
|
+
if (cleaned.includes("'")) {
|
|
170
|
+
cleaned = cleaned.replace(/'/g, '"');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 8. Fix unquoted keys
|
|
174
|
+
cleaned = cleaned.replace(RE_UNQUOTED_KEY, '$1"$2"$3');
|
|
175
|
+
|
|
176
|
+
// 9. Fix trailing commas
|
|
177
|
+
cleaned = cleaned.replace(RE_TRAILING_COMMA, '$1');
|
|
178
|
+
cleaned = cleaned.replace(/,\s*$/, '').trim();
|
|
179
|
+
|
|
180
|
+
// 10. Fix unquoted string values
|
|
181
|
+
cleaned = cleaned.replace(RE_UNQUOTED_STRING, (match, prefix: string, word: string, suffix: string) => {
|
|
182
|
+
if (word === 'null' || word === 'true' || word === 'false') return match;
|
|
183
|
+
if (/^-?\d+(\.\d+)?$/.test(word)) return match;
|
|
184
|
+
if (word.startsWith('"') || word.startsWith('{') || word.startsWith('[')) return match;
|
|
185
|
+
return `${prefix}"${word}"${suffix}`;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// 11. Attempt parse
|
|
189
|
+
try { return JSON.parse(cleaned); } catch { /* continue */ }
|
|
190
|
+
|
|
191
|
+
// 12. Balanced-brace extraction
|
|
192
|
+
let depth = 0;
|
|
193
|
+
let start = -1;
|
|
194
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
195
|
+
const ch = cleaned[i];
|
|
196
|
+
if (ch === '{') {
|
|
197
|
+
if (depth === 0) start = i;
|
|
198
|
+
depth++;
|
|
199
|
+
} else if (ch === '}') {
|
|
200
|
+
depth--;
|
|
201
|
+
if (depth === 0 && start >= 0) {
|
|
202
|
+
try { return JSON.parse(cleaned.slice(start, i + 1)); } catch { /* continue */ }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Auto-close unclosed braces
|
|
208
|
+
if (start >= 0 && depth > 0) {
|
|
209
|
+
const candidate = cleaned.slice(start) + '}'.repeat(depth);
|
|
210
|
+
try { return JSON.parse(candidate); } catch { /* ignore */ }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Heuristic: does this tool result indicate a dead-end the LLM should stop retrying?
|
|
218
|
+
*/
|
|
219
|
+
export function looksLikeFailedToolResult(result: string): boolean {
|
|
220
|
+
if (!result) return true;
|
|
221
|
+
const head = result.slice(0, 300).toLowerCase();
|
|
222
|
+
return TOOL_FAILURE_MARKERS.some(m => head.includes(m));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Walk assistant turns and pull out file paths that write_file / edit_file / etc. touched.
|
|
227
|
+
*/
|
|
228
|
+
export function extractFilePathsFromMessages(messages: Message[]): string[] {
|
|
229
|
+
const toolResults: Record<string, string> = {};
|
|
230
|
+
for (const m of messages) {
|
|
231
|
+
if (m.role === 'tool' && m.toolCallId) {
|
|
232
|
+
toolResults[m.toolCallId] = m.content || '';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const paths: string[] = [];
|
|
237
|
+
const seen = new Set<string>();
|
|
238
|
+
|
|
239
|
+
for (const m of messages) {
|
|
240
|
+
if (m.role !== 'assistant' || !m.toolCalls) continue;
|
|
241
|
+
for (const tc of m.toolCalls) {
|
|
242
|
+
const name = tc.function?.name || '';
|
|
243
|
+
const argKeys = FILE_PRODUCING_TOOLS[name];
|
|
244
|
+
if (!argKeys) continue;
|
|
245
|
+
|
|
246
|
+
const raw = tc.function?.arguments || '';
|
|
247
|
+
let args: Record<string, any>;
|
|
248
|
+
try {
|
|
249
|
+
args = typeof raw === 'string' ? JSON.parse(raw) : (raw || {});
|
|
250
|
+
} catch {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (typeof args !== 'object') continue;
|
|
254
|
+
|
|
255
|
+
let filePath: string | undefined;
|
|
256
|
+
for (const k of argKeys) {
|
|
257
|
+
const v = args[k];
|
|
258
|
+
if (typeof v === 'string' && v.trim()) {
|
|
259
|
+
filePath = v.trim();
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (!filePath) continue;
|
|
264
|
+
|
|
265
|
+
const result = toolResults[tc.id || ''] || '';
|
|
266
|
+
if (result && (result.startsWith('Error:') || result.startsWith('[Error'))) continue;
|
|
267
|
+
if (!seen.has(filePath)) {
|
|
268
|
+
seen.add(filePath);
|
|
269
|
+
paths.push(filePath);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return paths;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Append a deterministic artifact footer when the agent wrote files during this task.
|
|
278
|
+
*/
|
|
279
|
+
export function enrichResponseWithArtifacts(content: string, filePaths: string[]): string {
|
|
280
|
+
if (!filePaths.length) return content;
|
|
281
|
+
const body = content || '';
|
|
282
|
+
const missing = filePaths.filter(p => !body.includes(p));
|
|
283
|
+
if (!missing.length) return body;
|
|
284
|
+
|
|
285
|
+
const lines = ['', '> **Artifacts produced**'];
|
|
286
|
+
for (const p of filePaths) {
|
|
287
|
+
const marker = body.includes(p) ? ' — already cited above' : '';
|
|
288
|
+
lines.push(`> - \`${p}\`${marker}`);
|
|
289
|
+
}
|
|
290
|
+
return body.trimEnd() + '\n\n' + lines.join('\n');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Compact, stable fingerprint of a tool call for loop detection.
|
|
295
|
+
*/
|
|
296
|
+
export function toolCallSignature(toolName: string, args: Record<string, any> | null): string {
|
|
297
|
+
const a = args || {};
|
|
298
|
+
|
|
299
|
+
if (toolName === 'edit_file') {
|
|
300
|
+
const osVal = (a.old_text as string) || (a.oldText as string) || '';
|
|
301
|
+
if (osVal) {
|
|
302
|
+
const hash = crypto.createHash('sha1').update(osVal).digest('hex').slice(0, 8);
|
|
303
|
+
return `edit_file:${a.path || ''}#${hash}`;
|
|
304
|
+
}
|
|
305
|
+
return `edit_file:${a.path || ''}`;
|
|
306
|
+
}
|
|
307
|
+
if (['write_file', 'read_file', 'delete_file'].includes(toolName)) {
|
|
308
|
+
return `${toolName}:${a.path || ''}`;
|
|
309
|
+
}
|
|
310
|
+
if (['copy_file', 'move_file'].includes(toolName)) {
|
|
311
|
+
return `${toolName}:${a.src || a.source || ''}->${a.dst || a.destination || ''}`;
|
|
312
|
+
}
|
|
313
|
+
if (['run_bash', 'bash', 'shell', 'run_shell'].includes(toolName)) {
|
|
314
|
+
const cmd = (a.command || a.cmd || '') as string;
|
|
315
|
+
if (!cmd) return toolName;
|
|
316
|
+
const first = cmd.split(/\s+/)[0] || '';
|
|
317
|
+
return `${toolName}:${first.slice(0, 30)}`;
|
|
318
|
+
}
|
|
319
|
+
if (['web_search', 'search', 'search_web', 'search_files', 'grep'].includes(toolName)) {
|
|
320
|
+
let q = ((a.query || a.pattern || '') as string).slice(0, 40);
|
|
321
|
+
q = q.replace(/[\s"'「」『』""''‘’“”]/g, '');
|
|
322
|
+
return `${toolName}:${q.toLowerCase().slice(0, 30)}`;
|
|
323
|
+
}
|
|
324
|
+
if (['fetch_page', 'fetch_web_page', 'http_get', 'http_post'].includes(toolName)) {
|
|
325
|
+
return `${toolName}:${(a.url || '').slice(0, 60)}`;
|
|
326
|
+
}
|
|
327
|
+
if (toolName === 'delegate_to') {
|
|
328
|
+
return `delegate_to:${a.agent || ''}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Generic fallback
|
|
332
|
+
if (Object.keys(a).length > 0) {
|
|
333
|
+
const blob = JSON.stringify(a, Object.keys(a).sort());
|
|
334
|
+
const hash = crypto.createHash('sha1').update(blob.slice(0, 300)).digest('hex').slice(0, 8);
|
|
335
|
+
return `${toolName}:${hash}`;
|
|
336
|
+
}
|
|
337
|
+
return toolName;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Cheap similarity for narration-loop detection.
|
|
342
|
+
*/
|
|
343
|
+
export function textSimilarity(a: string, b: string): number {
|
|
344
|
+
if (a.length < 12 || b.length < 12) return 0.0;
|
|
345
|
+
const longer = a.length >= b.length ? a : b;
|
|
346
|
+
const shorter = a.length < b.length ? a : b;
|
|
347
|
+
|
|
348
|
+
if (longer.length === 0) return 0.0;
|
|
349
|
+
|
|
350
|
+
// Use simple character overlap as a cheap similarity measure
|
|
351
|
+
const common = [...shorter].filter(ch => longer.includes(ch)).length;
|
|
352
|
+
return common / longer.length;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Produce a tool-result error string that helps the LLM recover.
|
|
357
|
+
*/
|
|
358
|
+
export function formatArgsParseError(toolName: string, rawArgs: string): string {
|
|
359
|
+
const stripped = rawArgs.trimEnd();
|
|
360
|
+
const hasClosingBrace = stripped.endsWith('}') || stripped.endsWith(']');
|
|
361
|
+
|
|
362
|
+
// Crude quote counter
|
|
363
|
+
let inString = false;
|
|
364
|
+
let i = 0;
|
|
365
|
+
while (i < stripped.length) {
|
|
366
|
+
const ch = stripped[i];
|
|
367
|
+
if (ch === '\\' && inString) { i += 2; continue; }
|
|
368
|
+
if (ch === '"') inString = !inString;
|
|
369
|
+
i++;
|
|
370
|
+
}
|
|
371
|
+
const looksTruncated = inString || !hasClosingBrace;
|
|
372
|
+
const preview = rawArgs.slice(0, 200) + (rawArgs.length > 200 ? '...[truncated]' : '');
|
|
373
|
+
|
|
374
|
+
if (looksTruncated) {
|
|
375
|
+
return (
|
|
376
|
+
`Error: tool '${toolName}' arguments were truncated by the model's ` +
|
|
377
|
+
`output budget (max_tokens). The JSON ended mid-value so it cannot ` +
|
|
378
|
+
`be parsed. For large content, split into multiple smaller calls. ` +
|
|
379
|
+
`Args preview: ${preview}`
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return `Error: invalid JSON in tool call arguments for '${toolName}': ${preview}`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Return the closest existing tool names for a hallucinated name.
|
|
387
|
+
*/
|
|
388
|
+
export function suggestToolNames(missing: string, registry: ToolRegistry, maxN: number = 3): string[] {
|
|
389
|
+
const allNames = registry.listNames();
|
|
390
|
+
if (!allNames.length) return [];
|
|
391
|
+
|
|
392
|
+
const missingLower = missing.toLowerCase();
|
|
393
|
+
const missingChunks = missingLower.split('_').filter(c => c.length >= 3);
|
|
394
|
+
|
|
395
|
+
// Score by name overlap
|
|
396
|
+
const scored: Array<{ name: string; score: number }> = [];
|
|
397
|
+
const descScored: Array<{ name: string; score: number }> = [];
|
|
398
|
+
|
|
399
|
+
for (const n of allNames) {
|
|
400
|
+
const nlow = n.toLowerCase();
|
|
401
|
+
let nameScore = 0;
|
|
402
|
+
for (const chunk of missingChunks) {
|
|
403
|
+
if (nlow.includes(chunk)) nameScore += 2;
|
|
404
|
+
}
|
|
405
|
+
for (const chunk of nlow.split('_')) {
|
|
406
|
+
if (chunk.length >= 3 && missingLower.includes(chunk)) nameScore += 1;
|
|
407
|
+
}
|
|
408
|
+
if (nameScore > 0) {
|
|
409
|
+
scored.push({ name: n, score: nameScore });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const tool = registry.get(n);
|
|
413
|
+
if (!tool) continue;
|
|
414
|
+
const desc = tool.description.toLowerCase();
|
|
415
|
+
const descScore = missingChunks.filter(ch => desc.includes(ch)).length;
|
|
416
|
+
if (descScore > 0) {
|
|
417
|
+
descScored.push({ name: n, score: descScore });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (scored.length > 0) {
|
|
422
|
+
scored.sort((a, b) => b.score - a.score);
|
|
423
|
+
return scored.slice(0, maxN).map(s => s.name);
|
|
424
|
+
}
|
|
425
|
+
descScored.sort((a, b) => b.score - a.score);
|
|
426
|
+
return descScored.slice(0, maxN).map(s => s.name);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Build a human-readable one-liner for a tool call.
|
|
431
|
+
*/
|
|
432
|
+
export function toolStatusLabel(name: string, args: Record<string, any>): string {
|
|
433
|
+
const template = TOOL_LABELS[name];
|
|
434
|
+
let label: string;
|
|
435
|
+
if (template) {
|
|
436
|
+
try {
|
|
437
|
+
label = template.replace(/\{(\w+)\}/g, (_m, key) => String(args[key] ?? ''));
|
|
438
|
+
} catch {
|
|
439
|
+
label = `${name}...`;
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
label = `${name}...`;
|
|
443
|
+
}
|
|
444
|
+
if (label.length > 100) {
|
|
445
|
+
label = label.slice(0, 97) + '...';
|
|
446
|
+
}
|
|
447
|
+
return label;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Build a short fallback line shown when a turn ends with delegate_to calls but no plain text.
|
|
452
|
+
*/
|
|
453
|
+
export function synthesizeDelegationSummary(delegations: Array<[string, boolean]>): string {
|
|
454
|
+
if (!delegations.length) return '';
|
|
455
|
+
const ok = delegations.filter(([_, s]) => s).map(([n]) => n);
|
|
456
|
+
const failed = delegations.filter(([_, s]) => !s).map(([n]) => n);
|
|
457
|
+
const parts: string[] = [];
|
|
458
|
+
if (ok.length) parts.push('Delegated: ' + ok.join(', '));
|
|
459
|
+
if (failed.length) parts.push('Failed: ' + failed.join(', '));
|
|
460
|
+
return '[' + parts.join(' | ') + ']';
|
|
461
|
+
}
|
package/src/core/bus.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async event-driven message bus for inter-agent communication.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Event types for the message bus.
|
|
9
|
+
*/
|
|
10
|
+
export enum EventType {
|
|
11
|
+
TASK_ASSIGNED = 'task_assigned',
|
|
12
|
+
TASK_COMPLETED = 'task_completed',
|
|
13
|
+
TASK_FEEDBACK = 'task_feedback',
|
|
14
|
+
AGENT_REQUEST = 'agent_request',
|
|
15
|
+
AGENT_RESPONSE = 'agent_response',
|
|
16
|
+
SYSTEM_EVENT = 'system_event',
|
|
17
|
+
STATE_CHANGE = 'state_change', // agent state changes
|
|
18
|
+
LLM_CALL = 'llm_call', // LLM request made
|
|
19
|
+
TOOL_CALL = 'tool_call', // tool was called
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Event object for message bus.
|
|
24
|
+
*/
|
|
25
|
+
export class Event {
|
|
26
|
+
type: EventType;
|
|
27
|
+
source: string; // agent name or "system"
|
|
28
|
+
target: string | null; // null = broadcast
|
|
29
|
+
data: Record<string, any>;
|
|
30
|
+
timestamp: Date;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
type: EventType,
|
|
34
|
+
source: string,
|
|
35
|
+
target?: string | null,
|
|
36
|
+
data?: Record<string, any>
|
|
37
|
+
) {
|
|
38
|
+
this.type = type;
|
|
39
|
+
this.source = source;
|
|
40
|
+
this.target = target || null;
|
|
41
|
+
this.data = data || {};
|
|
42
|
+
this.timestamp = new Date();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Convert event to JSON-serializable format.
|
|
47
|
+
*/
|
|
48
|
+
toJSON(): Record<string, any> {
|
|
49
|
+
return {
|
|
50
|
+
type: this.type,
|
|
51
|
+
source: this.source,
|
|
52
|
+
target: this.target,
|
|
53
|
+
data: this.data,
|
|
54
|
+
timestamp: this.timestamp.toISOString(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Handler function for events.
|
|
61
|
+
*/
|
|
62
|
+
export type Handler = (event: Event) => Promise<void>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Pub/sub message bus for agent communication.
|
|
66
|
+
*/
|
|
67
|
+
export class MessageBus {
|
|
68
|
+
private subscribers: Map<string, Handler[]> = new Map();
|
|
69
|
+
private stateListeners: Handler[] = [];
|
|
70
|
+
private history: Event[] = [];
|
|
71
|
+
private maxHistory: number = 2000;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Subscribe an agent to events.
|
|
75
|
+
*/
|
|
76
|
+
subscribe(agentName: string, handler: Handler): void {
|
|
77
|
+
if (!this.subscribers.has(agentName)) {
|
|
78
|
+
this.subscribers.set(agentName, []);
|
|
79
|
+
}
|
|
80
|
+
this.subscribers.get(agentName)!.push(handler);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Unsubscribe an agent.
|
|
85
|
+
*/
|
|
86
|
+
unsubscribe(agentName: string): void {
|
|
87
|
+
this.subscribers.delete(agentName);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Register a global handler for state change events.
|
|
92
|
+
*/
|
|
93
|
+
onStateChange(handler: Handler): void {
|
|
94
|
+
this.stateListeners.push(handler);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Remove a state change listener.
|
|
99
|
+
*/
|
|
100
|
+
removeStateListener(handler: Handler): void {
|
|
101
|
+
const idx = this.stateListeners.indexOf(handler);
|
|
102
|
+
if (idx >= 0) {
|
|
103
|
+
this.stateListeners.splice(idx, 1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Trim history to max size.
|
|
109
|
+
*/
|
|
110
|
+
private trimHistory(): void {
|
|
111
|
+
if (this.history.length > this.maxHistory) {
|
|
112
|
+
this.history = this.history.slice(this.history.length - this.maxHistory);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Record event without routing (for state changes / local observation).
|
|
118
|
+
*/
|
|
119
|
+
addEvent(event: Event): void {
|
|
120
|
+
this.history.push(event);
|
|
121
|
+
this.trimHistory();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Notify state change listeners.
|
|
126
|
+
*/
|
|
127
|
+
async notifyStateChange(event: Event): Promise<void> {
|
|
128
|
+
if (event.type !== EventType.STATE_CHANGE) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const handler of this.stateListeners) {
|
|
133
|
+
try {
|
|
134
|
+
await handler(event);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error('state_change handler failed:', err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Publish an event to subscribers.
|
|
143
|
+
*/
|
|
144
|
+
async publish(event: Event): Promise<void> {
|
|
145
|
+
this.history.push(event);
|
|
146
|
+
this.trimHistory();
|
|
147
|
+
|
|
148
|
+
if (event.target) {
|
|
149
|
+
// Direct message
|
|
150
|
+
const handlers = this.subscribers.get(event.target) || [];
|
|
151
|
+
for (const handler of handlers) {
|
|
152
|
+
try {
|
|
153
|
+
await handler(event);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error(`bus handler failed for target=${event.target}:`, err);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
// Broadcast to all except source
|
|
160
|
+
for (const [name, handlers] of this.subscribers.entries()) {
|
|
161
|
+
if (name === event.source) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
for (const handler of handlers) {
|
|
165
|
+
try {
|
|
166
|
+
await handler(event);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error(`bus handler failed for subscriber=${name}:`, err);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get event history.
|
|
177
|
+
*/
|
|
178
|
+
getHistory(
|
|
179
|
+
agentName?: string | null,
|
|
180
|
+
eventType?: EventType | null,
|
|
181
|
+
limit: number = 50
|
|
182
|
+
): Event[] {
|
|
183
|
+
let events = this.history;
|
|
184
|
+
|
|
185
|
+
if (agentName) {
|
|
186
|
+
events = events.filter(e => e.source === agentName || e.target === agentName);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (eventType) {
|
|
190
|
+
events = events.filter(e => e.type === eventType);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return events.slice(Math.max(0, events.length - limit));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Clear all history.
|
|
198
|
+
*/
|
|
199
|
+
clearHistory(): void {
|
|
200
|
+
this.history = [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get event count.
|
|
205
|
+
*/
|
|
206
|
+
getEventCount(): number {
|
|
207
|
+
return this.history.length;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get active subscriber count.
|
|
212
|
+
*/
|
|
213
|
+
getSubscriberCount(): number {
|
|
214
|
+
return this.subscribers.size;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Global event emitter (optional, for direct event emission).
|
|
220
|
+
*/
|
|
221
|
+
export const eventEmitter = new EventEmitter();
|