@parallel-cli/parallel 0.4.9 → 0.5.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/CHANGELOG.md +33 -0
- package/README.md +23 -6
- package/dist/agents/agent.js +194 -25
- package/dist/agents/execution-policy.js +58 -0
- package/dist/agents/tools.js +71 -17
- package/dist/commands.js +62 -5
- package/dist/controller.js +136 -5
- package/dist/diagnostics.js +209 -0
- package/dist/i18n.js +40 -4
- package/dist/index.js +12 -2
- package/dist/llm/client.js +7 -3
- package/dist/project-context.js +477 -0
- package/dist/project-index.js +186 -0
- package/dist/ui/AgentPanel.js +5 -2
- package/dist/ui/App.js +4 -2
- package/dist/ui/SettingsPanel.js +22 -23
- package/dist/ui/Wizard.js +49 -21
- package/dist/ui/views.js +4 -2
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/agents/tools.js
CHANGED
|
@@ -29,7 +29,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
29
29
|
type: 'function',
|
|
30
30
|
function: {
|
|
31
31
|
name: 'list_files',
|
|
32
|
-
description: 'List project files (recursive, ignores node_modules/.git/dist). Use
|
|
32
|
+
description: 'List project files (recursive, ignores node_modules/.git/dist). Use only when shared project context does not already identify the relevant area.',
|
|
33
33
|
parameters: {
|
|
34
34
|
type: 'object',
|
|
35
35
|
properties: {
|
|
@@ -47,6 +47,8 @@ export const TOOL_DEFINITIONS = [
|
|
|
47
47
|
type: 'object',
|
|
48
48
|
properties: {
|
|
49
49
|
path: { type: 'string', description: 'Relative file path' },
|
|
50
|
+
startLine: { type: 'integer', description: 'Optional first line, 1-based' },
|
|
51
|
+
endLine: { type: 'integer', description: 'Optional last line, inclusive' },
|
|
50
52
|
},
|
|
51
53
|
required: ['path'],
|
|
52
54
|
},
|
|
@@ -70,6 +72,22 @@ export const TOOL_DEFINITIONS = [
|
|
|
70
72
|
},
|
|
71
73
|
},
|
|
72
74
|
},
|
|
75
|
+
{
|
|
76
|
+
type: 'function',
|
|
77
|
+
function: {
|
|
78
|
+
name: 'read_artifact',
|
|
79
|
+
description: 'Read a targeted line range from a long tool output previously stored as an artifact.',
|
|
80
|
+
parameters: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
id: { type: 'string', description: 'Artifact id returned by a previous tool result' },
|
|
84
|
+
startLine: { type: 'integer', description: 'First line, 1-based' },
|
|
85
|
+
lineCount: { type: 'integer', description: 'Number of lines, max 200' },
|
|
86
|
+
},
|
|
87
|
+
required: ['id'],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
73
91
|
{
|
|
74
92
|
type: 'function',
|
|
75
93
|
function: {
|
|
@@ -120,7 +138,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
120
138
|
type: 'function',
|
|
121
139
|
function: {
|
|
122
140
|
name: 'inspect_project',
|
|
123
|
-
description: 'Batch read-only inspection: list files under paths and/or run several regex searches in one call. Prefer this over cascades of grep/head/tail/wc/awk
|
|
141
|
+
description: 'Batch targeted read-only inspection: list files under task-relevant paths and/or run several regex searches in one call. Prefer this over generic repository exploration or cascades of grep/head/tail/wc/awk.',
|
|
124
142
|
parameters: {
|
|
125
143
|
type: 'object',
|
|
126
144
|
properties: {
|
|
@@ -185,7 +203,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
185
203
|
type: 'function',
|
|
186
204
|
function: {
|
|
187
205
|
name: 'update_steps',
|
|
188
|
-
description: 'Update the visible Cursor-style task checklist. At task start create 3-6
|
|
206
|
+
description: 'Update the visible Cursor-style task checklist. At task start create 3-6 outcome-oriented steps; do not add a generic project-exploration step when shared context already covers it. Keep exactly one active step.',
|
|
189
207
|
parameters: {
|
|
190
208
|
type: 'object',
|
|
191
209
|
properties: {
|
|
@@ -315,12 +333,15 @@ export class ToolExecutor {
|
|
|
315
333
|
requestQuestion;
|
|
316
334
|
skills;
|
|
317
335
|
mode;
|
|
336
|
+
onInspect;
|
|
337
|
+
profile;
|
|
338
|
+
maxOutput;
|
|
318
339
|
/** Last content this agent has seen for each file — basis of adaptive merging. */
|
|
319
340
|
lastRead = new Map();
|
|
320
341
|
/** Questions already asked — capped at 3 per task. */
|
|
321
342
|
questionsAsked = 0;
|
|
322
343
|
planApproved = false;
|
|
323
|
-
constructor(board, agentId, agentName, projectRoot, requestApproval, requestQuestion, skills, mode = 'task') {
|
|
344
|
+
constructor(board, agentId, agentName, projectRoot, requestApproval, requestQuestion, skills, mode = 'task', onInspect, profile = 'standard', maxOutput = MAX_OUTPUT) {
|
|
324
345
|
this.board = board;
|
|
325
346
|
this.agentId = agentId;
|
|
326
347
|
this.agentName = agentName;
|
|
@@ -329,6 +350,9 @@ export class ToolExecutor {
|
|
|
329
350
|
this.requestQuestion = requestQuestion;
|
|
330
351
|
this.skills = skills;
|
|
331
352
|
this.mode = mode;
|
|
353
|
+
this.onInspect = onInspect;
|
|
354
|
+
this.profile = profile;
|
|
355
|
+
this.maxOutput = maxOutput;
|
|
332
356
|
}
|
|
333
357
|
resolve(rel) {
|
|
334
358
|
const root = path.resolve(this.projectRoot);
|
|
@@ -344,6 +368,7 @@ export class ToolExecutor {
|
|
|
344
368
|
}
|
|
345
369
|
rememberRead(relPath, content) {
|
|
346
370
|
this.lastRead.set(relPath, { content, revision: this.board.fileRevision(relPath) });
|
|
371
|
+
this.onInspect?.(this.agentId, relPath, content);
|
|
347
372
|
}
|
|
348
373
|
adaptationMessage(relPath, seen, current, verb = 'was modified') {
|
|
349
374
|
this.board.recordConflict(relPath);
|
|
@@ -421,9 +446,11 @@ export class ToolExecutor {
|
|
|
421
446
|
case 'list_files':
|
|
422
447
|
return this.listFiles(args?.path ?? '.');
|
|
423
448
|
case 'read_file':
|
|
424
|
-
return this.readFile(args.path);
|
|
449
|
+
return this.readFile(args.path, args?.startLine, args?.endLine);
|
|
425
450
|
case 'read_many':
|
|
426
451
|
return this.readMany(args.paths);
|
|
452
|
+
case 'read_artifact':
|
|
453
|
+
return this.readArtifact(args.id, args?.startLine, args?.lineCount);
|
|
427
454
|
case 'write_file':
|
|
428
455
|
return this.writeFile(args.path, args.content);
|
|
429
456
|
case 'edit_file':
|
|
@@ -622,16 +649,18 @@ export class ToolExecutor {
|
|
|
622
649
|
return '(empty folder)';
|
|
623
650
|
return out.join('\n');
|
|
624
651
|
}
|
|
625
|
-
readFile(rel) {
|
|
652
|
+
readFile(rel, requestedStart, requestedEnd) {
|
|
626
653
|
const abs = this.resolve(rel);
|
|
627
654
|
const relPath = this.relOf(rel);
|
|
628
655
|
const content = fs.readFileSync(abs, 'utf8');
|
|
629
656
|
this.rememberRead(relPath, content);
|
|
630
657
|
this.board.resolveConflict(relPath);
|
|
631
658
|
const lines = content.split('\n');
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
659
|
+
const start = Math.max(1, Math.floor(Number(requestedStart) || 1));
|
|
660
|
+
const end = Math.min(lines.length, Math.max(start, Math.floor(Number(requestedEnd) || lines.length)));
|
|
661
|
+
const numbered = lines.slice(start - 1, end).map((l, i) => `${String(start + i).padStart(4)}|${l}`).join('\n');
|
|
662
|
+
return numbered.length > this.maxOutput
|
|
663
|
+
? numbered.slice(0, this.maxOutput) + `\n... (truncated; requested ${start}-${end}, ${lines.length} lines total)`
|
|
635
664
|
: numbered;
|
|
636
665
|
}
|
|
637
666
|
readMany(paths) {
|
|
@@ -648,8 +677,9 @@ export class ToolExecutor {
|
|
|
648
677
|
this.board.resolveConflict(relPath);
|
|
649
678
|
const lines = content.split('\n');
|
|
650
679
|
const numbered = lines.map((l, i) => `${String(i + 1).padStart(4)}|${l}`).join('\n');
|
|
651
|
-
const
|
|
652
|
-
|
|
680
|
+
const perFile = Math.max(1_000, Math.floor(this.maxOutput / relPaths.length));
|
|
681
|
+
const body = numbered.length > perFile
|
|
682
|
+
? numbered.slice(0, perFile) + `\n... (truncated, ${lines.length} lines total)`
|
|
653
683
|
: numbered;
|
|
654
684
|
chunks.push(`--- ${relPath} (${lines.length} lines) ---\n${body}`);
|
|
655
685
|
}
|
|
@@ -659,6 +689,19 @@ export class ToolExecutor {
|
|
|
659
689
|
}
|
|
660
690
|
return chunks.join('\n\n');
|
|
661
691
|
}
|
|
692
|
+
readArtifact(id, requestedStart, requestedCount) {
|
|
693
|
+
const safeId = String(id ?? '');
|
|
694
|
+
if (!/^artifact-\d+\.txt$/.test(safeId))
|
|
695
|
+
return 'ERROR: invalid artifact id.';
|
|
696
|
+
const file = path.join(this.projectRoot, '.parallel', 'runs', this.agentId, 'artifacts', safeId);
|
|
697
|
+
if (!fs.existsSync(file))
|
|
698
|
+
return `ERROR: artifact not found: ${safeId}`;
|
|
699
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
|
700
|
+
const start = Math.max(1, Math.floor(Number(requestedStart) || 1));
|
|
701
|
+
const count = Math.min(200, Math.max(1, Math.floor(Number(requestedCount) || 80)));
|
|
702
|
+
const end = Math.min(lines.length, start + count - 1);
|
|
703
|
+
return lines.slice(start - 1, end).map((line, index) => `${String(start + index).padStart(4)}|${line}`).join('\n');
|
|
704
|
+
}
|
|
662
705
|
/**
|
|
663
706
|
* Adaptive co-editing: writing is NEVER blocked by another agent.
|
|
664
707
|
* But if the file changed under you since your last read, you first get
|
|
@@ -737,7 +780,8 @@ export class ToolExecutor {
|
|
|
737
780
|
}
|
|
738
781
|
const results = [];
|
|
739
782
|
const walk = (dir, depth) => {
|
|
740
|
-
|
|
783
|
+
const resultLimit = this.profile === 'quick' ? 40 : this.profile === 'standard' ? 80 : 150;
|
|
784
|
+
if (depth > 6 || results.length >= resultLimit)
|
|
741
785
|
return;
|
|
742
786
|
let entries;
|
|
743
787
|
try {
|
|
@@ -765,11 +809,15 @@ export class ToolExecutor {
|
|
|
765
809
|
continue;
|
|
766
810
|
}
|
|
767
811
|
const lines = content.split('\n');
|
|
768
|
-
|
|
812
|
+
let matched = false;
|
|
813
|
+
for (let i = 0; i < lines.length && results.length < resultLimit; i++) {
|
|
769
814
|
if (re.test(lines[i])) {
|
|
815
|
+
matched = true;
|
|
770
816
|
results.push(`${path.relative(this.projectRoot, full)}:${i + 1}: ${lines[i].trim().slice(0, 160)}`);
|
|
771
817
|
}
|
|
772
818
|
}
|
|
819
|
+
if (matched)
|
|
820
|
+
this.onInspect?.(this.agentId, path.relative(this.projectRoot, full), content);
|
|
773
821
|
}
|
|
774
822
|
}
|
|
775
823
|
};
|
|
@@ -829,12 +877,17 @@ export class ToolExecutor {
|
|
|
829
877
|
return;
|
|
830
878
|
}
|
|
831
879
|
const lines = content.split('\n');
|
|
880
|
+
let matched = false;
|
|
832
881
|
for (let i = 0; i < lines.length && matches.length <= 200; i++) {
|
|
833
882
|
for (const { raw, re } of regexes) {
|
|
834
|
-
if (re.test(lines[i]))
|
|
883
|
+
if (re.test(lines[i])) {
|
|
884
|
+
matched = true;
|
|
835
885
|
matches.push(`${raw} :: ${relPath}:${i + 1}: ${lines[i].trim().slice(0, 140)}`);
|
|
886
|
+
}
|
|
836
887
|
}
|
|
837
888
|
}
|
|
889
|
+
if (matched)
|
|
890
|
+
this.onInspect?.(this.agentId, relPath, content);
|
|
838
891
|
};
|
|
839
892
|
for (const rel of paths)
|
|
840
893
|
visit(this.resolve(rel), 0);
|
|
@@ -863,11 +916,12 @@ export class ToolExecutor {
|
|
|
863
916
|
out += '\n(process killed: 120s timeout)';
|
|
864
917
|
else if (err)
|
|
865
918
|
out += `\n(exit code: ${err.code ?? 1})`;
|
|
866
|
-
if (out.length > MAX_OUTPUT)
|
|
867
|
-
out = out.slice(0, MAX_OUTPUT) + '\n... (output truncated)';
|
|
868
919
|
const result = out || '(no output, success)';
|
|
869
920
|
const changed = this.recordShellMutations(before);
|
|
870
|
-
|
|
921
|
+
const logged = result.length > this.maxOutput
|
|
922
|
+
? `${result.slice(0, this.maxOutput)}\n... (${result.length.toLocaleString()} characters; full output retained as an agent artifact)`
|
|
923
|
+
: result;
|
|
924
|
+
this.board.log(this.agentId, 'tool_result', logged);
|
|
871
925
|
resolve(changed > 0 ? `${result}\n\nTracked shell mutations: ${changed} file${changed === 1 ? '' : 's'}.` : result);
|
|
872
926
|
});
|
|
873
927
|
});
|
package/dist/commands.js
CHANGED
|
@@ -9,9 +9,9 @@ import { t } from './i18n.js';
|
|
|
9
9
|
// inspect the session → git safety net → session & config → exit.
|
|
10
10
|
export const COMMANDS = [
|
|
11
11
|
// create agents
|
|
12
|
-
{ name: '/ask', args: '[Name:] <question> [--model=m]', descKey: 'cmd.ask', group: 'modes', aliases: ['/a'] },
|
|
13
|
-
{ name: '/task', args: '[Name:] <task> [--model=m] [#skill]', descKey: 'cmd.task', group: 'modes', aliases: ['/t'] },
|
|
14
|
-
{ name: '/plan', args: '[Name:] <task> [--model=m]', descKey: 'cmd.plan', group: 'modes', aliases: ['/p'] },
|
|
12
|
+
{ name: '/ask', args: '[Name:] <question> [--quick|--standard|--deep] [--model=m]', descKey: 'cmd.ask', group: 'modes', aliases: ['/a'] },
|
|
13
|
+
{ name: '/task', args: '[Name:] <task> [--quick|--standard|--deep] [--model=m] [#skill]', descKey: 'cmd.task', group: 'modes', aliases: ['/t'] },
|
|
14
|
+
{ name: '/plan', args: '[Name:] <task> [--quick|--standard|--deep] [--model=m]', descKey: 'cmd.plan', group: 'modes', aliases: ['/p'] },
|
|
15
15
|
{ name: '/review', args: '[agent|all] [prompt]', descKey: 'cmd.review', group: 'modes' },
|
|
16
16
|
{ name: '/issue', args: '<n>', descKey: 'cmd.issue', group: 'git' },
|
|
17
17
|
{ name: '/specialist', args: '<name> <task> | new <name> [global]', descKey: 'cmd.specialist', group: 'modes' },
|
|
@@ -39,6 +39,7 @@ export const COMMANDS = [
|
|
|
39
39
|
{ name: '/diff', args: '', descKey: 'cmd.diff', group: 'views' },
|
|
40
40
|
{ name: '/cost', args: '', descKey: 'cmd.cost', group: 'views' },
|
|
41
41
|
{ name: '/status', args: '', descKey: 'cmd.status', group: 'views' },
|
|
42
|
+
{ name: '/memory', args: '[refresh]', descKey: 'cmd.memory', group: 'views' },
|
|
42
43
|
// sessions
|
|
43
44
|
{ name: '/save', args: '[name]', descKey: 'cmd.save', group: 'git' },
|
|
44
45
|
{ name: '/sessions', args: '', descKey: 'cmd.sessions', group: 'git' },
|
|
@@ -172,6 +173,10 @@ async function doctorReport(ctl, ui) {
|
|
|
172
173
|
}
|
|
173
174
|
lines.push(commandExists('git') ? t('m.doctorGitOk') : t('m.doctorGitMissing'));
|
|
174
175
|
lines.push(commandExists('gh') ? t('m.doctorGhOk') : t('m.doctorGhMissing'));
|
|
176
|
+
const memory = ctl.projectContextStatus();
|
|
177
|
+
if (['idle', 'fallback', 'error'].includes(memory.status) && level !== 'error')
|
|
178
|
+
level = 'warn';
|
|
179
|
+
lines.push(t('m.doctorMemory', { status: memory.status, date: memory.generatedAt ?? '—' }));
|
|
175
180
|
ui.system(t('m.doctorReport', { lines: lines.join('\n') }), level);
|
|
176
181
|
}
|
|
177
182
|
/**
|
|
@@ -225,12 +230,21 @@ function spawnFrom(arg, ctl, ui, images, specialist, mode = 'task') {
|
|
|
225
230
|
}
|
|
226
231
|
// optional --model=xxx flag
|
|
227
232
|
let model;
|
|
233
|
+
let profile;
|
|
228
234
|
let task = arg;
|
|
229
235
|
const mFlag = task.match(/\s--model=(\S+)/);
|
|
230
236
|
if (mFlag) {
|
|
231
237
|
model = mFlag[1];
|
|
232
238
|
task = task.replace(mFlag[0], '').trim();
|
|
233
239
|
}
|
|
240
|
+
for (const candidate of ['quick', 'standard', 'deep']) {
|
|
241
|
+
const flag = `--${candidate}`;
|
|
242
|
+
if (new RegExp(`(^|\\s)${flag}(?=\\s|$)`).test(task)) {
|
|
243
|
+
profile = candidate;
|
|
244
|
+
task = task.replace(new RegExp(`(^|\\s)${flag}(?=\\s|$)`), ' ').trim();
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
234
248
|
// optional #skill tokens → force-load these skills at the start of the task
|
|
235
249
|
const forced = [];
|
|
236
250
|
const available = ctl.getSkills();
|
|
@@ -249,11 +263,12 @@ function spawnFrom(arg, ctl, ui, images, specialist, mode = 'task') {
|
|
|
249
263
|
// optional "Name:" prefix
|
|
250
264
|
const named = task.match(/^([\p{L}\p{N}_-]{1,16}):\s+(.+)$/su);
|
|
251
265
|
const finalTask = named ? named[2] : task;
|
|
252
|
-
const agent = ctl.spawnAgent(finalTask, named ? named[1] : undefined, model, images, specialist, undefined, mode);
|
|
266
|
+
const agent = ctl.spawnAgent(finalTask, named ? named[1] : undefined, model, images, specialist, undefined, mode, profile);
|
|
253
267
|
if (!agent)
|
|
254
268
|
return ui.system(specialist ? t('m.noSpecialist', { name: specialist }) : t('m.spawnFail'), 'error');
|
|
255
269
|
ui.system(t('m.spawned', { name: agent.name, model: model ? ` (${model})` : '' }) +
|
|
256
270
|
` /${mode}` +
|
|
271
|
+
` [${ctl.board.agents.get(agent.id)?.profile ?? profile ?? 'auto'}]` +
|
|
257
272
|
(specialist ? ` 🎓${specialist}` : '') +
|
|
258
273
|
(forced.length > 0 ? ` 🧩${forced.join(',')}` : ''), 'info');
|
|
259
274
|
}
|
|
@@ -495,6 +510,34 @@ export function executeInput(raw, ctl, ui, images) {
|
|
|
495
510
|
case '/cost':
|
|
496
511
|
ui.setView('cost');
|
|
497
512
|
return;
|
|
513
|
+
case '/memory': {
|
|
514
|
+
if (arg && arg !== 'refresh')
|
|
515
|
+
return ui.system(t('m.usageMemory'), 'warn');
|
|
516
|
+
if (arg === 'refresh') {
|
|
517
|
+
ui.system(t('memory.indexing'), 'info');
|
|
518
|
+
void ctl.refreshProjectContext().then(() => {
|
|
519
|
+
const memory = ctl.projectContextStatus();
|
|
520
|
+
ui.system(t('m.memoryStatus', {
|
|
521
|
+
status: memory.status,
|
|
522
|
+
date: memory.generatedAt ?? '—',
|
|
523
|
+
model: memory.model ?? '—',
|
|
524
|
+
tokens: memory.tokensIn + memory.tokensOut,
|
|
525
|
+
cost: memory.cost === null ? '—' : memory.cost.toFixed(4),
|
|
526
|
+
}), memory.status === 'ready' ? 'ok' : 'warn');
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const memory = ctl.projectContextStatus();
|
|
531
|
+
const index = ctl.projectIndexStatus();
|
|
532
|
+
ui.system(t('m.memoryStatus', {
|
|
533
|
+
status: memory.status,
|
|
534
|
+
date: memory.generatedAt ?? '—',
|
|
535
|
+
model: memory.model ?? '—',
|
|
536
|
+
tokens: memory.tokensIn + memory.tokensOut,
|
|
537
|
+
cost: memory.cost === null ? '—' : memory.cost.toFixed(4),
|
|
538
|
+
}) + ` · index ${index.files} files/${index.symbols} symbols`, memory.status === 'ready' ? 'ok' : memory.status === 'indexing' ? 'info' : 'warn');
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
498
541
|
case '/status': {
|
|
499
542
|
const p = ctl.sessionProvider();
|
|
500
543
|
const agents = [...ctl.board.agents.values()];
|
|
@@ -502,8 +545,22 @@ export function executeInput(raw, ctl, ui, images) {
|
|
|
502
545
|
const cost = agents.reduce((s, a) => s + (a.cost ?? 0), 0);
|
|
503
546
|
const changed = new Set(ctl.board.changes.map((c) => c.path)).size;
|
|
504
547
|
const pm = p ? `${p.name}:${ctl.session.model}` : '-';
|
|
548
|
+
const memory = ctl.projectContextStatus();
|
|
549
|
+
const profiles = agents.reduce((counts, agent) => {
|
|
550
|
+
counts[agent.profile] = (counts[agent.profile] ?? 0) + 1;
|
|
551
|
+
return counts;
|
|
552
|
+
}, {});
|
|
505
553
|
// Multiline: each metric on its own line for readability.
|
|
506
|
-
ui.system(t('m.status', {
|
|
554
|
+
ui.system(t('m.status', {
|
|
555
|
+
pm,
|
|
556
|
+
approval: ctl.session.approvalMode,
|
|
557
|
+
total: agents.length,
|
|
558
|
+
active,
|
|
559
|
+
changed,
|
|
560
|
+
cost: cost.toFixed(3),
|
|
561
|
+
memory: memory.status,
|
|
562
|
+
memoryCost: memory.cost === null ? '—' : memory.cost.toFixed(4),
|
|
563
|
+
}) + `\nProfiles: ${Object.entries(profiles).map(([profile, count]) => `${profile}=${count}`).join(', ') || 'none'}`, 'info');
|
|
507
564
|
return;
|
|
508
565
|
}
|
|
509
566
|
case '/raw':
|
package/dist/controller.js
CHANGED
|
@@ -5,11 +5,14 @@ import { exec, execFileSync, spawn } from 'node:child_process';
|
|
|
5
5
|
import { Blackboard } from './coordination/blackboard.js';
|
|
6
6
|
import { LLMClient } from './llm/client.js';
|
|
7
7
|
import { Agent } from './agents/agent.js';
|
|
8
|
-
import { configFile, saveConfig, getProvider, upsertProvider } from './config.js';
|
|
9
|
-
import { priceFor, fmtCost } from './pricing.js';
|
|
8
|
+
import { configFile, saveConfig, getProvider, providerReady, upsertProvider } from './config.js';
|
|
9
|
+
import { priceFor, fmtCost, costOf } from './pricing.js';
|
|
10
10
|
import { loadSkills, loadSpecialists } from './skills.js';
|
|
11
11
|
import { t } from './i18n.js';
|
|
12
12
|
import { chmodPrivateTree, ensurePrivateDir, sanitizeForPersistence, writeFileAtomicPrivate } from './security.js';
|
|
13
|
+
import { ProjectContextStore, } from './project-context.js';
|
|
14
|
+
import { ProjectIndex } from './project-index.js';
|
|
15
|
+
import { classifyExecutionProfile, EXECUTION_BUDGETS } from './agents/execution-policy.js';
|
|
13
16
|
const AGENT_COLORS = ['#f3e7c7', 'magenta', 'yellow', 'green', 'whiteBright', 'redBright', '#c8bfa6', 'magentaBright'];
|
|
14
17
|
export function normalizeShellApprovalMode(mode) {
|
|
15
18
|
if (mode === 'ask' || mode === 'auto-safe' || mode === 'yolo')
|
|
@@ -85,6 +88,8 @@ export class Controller extends EventEmitter {
|
|
|
85
88
|
config;
|
|
86
89
|
projectRoot;
|
|
87
90
|
board;
|
|
91
|
+
projectContext;
|
|
92
|
+
projectIndex;
|
|
88
93
|
agents = new Map();
|
|
89
94
|
approvals = [];
|
|
90
95
|
questions = [];
|
|
@@ -106,11 +111,25 @@ export class Controller extends EventEmitter {
|
|
|
106
111
|
sessionOnlyProvider = null;
|
|
107
112
|
sessionRetentionDays = 30;
|
|
108
113
|
sessionRetentionMax = 30;
|
|
114
|
+
restoredSessionContext = '';
|
|
109
115
|
constructor(config, projectRoot) {
|
|
110
116
|
super();
|
|
111
117
|
this.config = config;
|
|
112
118
|
this.projectRoot = projectRoot;
|
|
113
119
|
this.board = new Blackboard(projectRoot);
|
|
120
|
+
this.projectContext = new ProjectContextStore(projectRoot, (status, detail) => {
|
|
121
|
+
const text = status === 'indexing'
|
|
122
|
+
? t('memory.indexing')
|
|
123
|
+
: status === 'ready'
|
|
124
|
+
? t('memory.ready')
|
|
125
|
+
: status === 'fallback'
|
|
126
|
+
? t('memory.fallback', { detail: detail ?? '' })
|
|
127
|
+
: '';
|
|
128
|
+
if (text)
|
|
129
|
+
this.board.log('', 'memory', text);
|
|
130
|
+
this.emit('update');
|
|
131
|
+
});
|
|
132
|
+
this.projectIndex = new ProjectIndex(projectRoot);
|
|
114
133
|
const p = getProvider(config);
|
|
115
134
|
this.session = {
|
|
116
135
|
providerName: p?.name ?? '',
|
|
@@ -122,6 +141,10 @@ export class Controller extends EventEmitter {
|
|
|
122
141
|
this.board.on('agent-event', (ev) => this.onAgentEvent(ev));
|
|
123
142
|
this.board.on('note', (note) => this.nudgeFromNote(note));
|
|
124
143
|
this.hardenPrivateState();
|
|
144
|
+
queueMicrotask(() => {
|
|
145
|
+
this.projectIndex.refresh();
|
|
146
|
+
void this.prewarmProjectContext();
|
|
147
|
+
});
|
|
125
148
|
// Autosave: the session (+ conversations, written live by agents) survives a crash.
|
|
126
149
|
const autosave = setInterval(() => this.saveSession(), 30_000);
|
|
127
150
|
autosave.unref();
|
|
@@ -165,6 +188,69 @@ export class Controller extends EventEmitter {
|
|
|
165
188
|
}
|
|
166
189
|
return c;
|
|
167
190
|
}
|
|
191
|
+
projectContextGenerator() {
|
|
192
|
+
const provider = this.sessionProvider();
|
|
193
|
+
const model = this.session.model || provider?.defaultModel || provider?.models[0] || '';
|
|
194
|
+
if (!provider || !model || !providerReady(provider))
|
|
195
|
+
return undefined;
|
|
196
|
+
const llm = this.llmFor(provider, model);
|
|
197
|
+
const price = priceFor(provider, model);
|
|
198
|
+
return async (messages) => {
|
|
199
|
+
const result = await llm.chat(messages);
|
|
200
|
+
return {
|
|
201
|
+
content: result.message.content ?? '',
|
|
202
|
+
tokensIn: result.tokensIn,
|
|
203
|
+
tokensOut: result.tokensOut,
|
|
204
|
+
model,
|
|
205
|
+
cost: price ? costOf(price, result.tokensIn, result.tokensOut) : null,
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
prewarmProjectContext(force = false) {
|
|
210
|
+
return this.projectContext.refresh(this.projectContextGenerator(), force);
|
|
211
|
+
}
|
|
212
|
+
refreshProjectContext() {
|
|
213
|
+
this.projectIndex.refresh();
|
|
214
|
+
return this.prewarmProjectContext(true);
|
|
215
|
+
}
|
|
216
|
+
projectContextStatus() {
|
|
217
|
+
return this.projectContext.statusSnapshot();
|
|
218
|
+
}
|
|
219
|
+
projectIndexStatus() {
|
|
220
|
+
return this.projectIndex.status();
|
|
221
|
+
}
|
|
222
|
+
async projectContextBootstrap(task = '') {
|
|
223
|
+
const agents = [...this.board.agents.values()]
|
|
224
|
+
.map((agent) => `- ${agent.name} [${agent.state}] ${agent.task}` +
|
|
225
|
+
(agent.currentAction ? ` | current: ${agent.currentAction}` : '') +
|
|
226
|
+
(agent.lastResult ? ` | result: ${agent.lastResult.slice(0, 600)}` : ''))
|
|
227
|
+
.join('\n');
|
|
228
|
+
const notes = this.board.notes
|
|
229
|
+
.slice(-20)
|
|
230
|
+
.map((note) => `- ${note.from} → ${note.to}: ${note.content.slice(0, 500)}`)
|
|
231
|
+
.join('\n');
|
|
232
|
+
const changes = this.board.changes
|
|
233
|
+
.slice(-20)
|
|
234
|
+
.map((change) => `- ${change.agentName}: ${change.path}`)
|
|
235
|
+
.join('\n');
|
|
236
|
+
const preSpawnContext = `PRE-SPAWN SESSION CONTEXT
|
|
237
|
+
Agents:
|
|
238
|
+
${agents || '- none'}
|
|
239
|
+
Recent notes:
|
|
240
|
+
${notes || '- none'}
|
|
241
|
+
Changes made before this agent started:
|
|
242
|
+
${changes || '- none'}`;
|
|
243
|
+
const shared = this.projectContext.snapshot();
|
|
244
|
+
const targeted = this.projectIndex.retrieve(task);
|
|
245
|
+
return [
|
|
246
|
+
shared,
|
|
247
|
+
targeted,
|
|
248
|
+
preSpawnContext,
|
|
249
|
+
this.restoredSessionContext ? `RESTORED SESSION CONTEXT\n${this.restoredSessionContext}` : '',
|
|
250
|
+
]
|
|
251
|
+
.filter(Boolean)
|
|
252
|
+
.join('\n\n');
|
|
253
|
+
}
|
|
168
254
|
// ---------- sound cues (terminal bell) ----------
|
|
169
255
|
onAgentEvent(ev) {
|
|
170
256
|
if (ev.type === 'conflict' && ev.path) {
|
|
@@ -207,6 +293,10 @@ export class Controller extends EventEmitter {
|
|
|
207
293
|
nudgeFromNote(note) {
|
|
208
294
|
if (note.to === 'user' || note.from === 'system')
|
|
209
295
|
return;
|
|
296
|
+
// User steering is urgent. Ordinary team notes are consumed as a batch on
|
|
297
|
+
// the next natural turn so they do not discard an in-flight model call.
|
|
298
|
+
if (note.from !== 'user' && !/\b(urgent|blocker|blocked|conflict|collision)\b/i.test(note.content))
|
|
299
|
+
return;
|
|
210
300
|
const recipients = note.to === 'all'
|
|
211
301
|
? [...this.board.agents.values()].filter((a) => a.name.toLowerCase() !== note.from.toLowerCase())
|
|
212
302
|
: [this.board.getAgentByName(note.to)].filter((a) => Boolean(a));
|
|
@@ -335,7 +425,7 @@ export class Controller extends EventEmitter {
|
|
|
335
425
|
return out;
|
|
336
426
|
}
|
|
337
427
|
/** Launch agent N+1 — works at any time, even while others are running. */
|
|
338
|
-
spawnAgent(task, name, modelSpec, images, specialistName, initialHistory, mode = 'task') {
|
|
428
|
+
spawnAgent(task, name, modelSpec, images, specialistName, initialHistory, mode = 'task', forcedProfile) {
|
|
339
429
|
// Specialist persona: role appended to the system prompt, may pin a model.
|
|
340
430
|
let specialist;
|
|
341
431
|
if (specialistName) {
|
|
@@ -358,6 +448,8 @@ export class Controller extends EventEmitter {
|
|
|
358
448
|
// A custom name keeps its alias, so the agent stays addressable both ways.
|
|
359
449
|
const alias = `a${this.agentSeq}`;
|
|
360
450
|
const agentName = name?.trim() || alias;
|
|
451
|
+
const profile = classifyExecutionProfile(task, mode, forcedProfile);
|
|
452
|
+
const budget = EXECUTION_BUDGETS[profile];
|
|
361
453
|
const color = AGENT_COLORS[(this.agentSeq - 1) % AGENT_COLORS.length];
|
|
362
454
|
// Conversation file (JSONL, appended live) — enables /restore after a save.
|
|
363
455
|
let historyFile;
|
|
@@ -376,11 +468,13 @@ export class Controller extends EventEmitter {
|
|
|
376
468
|
color,
|
|
377
469
|
task,
|
|
378
470
|
mode,
|
|
471
|
+
profile,
|
|
379
472
|
model: resolved.model,
|
|
380
473
|
llm: this.llmFor(resolved.provider, resolved.model),
|
|
381
474
|
board: this.board,
|
|
382
475
|
projectRoot: this.projectRoot,
|
|
383
476
|
maxSteps: this.config.maxStepsPerAgent,
|
|
477
|
+
budget,
|
|
384
478
|
requestApproval: this.requestApproval,
|
|
385
479
|
requestQuestion: this.requestQuestion,
|
|
386
480
|
images,
|
|
@@ -390,6 +484,27 @@ export class Controller extends EventEmitter {
|
|
|
390
484
|
projectMemory: this.projectMemory(),
|
|
391
485
|
historyFile,
|
|
392
486
|
initialHistory,
|
|
487
|
+
projectContext: this.projectContextBootstrap(task),
|
|
488
|
+
onInspect: (agentId, relPath, content) => {
|
|
489
|
+
this.projectContext.recordInspection(agentId, relPath, content);
|
|
490
|
+
const current = this.board.agents.get(agentId);
|
|
491
|
+
if (current) {
|
|
492
|
+
current.inspectedFiles = this.projectContext.inspectedFiles(agentId);
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
onComplete: (agentId, summary) => {
|
|
496
|
+
const info = this.board.agents.get(agentId);
|
|
497
|
+
if (!info)
|
|
498
|
+
return;
|
|
499
|
+
const changedFiles = [...new Set(this.board.changes.filter((change) => change.agentId === agentId).map((change) => change.path))];
|
|
500
|
+
this.projectContext.recordOutcome(agentId, {
|
|
501
|
+
agentName: info.name,
|
|
502
|
+
task: info.task,
|
|
503
|
+
result: summary,
|
|
504
|
+
changedFiles,
|
|
505
|
+
});
|
|
506
|
+
this.projectIndex.refresh();
|
|
507
|
+
},
|
|
393
508
|
});
|
|
394
509
|
if (historyFile)
|
|
395
510
|
this.conversationFiles.set(id, historyFile);
|
|
@@ -479,7 +594,7 @@ export class Controller extends EventEmitter {
|
|
|
479
594
|
if (history.length === 0)
|
|
480
595
|
return 'no-conversation';
|
|
481
596
|
const modelSpec = sa.model ? (sa.providerName ? `${sa.providerName}:${sa.model}` : sa.model) : undefined;
|
|
482
|
-
return this.spawnAgent(sa.task, sa.name, modelSpec, undefined, sa.specialist, history, sa.mode ?? 'task');
|
|
597
|
+
return this.spawnAgent(sa.task, sa.name, modelSpec, undefined, sa.specialist, history, sa.mode ?? 'task', sa.profile);
|
|
483
598
|
}
|
|
484
599
|
pauseAgent(name) {
|
|
485
600
|
const a = this.findAgent(name);
|
|
@@ -728,8 +843,10 @@ export class Controller extends EventEmitter {
|
|
|
728
843
|
alias: a.alias,
|
|
729
844
|
task: a.task,
|
|
730
845
|
mode: a.mode,
|
|
846
|
+
profile: a.profile,
|
|
731
847
|
state: a.state,
|
|
732
848
|
lastResult: a.lastResult,
|
|
849
|
+
startedAt: a.startedAt,
|
|
733
850
|
steps: a.steps,
|
|
734
851
|
tokensIn: a.tokensIn,
|
|
735
852
|
tokensOut: a.tokensOut,
|
|
@@ -742,6 +859,7 @@ export class Controller extends EventEmitter {
|
|
|
742
859
|
ctxPct: a.ctxPct,
|
|
743
860
|
progressSteps: a.progressSteps,
|
|
744
861
|
perf: a.perf,
|
|
862
|
+
inspectedFiles: a.inspectedFiles,
|
|
745
863
|
conversation: this.conversationFiles.get(a.id),
|
|
746
864
|
})),
|
|
747
865
|
notes: this.board.notes.slice(-200),
|
|
@@ -749,6 +867,12 @@ export class Controller extends EventEmitter {
|
|
|
749
867
|
fileActivity: [...this.board.fileActivity.values()].slice(-100),
|
|
750
868
|
workMapWarnings: this.board.workMapWarnings.slice(-80),
|
|
751
869
|
changedFiles: [...new Set(this.board.changes.map((c) => c.path))],
|
|
870
|
+
projectContext: {
|
|
871
|
+
schemaVersion: 1,
|
|
872
|
+
generatedAt: this.projectContextStatus().generatedAt,
|
|
873
|
+
fingerprint: this.projectContextStatus().fingerprint,
|
|
874
|
+
status: this.projectContextStatus().status,
|
|
875
|
+
},
|
|
752
876
|
};
|
|
753
877
|
const file = path.join(dir, `session-${this.sessionStamp}.json`);
|
|
754
878
|
writeFileAtomicPrivate(file, sanitizeForPersistence(JSON.stringify(data, null, 2)));
|
|
@@ -783,13 +907,17 @@ export class Controller extends EventEmitter {
|
|
|
783
907
|
if (data.name)
|
|
784
908
|
this.sessionName = data.name;
|
|
785
909
|
const tasks = data.agents.map((a) => `${a.name} [${a.state}] : ${a.task}${a.lastResult ? ` → ${a.lastResult}` : ''}`);
|
|
910
|
+
this.restoredSessionContext = `Previous session saved at ${data.savedAt}.
|
|
911
|
+
Past agents:
|
|
912
|
+
${tasks.join('\n')}
|
|
913
|
+
Files changed then: ${(data.changedFiles ?? []).join(', ') || '(none)'}`;
|
|
786
914
|
this.board.hydrate({
|
|
787
915
|
notes: data.notes ?? [],
|
|
788
916
|
changes: data.changes ?? [],
|
|
789
917
|
fileActivity: data.fileActivity ?? [],
|
|
790
918
|
workMapWarnings: data.workMapWarnings ?? [],
|
|
791
919
|
});
|
|
792
|
-
this.board.addNote('system', 'all',
|
|
920
|
+
this.board.addNote('system', 'all', this.restoredSessionContext);
|
|
793
921
|
this.board.log('', 'system', t('m.sessionRestored', { date: new Date(data.savedAt).toLocaleString() }));
|
|
794
922
|
// Financial history: per-agent cost/steps/tokens of the restored session.
|
|
795
923
|
const withCost = data.agents.filter((a) => a.cost !== undefined || a.tokensIn !== undefined);
|
|
@@ -809,6 +937,7 @@ export class Controller extends EventEmitter {
|
|
|
809
937
|
this.session.providerName = r.provider.name;
|
|
810
938
|
this.session.model = r.model;
|
|
811
939
|
this.emit('update');
|
|
940
|
+
void this.prewarmProjectContext();
|
|
812
941
|
return { provider: r.provider.name, model: r.model };
|
|
813
942
|
}
|
|
814
943
|
setSessionProvider(name) {
|
|
@@ -819,6 +948,7 @@ export class Controller extends EventEmitter {
|
|
|
819
948
|
this.session.providerName = p.name;
|
|
820
949
|
this.session.model = p.defaultModel || p.models[0] || '';
|
|
821
950
|
this.emit('update');
|
|
951
|
+
void this.prewarmProjectContext();
|
|
822
952
|
return true;
|
|
823
953
|
}
|
|
824
954
|
setSessionProviderConfig(p) {
|
|
@@ -827,6 +957,7 @@ export class Controller extends EventEmitter {
|
|
|
827
957
|
this.session.model = p.defaultModel || p.models[0] || '';
|
|
828
958
|
this.llmCache.clear();
|
|
829
959
|
this.emit('update');
|
|
960
|
+
void this.prewarmProjectContext();
|
|
830
961
|
}
|
|
831
962
|
setSessionApprovalMode(mode) {
|
|
832
963
|
this.session.approvalMode = mode;
|