scalix 1.0.0 → 1.1.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/.env.example +29 -0
- package/README.md +1 -1
- package/dist/agent-definitions.js +100 -0
- package/dist/agent-registry.js +226 -0
- package/dist/agent.js +352 -74
- package/dist/api-client.js +77 -0
- package/dist/cli.js +160 -35
- package/dist/constants.js +40 -6
- package/dist/context-builder.js +8 -1
- package/dist/context-retriever.js +257 -0
- package/dist/git-context.js +32 -29
- package/dist/grounding-checker.js +154 -0
- package/dist/headless.js +109 -20
- package/dist/hooks.js +7 -1
- package/dist/injection-detector.js +86 -0
- package/dist/ipc-server.js +6 -2
- package/dist/project-index.js +2 -0
- package/dist/sandbox.js +135 -11
- package/dist/sessions.js +37 -2
- package/dist/settings.js +111 -6
- package/dist/sub-agent.js +83 -8
- package/dist/task-board.js +105 -0
- package/dist/tools-advanced.js +304 -49
- package/dist/tools-browser.js +280 -19
- package/dist/tools-extra.js +7 -0
- package/dist/tools-vision.js +4 -3
- package/dist/tools.js +13 -3
- package/dist/update-checker.js +111 -0
- package/dist/worktree.js +172 -0
- package/package.json +6 -2
- package/app.py +0 -55
package/dist/sessions.js
CHANGED
|
@@ -5,8 +5,34 @@ import { randomUUID } from 'node:crypto';
|
|
|
5
5
|
import { logger } from './logger.js';
|
|
6
6
|
import { USER_CONFIG_DIR } from './constants.js';
|
|
7
7
|
const SESSIONS_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', USER_CONFIG_DIR, 'sessions');
|
|
8
|
+
let cleanupDone = false;
|
|
9
|
+
async function cleanupStaleTempFiles() {
|
|
10
|
+
if (cleanupDone)
|
|
11
|
+
return;
|
|
12
|
+
cleanupDone = true;
|
|
13
|
+
try {
|
|
14
|
+
const tmpDir = os.tmpdir();
|
|
15
|
+
const entries = await fs.readdir(tmpDir);
|
|
16
|
+
const oneHourAgo = Date.now() - 3600_000;
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (!entry.startsWith('scalix-session-') || !entry.endsWith('.tmp'))
|
|
19
|
+
continue;
|
|
20
|
+
try {
|
|
21
|
+
const fullPath = path.join(tmpDir, entry);
|
|
22
|
+
const stat = await fs.stat(fullPath);
|
|
23
|
+
if (stat.mtimeMs < oneHourAgo) {
|
|
24
|
+
await fs.unlink(fullPath);
|
|
25
|
+
logger.debug('Cleaned up stale temp file', { file: entry });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
}
|
|
8
33
|
async function ensureDir() {
|
|
9
34
|
await fs.mkdir(SESSIONS_DIR, { recursive: true });
|
|
35
|
+
await cleanupStaleTempFiles();
|
|
10
36
|
}
|
|
11
37
|
function sessionPath(id) {
|
|
12
38
|
return path.join(SESSIONS_DIR, `${id}.json`);
|
|
@@ -32,12 +58,19 @@ export async function saveSession(id, messages, model, name, costState) {
|
|
|
32
58
|
}
|
|
33
59
|
export async function loadSession(id) {
|
|
34
60
|
const raw = await fs.readFile(sessionPath(id), 'utf-8');
|
|
61
|
+
let data;
|
|
35
62
|
try {
|
|
36
|
-
|
|
63
|
+
data = JSON.parse(raw);
|
|
37
64
|
}
|
|
38
65
|
catch {
|
|
39
66
|
throw new Error(`Corrupted session file: ${id}`);
|
|
40
67
|
}
|
|
68
|
+
if (!data || typeof data !== 'object' ||
|
|
69
|
+
!('meta' in data) || !('messages' in data) ||
|
|
70
|
+
!Array.isArray(data.messages)) {
|
|
71
|
+
throw new Error(`Invalid session structure: ${id}`);
|
|
72
|
+
}
|
|
73
|
+
return data;
|
|
41
74
|
}
|
|
42
75
|
export async function listSessions() {
|
|
43
76
|
await ensureDir();
|
|
@@ -49,7 +82,9 @@ export async function listSessions() {
|
|
|
49
82
|
try {
|
|
50
83
|
const raw = await fs.readFile(path.join(SESSIONS_DIR, file), 'utf-8');
|
|
51
84
|
const data = JSON.parse(raw);
|
|
52
|
-
|
|
85
|
+
if (data?.meta?.id && data?.meta?.updatedAt) {
|
|
86
|
+
sessions.push(data.meta);
|
|
87
|
+
}
|
|
53
88
|
}
|
|
54
89
|
catch (e) {
|
|
55
90
|
logger.warn('Failed to read session file', { file, error: e.message });
|
package/dist/settings.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { setSecurityConfig } from './sandbox.js';
|
|
4
|
+
import { DEFAULT_BASE_URL, DEFAULT_MODELS, AGENT_DEFAULTS, CONTEXT_DEFAULTS, WEB_DEFAULTS, TOOL_DEFAULTS, SUB_AGENT_DEFAULTS, MULTI_AGENT_DEFAULTS, USER_CONFIG_DIR, registerUserModels, setNetworkSecurityOverrides, } from './constants.js';
|
|
5
|
+
function getConfigDir() {
|
|
6
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || '.', USER_CONFIG_DIR);
|
|
7
|
+
}
|
|
8
|
+
function getConfigFile() {
|
|
9
|
+
return path.join(getConfigDir(), 'config.json');
|
|
10
|
+
}
|
|
6
11
|
export const THINKING_MODES = ['off', 'low', 'medium', 'high'];
|
|
12
|
+
export const DEFAULT_SECURITY = {};
|
|
7
13
|
export const DEFAULT_LIMITS = {
|
|
8
14
|
maxSteps: AGENT_DEFAULTS.maxSteps,
|
|
9
15
|
maxMessages: AGENT_DEFAULTS.maxMessages,
|
|
@@ -41,6 +47,9 @@ export const DEFAULT_LIMITS = {
|
|
|
41
47
|
maxNotebookSource: TOOL_DEFAULTS.maxNotebookSource,
|
|
42
48
|
maxWebFetchLength: TOOL_DEFAULTS.maxWebFetchLength,
|
|
43
49
|
slidingWindowTarget: CONTEXT_DEFAULTS.slidingWindowTarget,
|
|
50
|
+
maxConcurrentAgents: MULTI_AGENT_DEFAULTS.maxConcurrentAgents,
|
|
51
|
+
maxAgentDepth: MULTI_AGENT_DEFAULTS.maxDepth,
|
|
52
|
+
globalTokenBudget: MULTI_AGENT_DEFAULTS.globalTokenBudget,
|
|
44
53
|
};
|
|
45
54
|
const DEFAULTS = {
|
|
46
55
|
apiKey: '',
|
|
@@ -50,6 +59,9 @@ const DEFAULTS = {
|
|
|
50
59
|
limits: { ...DEFAULT_LIMITS },
|
|
51
60
|
permissionMode: 'default',
|
|
52
61
|
thinkingMode: 'off',
|
|
62
|
+
security: { ...DEFAULT_SECURITY },
|
|
63
|
+
injectionDetection: { enabled: true, sensitivity: 'medium' },
|
|
64
|
+
disableUpdateCheck: false,
|
|
53
65
|
};
|
|
54
66
|
function safeNumber(val, min, max, fallback) {
|
|
55
67
|
if (typeof val === 'number' && Number.isFinite(val) && val >= min && val <= max)
|
|
@@ -121,25 +133,118 @@ function validateConfig(parsed) {
|
|
|
121
133
|
maxNotebookSource: safeNumber(l.maxNotebookSource, 1000, Infinity, DEFAULT_LIMITS.maxNotebookSource),
|
|
122
134
|
maxWebFetchLength: safeNumber(l.maxWebFetchLength, 1000, Infinity, DEFAULT_LIMITS.maxWebFetchLength),
|
|
123
135
|
slidingWindowTarget: safeNumber(l.slidingWindowTarget, 0.1, 0.99, DEFAULT_LIMITS.slidingWindowTarget),
|
|
136
|
+
maxConcurrentAgents: safeNumber(l.maxConcurrentAgents, 1, 100, DEFAULT_LIMITS.maxConcurrentAgents),
|
|
137
|
+
maxAgentDepth: safeNumber(l.maxAgentDepth, 1, 20, DEFAULT_LIMITS.maxAgentDepth),
|
|
138
|
+
globalTokenBudget: safeNumber(l.globalTokenBudget, 10_000, Infinity, DEFAULT_LIMITS.globalTokenBudget),
|
|
124
139
|
};
|
|
125
140
|
}
|
|
126
141
|
if (typeof parsed.thinkingMode === 'string' && THINKING_MODES.includes(parsed.thinkingMode)) {
|
|
127
142
|
safe.thinkingMode = parsed.thinkingMode;
|
|
128
143
|
}
|
|
144
|
+
if (parsed.security && typeof parsed.security === 'object' && !Array.isArray(parsed.security)) {
|
|
145
|
+
safe.security = validateSecurityConfig(parsed.security);
|
|
146
|
+
}
|
|
147
|
+
if (parsed.injectionDetection && typeof parsed.injectionDetection === 'object') {
|
|
148
|
+
const id = parsed.injectionDetection;
|
|
149
|
+
const config = {
|
|
150
|
+
enabled: typeof id.enabled === 'boolean' ? id.enabled : true,
|
|
151
|
+
sensitivity: ['low', 'medium', 'high'].includes(id.sensitivity) ? id.sensitivity : 'medium',
|
|
152
|
+
};
|
|
153
|
+
if (isStringArray(id.customPatterns)) {
|
|
154
|
+
config.customPatterns = validateRegexStrings(id.customPatterns);
|
|
155
|
+
}
|
|
156
|
+
safe.injectionDetection = config;
|
|
157
|
+
}
|
|
158
|
+
if (typeof parsed.disableUpdateCheck === 'boolean') {
|
|
159
|
+
safe.disableUpdateCheck = parsed.disableUpdateCheck;
|
|
160
|
+
}
|
|
129
161
|
return safe;
|
|
130
162
|
}
|
|
163
|
+
function isStringArray(val) {
|
|
164
|
+
return Array.isArray(val) && val.every((v) => typeof v === 'string');
|
|
165
|
+
}
|
|
166
|
+
function validateRegexStrings(patterns) {
|
|
167
|
+
return patterns.filter(p => {
|
|
168
|
+
try {
|
|
169
|
+
new RegExp(p);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function validateSecurityConfig(raw) {
|
|
178
|
+
const sec = {};
|
|
179
|
+
if (isStringArray(raw.additionalBlockedCommands)) {
|
|
180
|
+
sec.additionalBlockedCommands = validateRegexStrings(raw.additionalBlockedCommands);
|
|
181
|
+
}
|
|
182
|
+
if (isStringArray(raw.disabledBlockedCommands)) {
|
|
183
|
+
sec.disabledBlockedCommands = raw.disabledBlockedCommands;
|
|
184
|
+
}
|
|
185
|
+
if (isStringArray(raw.additionalInjectionPatterns)) {
|
|
186
|
+
sec.additionalInjectionPatterns = validateRegexStrings(raw.additionalInjectionPatterns);
|
|
187
|
+
}
|
|
188
|
+
if (isStringArray(raw.disabledInjectionPatterns)) {
|
|
189
|
+
sec.disabledInjectionPatterns = raw.disabledInjectionPatterns;
|
|
190
|
+
}
|
|
191
|
+
if (isStringArray(raw.additionalReadOnlyTools))
|
|
192
|
+
sec.additionalReadOnlyTools = raw.additionalReadOnlyTools;
|
|
193
|
+
if (isStringArray(raw.additionalWriteTools))
|
|
194
|
+
sec.additionalWriteTools = raw.additionalWriteTools;
|
|
195
|
+
if (isStringArray(raw.additionalDangerousTools))
|
|
196
|
+
sec.additionalDangerousTools = raw.additionalDangerousTools;
|
|
197
|
+
if (raw.toolOverrides && typeof raw.toolOverrides === 'object' && !Array.isArray(raw.toolOverrides)) {
|
|
198
|
+
const valid = {};
|
|
199
|
+
const levels = new Set(['read-only', 'write', 'dangerous']);
|
|
200
|
+
for (const [tool, level] of Object.entries(raw.toolOverrides)) {
|
|
201
|
+
if (typeof level === 'string' && levels.has(level)) {
|
|
202
|
+
valid[tool] = level;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (Object.keys(valid).length > 0)
|
|
206
|
+
sec.toolOverrides = valid;
|
|
207
|
+
}
|
|
208
|
+
if (typeof raw.allowPrivateNetworkAccess === 'boolean')
|
|
209
|
+
sec.allowPrivateNetworkAccess = raw.allowPrivateNetworkAccess;
|
|
210
|
+
if (isStringArray(raw.additionalBlockedHosts))
|
|
211
|
+
sec.additionalBlockedHosts = raw.additionalBlockedHosts;
|
|
212
|
+
if (isStringArray(raw.allowedPrivateHosts))
|
|
213
|
+
sec.allowedPrivateHosts = raw.allowedPrivateHosts;
|
|
214
|
+
if (isStringArray(raw.additionalSensitiveEnvPatterns)) {
|
|
215
|
+
sec.additionalSensitiveEnvPatterns = validateRegexStrings(raw.additionalSensitiveEnvPatterns);
|
|
216
|
+
}
|
|
217
|
+
if (isStringArray(raw.allowedEnvPatterns)) {
|
|
218
|
+
sec.allowedEnvPatterns = validateRegexStrings(raw.allowedEnvPatterns);
|
|
219
|
+
}
|
|
220
|
+
if (typeof raw.maxImageSize === 'number' && raw.maxImageSize > 0) {
|
|
221
|
+
sec.maxImageSize = raw.maxImageSize;
|
|
222
|
+
}
|
|
223
|
+
return sec;
|
|
224
|
+
}
|
|
131
225
|
async function ensureDir() {
|
|
132
|
-
await fs.mkdir(
|
|
226
|
+
await fs.mkdir(getConfigDir(), { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
export function applySecurityConfig(security) {
|
|
229
|
+
setSecurityConfig(security);
|
|
230
|
+
setNetworkSecurityOverrides({
|
|
231
|
+
allowAll: security.allowPrivateNetworkAccess,
|
|
232
|
+
blockedHosts: security.additionalBlockedHosts,
|
|
233
|
+
allowedHosts: security.allowedPrivateHosts,
|
|
234
|
+
});
|
|
133
235
|
}
|
|
134
236
|
export async function loadConfig() {
|
|
135
237
|
try {
|
|
136
|
-
const raw = await fs.readFile(
|
|
238
|
+
const raw = await fs.readFile(getConfigFile(), 'utf-8');
|
|
137
239
|
const parsed = JSON.parse(raw);
|
|
138
240
|
const validated = validateConfig(parsed);
|
|
139
241
|
const config = { ...DEFAULTS, ...validated };
|
|
140
242
|
if (config.customModels && Object.keys(config.customModels).length > 0) {
|
|
141
243
|
registerUserModels(config.customModels);
|
|
142
244
|
}
|
|
245
|
+
if (config.security && Object.keys(config.security).length > 0) {
|
|
246
|
+
applySecurityConfig(config.security);
|
|
247
|
+
}
|
|
143
248
|
return config;
|
|
144
249
|
}
|
|
145
250
|
catch {
|
|
@@ -148,7 +253,7 @@ export async function loadConfig() {
|
|
|
148
253
|
}
|
|
149
254
|
export async function saveConfig(config) {
|
|
150
255
|
await ensureDir();
|
|
151
|
-
await fs.writeFile(
|
|
256
|
+
await fs.writeFile(getConfigFile(), JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
152
257
|
}
|
|
153
258
|
export function isConfigured(config) {
|
|
154
259
|
return config.apiKey.length > 0 && config.models.length > 0;
|
package/dist/sub-agent.js
CHANGED
|
@@ -5,15 +5,21 @@ import { webTools } from './tools-web.js';
|
|
|
5
5
|
import { advancedTools } from './tools-advanced.js';
|
|
6
6
|
import { logger } from './logger.js';
|
|
7
7
|
import { SUB_AGENT_DEFAULTS, CHEAP_MODEL_INDICATORS } from './constants.js';
|
|
8
|
+
import { AgentRegistry, generateAgentId } from './agent-registry.js';
|
|
9
|
+
import { createWorktree, removeWorktree, mergeWorktree } from './worktree.js';
|
|
8
10
|
const READ_ONLY_TOOL_NAMES = new Set([
|
|
9
11
|
'get_current_time', 'calculate', 'read_file', 'read_file_lines',
|
|
10
12
|
'list_files', 'search_files', 'find_files', 'tree',
|
|
11
13
|
'git_status', 'git_diff', 'web_search', 'monitor',
|
|
12
14
|
]);
|
|
13
|
-
function selectTools(level) {
|
|
14
|
-
// Include advanced tools but exclude 'agent' to prevent recursive spawning
|
|
15
|
+
function selectTools(level, allowAgentTool = false) {
|
|
15
16
|
const all = [...defaultTools, ...extraTools, ...webTools, ...advancedTools]
|
|
16
|
-
.filter((t) =>
|
|
17
|
+
.filter((t) => {
|
|
18
|
+
const name = t.function?.name || t.name;
|
|
19
|
+
if (name === 'agent' && !allowAgentTool)
|
|
20
|
+
return false;
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
17
23
|
switch (level) {
|
|
18
24
|
case 'read-only':
|
|
19
25
|
return all.filter((t) => READ_ONLY_TOOL_NAMES.has(t.function?.name || t.name));
|
|
@@ -26,10 +32,32 @@ function selectTools(level) {
|
|
|
26
32
|
}
|
|
27
33
|
export async function spawnSubAgent(parentConfig, prompt, options = {}) {
|
|
28
34
|
const model = options.model || parentConfig.models[parentConfig.models.length - 1];
|
|
29
|
-
const
|
|
35
|
+
const allowAgentTool = options.allowAgentTool ?? false;
|
|
36
|
+
const tools = selectTools(options.tools, allowAgentTool);
|
|
30
37
|
const timeout = options.timeout ?? SUB_AGENT_DEFAULTS.timeoutMs;
|
|
38
|
+
const subAgentId = generateAgentId();
|
|
39
|
+
const parentId = options.parentAgentId ?? null;
|
|
40
|
+
const description = options.agentDescription ?? 'sub-agent';
|
|
41
|
+
const registry = AgentRegistry.getInstance();
|
|
42
|
+
const spawnCheck = registry.canSpawn(parentId);
|
|
43
|
+
if (!spawnCheck.allowed) {
|
|
44
|
+
throw new Error(`Cannot spawn agent: ${spawnCheck.reason}`);
|
|
45
|
+
}
|
|
31
46
|
const permissionMode = options.permissionMode
|
|
32
|
-
?? (options.tools === 'read-only' ? 'plan' :
|
|
47
|
+
?? (options.tools === 'read-only' ? 'plan' : parentConfig.permissionMode ?? 'default');
|
|
48
|
+
// Worktree isolation for write-capable agents
|
|
49
|
+
const shouldIsolate = options.isolate !== false && options.tools !== 'read-only';
|
|
50
|
+
let worktreeInfo = null;
|
|
51
|
+
if (shouldIsolate) {
|
|
52
|
+
try {
|
|
53
|
+
worktreeInfo = await createWorktree(subAgentId, process.cwd());
|
|
54
|
+
logger.info('Agent isolated in worktree', { agentId: subAgentId, path: worktreeInfo.path });
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
logger.warn('Worktree creation failed, running without isolation', { error: err.message });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const isReadOnly = options.tools === 'read-only';
|
|
33
61
|
const subAgent = createAgent({
|
|
34
62
|
apiKey: parentConfig.apiKey,
|
|
35
63
|
baseUrl: parentConfig.baseUrl,
|
|
@@ -43,6 +71,11 @@ export async function spawnSubAgent(parentConfig, prompt, options = {}) {
|
|
|
43
71
|
maxContextTokens: SUB_AGENT_DEFAULTS.maxContextTokens,
|
|
44
72
|
compressionThreshold: SUB_AGENT_DEFAULTS.compressionThreshold,
|
|
45
73
|
},
|
|
74
|
+
agentId: subAgentId,
|
|
75
|
+
parentAgentId: parentId,
|
|
76
|
+
agentDescription: description,
|
|
77
|
+
groundingCheck: true,
|
|
78
|
+
contextRetrieval: !isReadOnly,
|
|
46
79
|
});
|
|
47
80
|
let steps = 0;
|
|
48
81
|
let tokenEstimate = 0;
|
|
@@ -56,18 +89,60 @@ export async function spawnSubAgent(parentConfig, prompt, options = {}) {
|
|
|
56
89
|
});
|
|
57
90
|
try {
|
|
58
91
|
const response = await Promise.race([subAgent.send(prompt), timeoutPromise]);
|
|
59
|
-
logger.info('Sub-agent completed', { model, steps, tokens: tokenEstimate });
|
|
60
|
-
|
|
92
|
+
logger.info('Sub-agent completed', { agentId: subAgentId, model, steps, tokens: tokenEstimate });
|
|
93
|
+
let mergeResult;
|
|
94
|
+
if (worktreeInfo) {
|
|
95
|
+
try {
|
|
96
|
+
mergeResult = await mergeWorktree(worktreeInfo, process.cwd());
|
|
97
|
+
logger.info('Worktree merge result', {
|
|
98
|
+
agentId: subAgentId,
|
|
99
|
+
merged: mergeResult.merged,
|
|
100
|
+
commits: mergeResult.commitCount,
|
|
101
|
+
conflicts: mergeResult.conflicts?.length ?? 0,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
logger.warn('Worktree merge failed', { error: err.message });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
response,
|
|
110
|
+
model,
|
|
111
|
+
tokenEstimate,
|
|
112
|
+
steps,
|
|
113
|
+
agentId: subAgentId,
|
|
114
|
+
worktree: mergeResult ? {
|
|
115
|
+
merged: mergeResult.merged,
|
|
116
|
+
conflicts: mergeResult.conflicts,
|
|
117
|
+
commitCount: mergeResult.commitCount,
|
|
118
|
+
} : undefined,
|
|
119
|
+
};
|
|
61
120
|
}
|
|
62
121
|
catch (err) {
|
|
63
122
|
subAgent.abort();
|
|
64
|
-
logger.error('Sub-agent failed', { model, error: err.message });
|
|
123
|
+
logger.error('Sub-agent failed', { agentId: subAgentId, model, error: err.message });
|
|
65
124
|
throw err;
|
|
66
125
|
}
|
|
67
126
|
finally {
|
|
68
127
|
clearTimeout(timer);
|
|
128
|
+
if (worktreeInfo) {
|
|
129
|
+
try {
|
|
130
|
+
await removeWorktree(worktreeInfo, process.cwd());
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
logger.warn('Worktree cleanup failed', { error: err.message });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
69
136
|
}
|
|
70
137
|
}
|
|
138
|
+
export async function spawnParallelAgents(parentConfig, requests) {
|
|
139
|
+
logger.info('Spawning parallel agents', { count: requests.length });
|
|
140
|
+
return Promise.allSettled(requests.map(r => spawnSubAgent(parentConfig, r.prompt, {
|
|
141
|
+
...r.options,
|
|
142
|
+
allowAgentTool: r.options?.allowAgentTool ?? true,
|
|
143
|
+
agentDescription: r.description,
|
|
144
|
+
})));
|
|
145
|
+
}
|
|
71
146
|
export function pickCheapestModel(models) {
|
|
72
147
|
const cheapIndicators = CHEAP_MODEL_INDICATORS;
|
|
73
148
|
for (const model of [...models].reverse()) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { EventEmitter } from 'eventemitter3';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
export class TaskBoard extends EventEmitter {
|
|
5
|
+
static instance = null;
|
|
6
|
+
tasks = new Map();
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
}
|
|
10
|
+
static getInstance() {
|
|
11
|
+
if (!TaskBoard.instance) {
|
|
12
|
+
TaskBoard.instance = new TaskBoard();
|
|
13
|
+
}
|
|
14
|
+
return TaskBoard.instance;
|
|
15
|
+
}
|
|
16
|
+
static resetInstance() {
|
|
17
|
+
if (TaskBoard.instance) {
|
|
18
|
+
TaskBoard.instance.removeAllListeners();
|
|
19
|
+
TaskBoard.instance.tasks.clear();
|
|
20
|
+
}
|
|
21
|
+
TaskBoard.instance = null;
|
|
22
|
+
}
|
|
23
|
+
create(agentId, title, description) {
|
|
24
|
+
const task = {
|
|
25
|
+
id: randomUUID().slice(0, 8),
|
|
26
|
+
title,
|
|
27
|
+
description,
|
|
28
|
+
status: 'open',
|
|
29
|
+
assignee: null,
|
|
30
|
+
createdBy: agentId,
|
|
31
|
+
createdAt: Date.now(),
|
|
32
|
+
updatedAt: Date.now(),
|
|
33
|
+
};
|
|
34
|
+
this.tasks.set(task.id, task);
|
|
35
|
+
this.emit('task:created', task);
|
|
36
|
+
logger.debug('Task created', { id: task.id, title, by: agentId });
|
|
37
|
+
return task;
|
|
38
|
+
}
|
|
39
|
+
claim(taskId, agentId) {
|
|
40
|
+
const task = this.tasks.get(taskId);
|
|
41
|
+
if (!task) {
|
|
42
|
+
return { error: `Task "${taskId}" not found` };
|
|
43
|
+
}
|
|
44
|
+
if (task.status !== 'open') {
|
|
45
|
+
return { error: `Task "${taskId}" is not open (status: ${task.status}, assignee: ${task.assignee})` };
|
|
46
|
+
}
|
|
47
|
+
task.assignee = agentId;
|
|
48
|
+
task.status = 'claimed';
|
|
49
|
+
task.updatedAt = Date.now();
|
|
50
|
+
this.emit('task:claimed', task);
|
|
51
|
+
logger.debug('Task claimed', { id: taskId, by: agentId });
|
|
52
|
+
return task;
|
|
53
|
+
}
|
|
54
|
+
update(taskId, agentId, updates) {
|
|
55
|
+
const task = this.tasks.get(taskId);
|
|
56
|
+
if (!task) {
|
|
57
|
+
return { error: `Task "${taskId}" not found` };
|
|
58
|
+
}
|
|
59
|
+
if (task.assignee && task.assignee !== agentId && task.createdBy !== agentId) {
|
|
60
|
+
return { error: `Agent "${agentId}" is not the assignee or creator of task "${taskId}"` };
|
|
61
|
+
}
|
|
62
|
+
if (updates.status) {
|
|
63
|
+
task.status = updates.status;
|
|
64
|
+
if (updates.status === 'in_progress' && !task.assignee) {
|
|
65
|
+
task.assignee = agentId;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (updates.result !== undefined) {
|
|
69
|
+
task.result = updates.result;
|
|
70
|
+
}
|
|
71
|
+
task.updatedAt = Date.now();
|
|
72
|
+
if (task.status === 'completed' || task.status === 'failed') {
|
|
73
|
+
this.emit('task:completed', task);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
this.emit('task:updated', task);
|
|
77
|
+
}
|
|
78
|
+
logger.debug('Task updated', { id: taskId, status: task.status, by: agentId });
|
|
79
|
+
return task;
|
|
80
|
+
}
|
|
81
|
+
list(filter) {
|
|
82
|
+
let tasks = [...this.tasks.values()];
|
|
83
|
+
if (filter?.status) {
|
|
84
|
+
tasks = tasks.filter(t => t.status === filter.status);
|
|
85
|
+
}
|
|
86
|
+
if (filter?.assignee) {
|
|
87
|
+
tasks = tasks.filter(t => t.assignee === filter.assignee);
|
|
88
|
+
}
|
|
89
|
+
return tasks;
|
|
90
|
+
}
|
|
91
|
+
get(taskId) {
|
|
92
|
+
return this.tasks.get(taskId);
|
|
93
|
+
}
|
|
94
|
+
getStats() {
|
|
95
|
+
const all = [...this.tasks.values()];
|
|
96
|
+
return {
|
|
97
|
+
total: all.length,
|
|
98
|
+
open: all.filter(t => t.status === 'open').length,
|
|
99
|
+
claimed: all.filter(t => t.status === 'claimed').length,
|
|
100
|
+
inProgress: all.filter(t => t.status === 'in_progress').length,
|
|
101
|
+
completed: all.filter(t => t.status === 'completed').length,
|
|
102
|
+
failed: all.filter(t => t.status === 'failed').length,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|