brl-subagent 2.3.8 → 2.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/AGENT.md +18 -6
- package/README.md +49 -10
- package/package.json +11 -9
- package/presets/test-engineer.md +2 -0
- package/src/concurrency.ts +1 -0
- package/src/diff.ts +1 -0
- package/src/event-bus.ts +1 -0
- package/src/git.ts +16 -0
- package/src/history.ts +18 -0
- package/src/index.ts +423 -678
- package/src/logging.ts +21 -1
- package/src/messaging.ts +1 -0
- package/src/metrics.ts +1 -0
- package/src/model-availability.ts +1 -0
- package/src/notify-completion.ts +1 -0
- package/src/params.ts +32 -6
- package/src/paths.ts +1 -0
- package/src/preflight.ts +1 -0
- package/src/prelude.ts +207 -0
- package/src/presets.ts +1 -0
- package/src/prompt.ts +2 -1
- package/src/reports.ts +64 -8
- package/src/router.ts +27 -5
- package/src/runner.ts +8 -1
- package/src/sanitize.ts +1 -0
- package/src/scheduler.ts +1 -0
- package/src/schema.ts +254 -0
- package/src/session-manager.ts +54 -101
- package/src/state.ts +1 -0
- package/src/templates.ts +1 -0
- package/src/transcript-tail.ts +1 -0
- package/src/transcript.ts +1 -0
- package/src/tui-format.ts +1 -0
- package/src/tui.ts +3 -208
- package/src/types.ts +34 -2
- package/src/unit-run.ts +5 -0
- package/src/validate.ts +127 -12
- package/templates/code-review.md +1 -1
- package/templates/debug-issue.md +2 -1
- package/templates/write-tests.md +1 -1
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Purpose: Entry point: tool/command registration, the delegate_task handlers, and the execution-mode runners.
|
|
1
2
|
/**
|
|
2
3
|
* brl-subagent — multi-agent orchestration for pi.
|
|
3
4
|
* Entry point: tool registrations, command surface, and the multi-mode runners.
|
|
@@ -29,6 +30,7 @@ import type {
|
|
|
29
30
|
GraphDetails,
|
|
30
31
|
GraphWave,
|
|
31
32
|
DelegateTaskDetails,
|
|
33
|
+
DelegateTaskParams,
|
|
32
34
|
Priority,
|
|
33
35
|
TaskTemplate,
|
|
34
36
|
ToolResult,
|
|
@@ -65,10 +67,12 @@ import {
|
|
|
65
67
|
mergeWorkBranch,
|
|
66
68
|
} from "./git";
|
|
67
69
|
import { preflightCheck } from "./preflight";
|
|
70
|
+
import { gateSessionCost, rejectApprovalAlwaysInBackground, runPreTaskValidation, validateDelegationTargets, formatValidationWarnings } from "./prelude";
|
|
68
71
|
import { loadBuiltinPresets, loadCustomPresets, getAllPresets, writePresetFile, formatPresetRestriction, formatToolRestriction } from "./presets";
|
|
69
72
|
import { modelIsAvailable } from "./model-availability";
|
|
70
|
-
import { validatePreTask, diagnoseFailure } from "./validate";
|
|
73
|
+
import { validatePreTask, diagnoseFailure, DEFAULT_BACKGROUND_DEADLINE_MS } from "./validate";
|
|
71
74
|
import { findUnknownParams, KNOWN_DELEGATE_KEYS, resolveSubagentParams, snapshotOriginalParams } from "./params";
|
|
75
|
+
import { delegateTaskParamsSchema } from "./schema";
|
|
72
76
|
import { createSessionState } from "./state";
|
|
73
77
|
import { makeLiveOnUpdate, createUnitRun, finalizeUnitRun, finalizeUnitRunCrash, pruneHistoryIfNeeded, registerLiveRun } from "./unit-run";
|
|
74
78
|
import { buildSubagentPrompt, describePromptMode } from "./prompt";
|
|
@@ -99,6 +103,7 @@ import {
|
|
|
99
103
|
showRunHistory,
|
|
100
104
|
showMonitor,
|
|
101
105
|
showDashboard,
|
|
106
|
+
showComplianceMenu,
|
|
102
107
|
showRetryMenu,
|
|
103
108
|
renderDelegateCall,
|
|
104
109
|
renderDelegateResult,
|
|
@@ -118,35 +123,6 @@ import {
|
|
|
118
123
|
// Helpers
|
|
119
124
|
// ---------------------------------------------------------------------------
|
|
120
125
|
|
|
121
|
-
/**
|
|
122
|
-
* Sanitize a text preview for markdown-rendered notifications: strip code
|
|
123
|
-
* fences (``` and ~~~) and backticks, keep whole non-empty lines with
|
|
124
|
-
* mid-line truncation for over-long lines, and never split surrogate pairs
|
|
125
|
-
* at the boundary.
|
|
126
|
-
*/
|
|
127
|
-
function sanitizePreview(text: string, maxLen = 500): string {
|
|
128
|
-
// Strip markdown fences and backticks
|
|
129
|
-
const cleaned = text.replace(/```/g, '').replace(/~~~+/g, '').replace(/`/g, '');
|
|
130
|
-
const lines = cleaned.split('\n').filter(l => l.trim());
|
|
131
|
-
let out = '';
|
|
132
|
-
for (const line of lines) {
|
|
133
|
-
if (out.length + line.length + 1 > maxLen) {
|
|
134
|
-
// Mid-line truncation: keep as much of this line as fits
|
|
135
|
-
const remaining = maxLen - out.length - 1;
|
|
136
|
-
if (remaining > 10) {
|
|
137
|
-
out += line.slice(0, remaining - 3) + '...\n';
|
|
138
|
-
}
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
out += line + '\n';
|
|
142
|
-
}
|
|
143
|
-
// Avoid splitting surrogate pairs at the boundary (reachable now via slice above)
|
|
144
|
-
const safe = out.slice(0, maxLen);
|
|
145
|
-
const lastChar = safe[safe.length - 1];
|
|
146
|
-
const stripped = lastChar && /[\uD800-\uDBFF]/.test(lastChar) ? safe.slice(0, -1) : safe;
|
|
147
|
-
return stripped.trimEnd();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
126
|
/**
|
|
151
127
|
* DRY helper (issue #154 review): shape an inventory of items into a single
|
|
152
128
|
* comma-delimited summary line for the LLM-facing delegation guidance. Both
|
|
@@ -191,6 +167,17 @@ export function buildTemplateGuideline(templateSummary: string): string {
|
|
|
191
167
|
// Extension entry point
|
|
192
168
|
// ---------------------------------------------------------------------------
|
|
193
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Compile-time coverage (issue #229 item 4): fails to compile if
|
|
172
|
+
* DelegateTaskParams omits any KNOWN_DELEGATE_KEYS entry. Runtime drift is
|
|
173
|
+
* invisible (the schema passes the key through, the handler ignores it), so
|
|
174
|
+
* only the compiler can catch it. Together with per-step-model.test.ts's
|
|
175
|
+
* runtime ratchet this closes the chain: registered schema <-> KNOWN_DELEGATE_KEYS <-> DelegateTaskParams.
|
|
176
|
+
*/
|
|
177
|
+
type _KnownDelegateKey = (typeof KNOWN_DELEGATE_KEYS extends Set<infer K> ? K : never) & string;
|
|
178
|
+
type _ExpectNever<T extends never> = T;
|
|
179
|
+
type _DelegateParamCoverage = _ExpectNever<Exclude<_KnownDelegateKey, keyof DelegateTaskParams>>;
|
|
180
|
+
|
|
194
181
|
export default function (pi: ExtensionAPI) {
|
|
195
182
|
const log = createLogger("brl-subagent");
|
|
196
183
|
|
|
@@ -399,49 +386,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
399
386
|
};
|
|
400
387
|
}
|
|
401
388
|
|
|
389
|
+
// Per-unit display name for validation prefixes: the label when present,
|
|
390
|
+
// else the task truncated to 60 chars. Shared by the foreground parallel
|
|
391
|
+
// pre-pass and the background fan-out pre-pass (issue #220) so a unit
|
|
392
|
+
// renders identically in either shape.
|
|
393
|
+
const displayTaskName = (merged: ReturnType<typeof mergeSubTaskParams>): string =>
|
|
394
|
+
merged.label ??
|
|
395
|
+
(merged.task.length > 60 ? `${merged.task.slice(0, 57)}...` : merged.task);
|
|
396
|
+
|
|
402
397
|
// -------------------------------------------------------------------
|
|
403
398
|
// P1: runChainMode
|
|
404
399
|
// -------------------------------------------------------------------
|
|
405
400
|
|
|
406
401
|
async function runChainMode(
|
|
407
|
-
params:
|
|
402
|
+
params: DelegateTaskParams,
|
|
408
403
|
signal: AbortSignal | undefined,
|
|
409
404
|
onUpdate: AgentToolUpdateCallback<DelegateTaskDetails> | undefined,
|
|
410
405
|
ctx: ExtensionContext,
|
|
411
406
|
): Promise<ToolResult<SubagentResult | ChainDetails | undefined>> {
|
|
412
|
-
const chainSteps = params.chain
|
|
407
|
+
const chainSteps: SubTaskParams[] = params.chain!;
|
|
413
408
|
|
|
414
409
|
// Issue #133: dispatch-start timestamp — the aggregate run entry's
|
|
415
410
|
// startedAt (captured once, at mode entry, not at completion).
|
|
416
411
|
const chainModeStartedAt = new Date().toISOString();
|
|
417
412
|
|
|
418
413
|
// R5: Check session cost limit before spawning
|
|
419
|
-
const
|
|
420
|
-
state
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
currentTotal,
|
|
428
|
-
estimatedCost: perTaskEstimate * chainSteps.length,
|
|
429
|
-
limit,
|
|
430
|
-
});
|
|
431
|
-
return {
|
|
432
|
-
content: [
|
|
433
|
-
{
|
|
434
|
-
type: "text" as const,
|
|
435
|
-
text:
|
|
436
|
-
`Cannot delegate: session cost limit reached ` +
|
|
437
|
-
`($${currentTotal.toFixed(4)} spent of $${limit.toFixed(2)} limit). ` +
|
|
438
|
-
`Increase the limit via /brl-subagent costlimit or set to 0 for unlimited.`,
|
|
439
|
-
},
|
|
440
|
-
],
|
|
441
|
-
details: undefined,
|
|
442
|
-
isError: true,
|
|
443
|
-
};
|
|
444
|
-
}
|
|
414
|
+
const costError = gateSessionCost({
|
|
415
|
+
state,
|
|
416
|
+
ctx,
|
|
417
|
+
log,
|
|
418
|
+
units: chainSteps.length,
|
|
419
|
+
label: "Chain delegation",
|
|
420
|
+
});
|
|
421
|
+
if (costError) return costError;
|
|
445
422
|
|
|
446
423
|
// Reject delegation if recursion depth exceeds configured max
|
|
447
424
|
const currentDepth = getCurrentDepth();
|
|
@@ -521,7 +498,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
521
498
|
// #32). Chain top-level tasks are empty (modeCount forbids task+chain), so
|
|
522
499
|
// keyword warnings skip on empty text and only the hard outputFile-vs-write
|
|
523
500
|
// conflict applies (issue #34). The mode-level outputFile on globalParams is
|
|
524
|
-
// the one validated;
|
|
501
|
+
// the one validated here; every step's own cwd/outputFile/tools are
|
|
502
|
+
// validated by the per-step pre-pass below (issue #222).
|
|
525
503
|
const validation = validatePreTask({
|
|
526
504
|
task: globalParams.task,
|
|
527
505
|
toolOptions: globalParams.toolOptions,
|
|
@@ -542,6 +520,47 @@ export default function (pi: ExtensionAPI) {
|
|
|
542
520
|
};
|
|
543
521
|
}
|
|
544
522
|
|
|
523
|
+
// Per-step pre-pass (issue #222): resolve + validate EVERY step before
|
|
524
|
+
// any spawn, so a single bad step rejects the chain with nothing
|
|
525
|
+
// started — the same all-before-any-spawn semantics the background
|
|
526
|
+
// fan-out and the parallel `tasks` shape use. Chain steps are fully
|
|
527
|
+
// known up front and a step's cwd/outputFile never depend on a previous
|
|
528
|
+
// step's output (only its TASK text consumes {previous}), so the pass
|
|
529
|
+
// can run up front — and must, because discovering step 5's bad cwd
|
|
530
|
+
// after four steps already ran would waste their whole budget.
|
|
531
|
+
const stepCwds: string[] = [];
|
|
532
|
+
const stepWarnings: string[] = [];
|
|
533
|
+
for (let i = 0; i < chainSteps.length; i++) {
|
|
534
|
+
const merged = mergeSubTaskParams(globalParams, chainSteps[i]);
|
|
535
|
+
const stepPrefix = `Step ${i + 1} ("${displayTaskName(merged)}")`;
|
|
536
|
+
|
|
537
|
+
const stepTargets = validateDelegationTargets({
|
|
538
|
+
ctx,
|
|
539
|
+
effectiveCwd: merged.effectiveCwd,
|
|
540
|
+
outputFile: merged.outputFile,
|
|
541
|
+
prefix: stepPrefix,
|
|
542
|
+
});
|
|
543
|
+
if (!stepTargets.ok) return stepTargets.error;
|
|
544
|
+
stepCwds.push(stepTargets.cwd);
|
|
545
|
+
|
|
546
|
+
const stepValidation = runPreTaskValidation({
|
|
547
|
+
log,
|
|
548
|
+
label: "Chain",
|
|
549
|
+
prefix: stepPrefix,
|
|
550
|
+
logContext: { step: i + 1 },
|
|
551
|
+
preTask: {
|
|
552
|
+
task: merged.task,
|
|
553
|
+
toolOptions: merged.toolOptions,
|
|
554
|
+
thinkingLevel: merged.thinkingLevel,
|
|
555
|
+
gitMode: globalParams.resolvedGitMode,
|
|
556
|
+
outputFile: merged.outputFile,
|
|
557
|
+
force: params.force as boolean | undefined,
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
if (stepValidation.error) return stepValidation.error;
|
|
561
|
+
for (const w of stepValidation.warnings) stepWarnings.push(`${stepPrefix}: ${w}`);
|
|
562
|
+
}
|
|
563
|
+
|
|
545
564
|
// Resolve model once (per-call top-level model override beats preset)
|
|
546
565
|
const modelResult = resolveSubagentModel(ctx, globalParams.resolvedPreset, params.model as string | undefined);
|
|
547
566
|
if (!modelResult.ok) return modelResult.error;
|
|
@@ -706,7 +725,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
706
725
|
let result: SubagentResult;
|
|
707
726
|
try {
|
|
708
727
|
result = await runSubagent(
|
|
709
|
-
|
|
728
|
+
// Issue #222: this step's OWN validated cwd (the pre-pass
|
|
729
|
+
// resolved it against ctx.cwd) — the mode-level
|
|
730
|
+
// resolvedCwd is the fallback for steps that declare none.
|
|
731
|
+
stepCwds[i],
|
|
710
732
|
subagentPrompt,
|
|
711
733
|
stepModel,
|
|
712
734
|
merged.thinkingLevel,
|
|
@@ -724,7 +746,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
724
746
|
// run entry as failed — crashOutput comes from the live entry's
|
|
725
747
|
// last streamed output (the crash result itself has no
|
|
726
748
|
// messages), and errorCategory is classified onto originalParams.
|
|
727
|
-
finalizeUnitRunCrash(state, pi, run, err, "Chain step",
|
|
749
|
+
finalizeUnitRunCrash(state, pi, run, err, "Chain step", stepCwds[i], log, {
|
|
728
750
|
step: i + 1,
|
|
729
751
|
});
|
|
730
752
|
}
|
|
@@ -869,9 +891,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
869
891
|
};
|
|
870
892
|
state.persistRun(pi, chainAggregateRun);
|
|
871
893
|
|
|
894
|
+
// Per-step validation warnings ride the result, prefixed with their
|
|
895
|
+
// step (issue #222), after the JSON payload — empty renders nothing so
|
|
896
|
+
// warning-free results stay byte-identical.
|
|
872
897
|
return {
|
|
873
898
|
content: [
|
|
874
|
-
{
|
|
899
|
+
{
|
|
900
|
+
type: "text" as const,
|
|
901
|
+
text: JSON.stringify(chainDetails, null, 2) + formatValidationWarnings(stepWarnings),
|
|
902
|
+
},
|
|
875
903
|
],
|
|
876
904
|
details: chainDetails,
|
|
877
905
|
};
|
|
@@ -895,40 +923,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
895
923
|
// -------------------------------------------------------------------
|
|
896
924
|
|
|
897
925
|
async function runParallelMode(
|
|
898
|
-
params:
|
|
926
|
+
params: DelegateTaskParams,
|
|
899
927
|
signal: AbortSignal | undefined,
|
|
900
928
|
onUpdate: AgentToolUpdateCallback<DelegateTaskDetails> | undefined,
|
|
901
929
|
ctx: ExtensionContext,
|
|
902
930
|
): Promise<ToolResult<SubagentResult | ParallelDetails | undefined>> {
|
|
903
|
-
const taskList = params.tasks
|
|
931
|
+
const taskList: SubTaskParams[] = params.tasks!;
|
|
904
932
|
|
|
905
933
|
// R5: Check session cost limit before spawning
|
|
906
|
-
const
|
|
907
|
-
state
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
currentTotal,
|
|
915
|
-
estimatedCost: perTaskEstimate * taskList.length,
|
|
916
|
-
limit,
|
|
917
|
-
});
|
|
918
|
-
return {
|
|
919
|
-
content: [
|
|
920
|
-
{
|
|
921
|
-
type: "text" as const,
|
|
922
|
-
text:
|
|
923
|
-
`Cannot delegate: session cost limit reached ` +
|
|
924
|
-
`($${currentTotal.toFixed(4)} spent of $${limit.toFixed(2)} limit). ` +
|
|
925
|
-
`Increase the limit via /brl-subagent costlimit or set to 0 for unlimited.`,
|
|
926
|
-
},
|
|
927
|
-
],
|
|
928
|
-
details: undefined,
|
|
929
|
-
isError: true,
|
|
930
|
-
};
|
|
931
|
-
}
|
|
934
|
+
const costError = gateSessionCost({
|
|
935
|
+
state,
|
|
936
|
+
ctx,
|
|
937
|
+
log,
|
|
938
|
+
units: taskList.length,
|
|
939
|
+
label: "Parallel delegation",
|
|
940
|
+
});
|
|
941
|
+
if (costError) return costError;
|
|
932
942
|
|
|
933
943
|
// Reject delegation if recursion depth exceeds configured max
|
|
934
944
|
const currentDepth = getCurrentDepth();
|
|
@@ -975,17 +985,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
975
985
|
);
|
|
976
986
|
|
|
977
987
|
// Validate CWD once
|
|
978
|
-
const
|
|
979
|
-
if (!
|
|
980
|
-
|
|
981
|
-
content: [
|
|
982
|
-
{ type: "text" as const, text: `Invalid cwd: ${cwdResult.error}` },
|
|
983
|
-
],
|
|
984
|
-
details: undefined,
|
|
985
|
-
isError: true,
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
|
-
const resolvedCwd = cwdResult.value;
|
|
988
|
+
const targets = validateDelegationTargets({ ctx, effectiveCwd: globalParams.effectiveCwd });
|
|
989
|
+
if (!targets.ok) return targets.error;
|
|
990
|
+
const resolvedCwd = targets.cwd;
|
|
989
991
|
|
|
990
992
|
// Pre-flight checks — fail fast before consuming resources
|
|
991
993
|
const pfResult = preflightCheck(resolvedCwd);
|
|
@@ -1008,26 +1010,64 @@ export default function (pi: ExtensionAPI) {
|
|
|
1008
1010
|
// #32). Parallel top-level tasks are empty (modeCount forbids task+tasks),
|
|
1009
1011
|
// so keyword warnings skip on empty text and only the hard
|
|
1010
1012
|
// outputFile-vs-write conflict applies (issue #34). The mode-level
|
|
1011
|
-
// outputFile on globalParams is the one validated;
|
|
1012
|
-
// are
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1013
|
+
// outputFile on globalParams is the one validated here; every task's own
|
|
1014
|
+
// cwd/outputFile/tools are validated by the per-task pre-pass below
|
|
1015
|
+
// (issue #222).
|
|
1016
|
+
const preTask = runPreTaskValidation({
|
|
1017
|
+
log,
|
|
1018
|
+
label: "Parallel",
|
|
1019
|
+
preTask: {
|
|
1020
|
+
task: globalParams.task,
|
|
1021
|
+
toolOptions: globalParams.toolOptions,
|
|
1022
|
+
thinkingLevel: globalParams.thinkingLevel,
|
|
1023
|
+
gitMode: globalParams.resolvedGitMode,
|
|
1024
|
+
outputFile: globalParams.outputFile,
|
|
1025
|
+
},
|
|
1019
1026
|
});
|
|
1020
|
-
if (
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1027
|
+
if (preTask.error) return preTask.error;
|
|
1028
|
+
|
|
1029
|
+
// Per-task pre-pass (issue #220, extended by #222): validate EVERY task
|
|
1030
|
+
// before any spawn, so a single bad task rejects the batch with nothing
|
|
1031
|
+
// started (`force` degrades capability errors to warnings) — the same
|
|
1032
|
+
// semantics runBackgroundFanOut applies. Each task's OWN cwd is resolved
|
|
1033
|
+
// and validated here (with its outputFile, resolved against that cwd) and
|
|
1034
|
+
// the resolved value is what that task spawns in — issue #222 closed the
|
|
1035
|
+
// gap where a per-task `cwd` was merged but the spawn still used the
|
|
1036
|
+
// mode-level cwd. Per-task warnings are prefixed with their unit and
|
|
1037
|
+
// collected here to ride the final result. The mode-entry validation
|
|
1038
|
+
// above stays for the mode-level outputFile (issue #34).
|
|
1039
|
+
const taskWarnings: string[] = [];
|
|
1040
|
+
// Parallel index → the task's own validated cwd (issue #222).
|
|
1041
|
+
const taskCwds: string[] = [];
|
|
1042
|
+
for (let i = 0; i < taskList.length; i++) {
|
|
1043
|
+
const merged = mergeSubTaskParams(globalParams, taskList[i]);
|
|
1044
|
+
const taskPrefix = `Task ${i + 1} ("${displayTaskName(merged)}")`;
|
|
1045
|
+
|
|
1046
|
+
const taskTargets = validateDelegationTargets({
|
|
1047
|
+
ctx,
|
|
1048
|
+
effectiveCwd: merged.effectiveCwd,
|
|
1049
|
+
outputFile: merged.outputFile,
|
|
1050
|
+
prefix: taskPrefix,
|
|
1051
|
+
});
|
|
1052
|
+
if (!taskTargets.ok) return taskTargets.error;
|
|
1053
|
+
taskCwds.push(taskTargets.cwd);
|
|
1054
|
+
|
|
1055
|
+
const unitValidation = runPreTaskValidation({
|
|
1056
|
+
log,
|
|
1057
|
+
label: "Parallel",
|
|
1058
|
+
prefix: taskPrefix,
|
|
1059
|
+
logContext: { task: i + 1 },
|
|
1060
|
+
preTask: {
|
|
1061
|
+
task: merged.task,
|
|
1062
|
+
toolOptions: merged.toolOptions,
|
|
1063
|
+
thinkingLevel: merged.thinkingLevel,
|
|
1064
|
+
gitMode: globalParams.resolvedGitMode,
|
|
1065
|
+
outputFile: merged.outputFile,
|
|
1066
|
+
force: params.force as boolean | undefined,
|
|
1067
|
+
},
|
|
1068
|
+
});
|
|
1069
|
+
if (unitValidation.error) return unitValidation.error;
|
|
1070
|
+
for (const w of unitValidation.warnings) taskWarnings.push(`${taskPrefix}: ${w}`);
|
|
1031
1071
|
}
|
|
1032
1072
|
|
|
1033
1073
|
// Resolve model once (per-call top-level model override beats preset)
|
|
@@ -1078,6 +1118,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1078
1118
|
const runTask = async (
|
|
1079
1119
|
index: number,
|
|
1080
1120
|
merged: ReturnType<typeof mergeSubTaskParams>,
|
|
1121
|
+
unitCwd: string,
|
|
1081
1122
|
): Promise<boolean> => {
|
|
1082
1123
|
|
|
1083
1124
|
// C3: Resolve this step's model override (step.model > global resolved model)
|
|
@@ -1154,7 +1195,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1154
1195
|
// the live ghost (grace logic treats a 'running' record as live).
|
|
1155
1196
|
try {
|
|
1156
1197
|
const result = await runSubagent(
|
|
1157
|
-
|
|
1198
|
+
// Issue #222: this task's OWN validated cwd.
|
|
1199
|
+
unitCwd,
|
|
1158
1200
|
subagentPrompt,
|
|
1159
1201
|
stepModel,
|
|
1160
1202
|
merged.thinkingLevel,
|
|
@@ -1197,7 +1239,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1197
1239
|
// the live drill-in loops forever. finalizeLiveSubagent is
|
|
1198
1240
|
// idempotent; the rethrow keeps Promise.allSettled semantics (the
|
|
1199
1241
|
// caller's finally still releases the concurrency slot).
|
|
1200
|
-
finalizeUnitRunCrash(state, pi, run, err, "Parallel subtask",
|
|
1242
|
+
finalizeUnitRunCrash(state, pi, run, err, "Parallel subtask", unitCwd, log, {
|
|
1201
1243
|
index,
|
|
1202
1244
|
});
|
|
1203
1245
|
}
|
|
@@ -1275,7 +1317,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1275
1317
|
}
|
|
1276
1318
|
let taskSuccess = false;
|
|
1277
1319
|
try {
|
|
1278
|
-
taskSuccess = await runTask(index, merged);
|
|
1320
|
+
taskSuccess = await runTask(index, merged, taskCwds[index]);
|
|
1279
1321
|
} finally {
|
|
1280
1322
|
// Issue #137: release with the unit's real success — the status
|
|
1281
1323
|
// bar's "N failed" count reflects the true outcome.
|
|
@@ -1332,9 +1374,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1332
1374
|
totalCost,
|
|
1333
1375
|
});
|
|
1334
1376
|
|
|
1377
|
+
// C: per-task validation warnings ride the result, prefixed with their
|
|
1378
|
+
// task (issue #220), after the JSON payload — empty renders nothing so
|
|
1379
|
+
// warning-free results stay byte-identical.
|
|
1335
1380
|
return {
|
|
1336
1381
|
content: [
|
|
1337
|
-
{
|
|
1382
|
+
{
|
|
1383
|
+
type: "text" as const,
|
|
1384
|
+
text: JSON.stringify(parallelDetails, null, 2) + formatValidationWarnings(taskWarnings),
|
|
1385
|
+
},
|
|
1338
1386
|
],
|
|
1339
1387
|
details: parallelDetails,
|
|
1340
1388
|
};
|
|
@@ -1345,12 +1393,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1345
1393
|
// -------------------------------------------------------------------
|
|
1346
1394
|
|
|
1347
1395
|
async function runGraphMode(
|
|
1348
|
-
params:
|
|
1396
|
+
params: DelegateTaskParams,
|
|
1349
1397
|
signal: AbortSignal | undefined,
|
|
1350
1398
|
onUpdate: AgentToolUpdateCallback<DelegateTaskDetails> | undefined,
|
|
1351
1399
|
ctx: ExtensionContext,
|
|
1352
1400
|
): Promise<ToolResult<SubagentResult | GraphDetails | undefined>> {
|
|
1353
|
-
const graphTasks = params.graph
|
|
1401
|
+
const graphTasks: GraphTask[] = params.graph!;
|
|
1354
1402
|
|
|
1355
1403
|
// Issue #133: dispatch-start timestamp — the aggregate run entry's
|
|
1356
1404
|
// startedAt (captured once, at mode entry, not at completion).
|
|
@@ -1372,32 +1420,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1372
1420
|
}
|
|
1373
1421
|
|
|
1374
1422
|
// R5: Check session cost limit before spawning
|
|
1375
|
-
const
|
|
1376
|
-
state
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
currentTotal,
|
|
1384
|
-
estimatedCost: perTaskEstimate * graphTasks.length,
|
|
1385
|
-
limit,
|
|
1386
|
-
});
|
|
1387
|
-
return {
|
|
1388
|
-
content: [
|
|
1389
|
-
{
|
|
1390
|
-
type: "text" as const,
|
|
1391
|
-
text:
|
|
1392
|
-
`Cannot delegate: session cost limit reached ` +
|
|
1393
|
-
`($${currentTotal.toFixed(4)} spent of $${limit.toFixed(2)} limit). ` +
|
|
1394
|
-
`Increase the limit via /brl-subagent costlimit or set to 0 for unlimited.`,
|
|
1395
|
-
},
|
|
1396
|
-
],
|
|
1397
|
-
details: undefined,
|
|
1398
|
-
isError: true,
|
|
1399
|
-
};
|
|
1400
|
-
}
|
|
1423
|
+
const costError = gateSessionCost({
|
|
1424
|
+
state,
|
|
1425
|
+
ctx,
|
|
1426
|
+
log,
|
|
1427
|
+
units: graphTasks.length,
|
|
1428
|
+
label: "Graph delegation",
|
|
1429
|
+
});
|
|
1430
|
+
if (costError) return costError;
|
|
1401
1431
|
|
|
1402
1432
|
// Reject delegation if recursion depth exceeds configured max
|
|
1403
1433
|
const currentDepth = getCurrentDepth();
|
|
@@ -1478,8 +1508,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1478
1508
|
// #32). Graph top-level tasks are empty (modeCount forbids task+graph),
|
|
1479
1509
|
// so keyword warnings skip on empty text and only the hard
|
|
1480
1510
|
// outputFile-vs-write conflict applies (issue #34). The mode-level
|
|
1481
|
-
// outputFile on globalParams is the one validated;
|
|
1482
|
-
// are
|
|
1511
|
+
// outputFile on globalParams is the one validated here; every node's own
|
|
1512
|
+
// cwd/outputFile/tools are validated by the per-node pre-pass below
|
|
1513
|
+
// (issue #222).
|
|
1483
1514
|
const validation = validatePreTask({
|
|
1484
1515
|
task: globalParams.task,
|
|
1485
1516
|
toolOptions: globalParams.toolOptions,
|
|
@@ -1555,6 +1586,73 @@ export default function (pi: ExtensionAPI) {
|
|
|
1555
1586
|
}
|
|
1556
1587
|
const waves = sortResult.waves;
|
|
1557
1588
|
|
|
1589
|
+
// Per-node pre-pass (issue #222): resolve + validate EVERY node before
|
|
1590
|
+
// any spawn, so a single bad node rejects the whole graph with nothing
|
|
1591
|
+
// started — the same all-before-any-spawn semantics the background
|
|
1592
|
+
// fan-out, the parallel `tasks` shape and the chain use. Each node's OWN
|
|
1593
|
+
// cwd is resolved and validated here (with its outputFile, resolved
|
|
1594
|
+
// against that cwd) and the resolved value is what that node spawns in.
|
|
1595
|
+
// The pre-pass runs on the DECLARED task text, before {id} substitution
|
|
1596
|
+
// (the substituted text is only known mid-dispatch, once a dependency's
|
|
1597
|
+
// output exists) — the declared text is validated with the placeholder
|
|
1598
|
+
// TOKENS stripped (a dependency whose id happens to be a blocking
|
|
1599
|
+
// keyword must not make `{<id>}` itself trip the keyword rules), while
|
|
1600
|
+
// the SUBSTITUTED content stays unvalidated by design. The host's own
|
|
1601
|
+
// wording is still validated, and refusing to validate up front would
|
|
1602
|
+
// mean a bad node in the LAST wave is discovered after every earlier
|
|
1603
|
+
// wave already spent its budget.
|
|
1604
|
+
const nodeCwds = new Map<string, string>();
|
|
1605
|
+
const nodeWarnings: string[] = [];
|
|
1606
|
+
for (const graphTask of graphTasks) {
|
|
1607
|
+
const merged = mergeSubTaskParams(globalParams, {
|
|
1608
|
+
task: graphTask.task,
|
|
1609
|
+
label: graphTask.label,
|
|
1610
|
+
model: graphTask.model,
|
|
1611
|
+
thinkingLevel: graphTask.thinkingLevel,
|
|
1612
|
+
priority: graphTask.priority,
|
|
1613
|
+
cwd: graphTask.cwd,
|
|
1614
|
+
timeout: graphTask.timeout,
|
|
1615
|
+
outputFile: graphTask.outputFile,
|
|
1616
|
+
tools: graphTask.tools,
|
|
1617
|
+
excludeTools: graphTask.excludeTools,
|
|
1618
|
+
noBuiltinTools: graphTask.noBuiltinTools,
|
|
1619
|
+
systemPrompt: graphTask.systemPrompt,
|
|
1620
|
+
inheritSystemPrompt: graphTask.inheritSystemPrompt,
|
|
1621
|
+
});
|
|
1622
|
+
const nodePrefix = `Node "${graphTask.id}"`;
|
|
1623
|
+
|
|
1624
|
+
const nodeTargets = validateDelegationTargets({
|
|
1625
|
+
ctx,
|
|
1626
|
+
effectiveCwd: merged.effectiveCwd,
|
|
1627
|
+
outputFile: merged.outputFile,
|
|
1628
|
+
prefix: nodePrefix,
|
|
1629
|
+
});
|
|
1630
|
+
if (!nodeTargets.ok) return nodeTargets.error;
|
|
1631
|
+
nodeCwds.set(graphTask.id, nodeTargets.cwd);
|
|
1632
|
+
|
|
1633
|
+
const nodeValidation = runPreTaskValidation({
|
|
1634
|
+
log,
|
|
1635
|
+
label: "Graph",
|
|
1636
|
+
prefix: nodePrefix,
|
|
1637
|
+
logContext: { id: graphTask.id },
|
|
1638
|
+
preTask: {
|
|
1639
|
+
// Placeholder TOKENS stripped (issue #222 review): the rules
|
|
1640
|
+
// read the declared string, so a `{<id>}` whose dependency id
|
|
1641
|
+
// is a blocking keyword would block on a token, not on text
|
|
1642
|
+
// the host wrote. The declared wording around it is unchanged
|
|
1643
|
+
// and still validated.
|
|
1644
|
+
task: merged.task.replace(GRAPH_OUTPUT_PLACEHOLDER_RE, " "),
|
|
1645
|
+
toolOptions: merged.toolOptions,
|
|
1646
|
+
thinkingLevel: merged.thinkingLevel,
|
|
1647
|
+
gitMode: globalParams.resolvedGitMode,
|
|
1648
|
+
outputFile: merged.outputFile,
|
|
1649
|
+
force: params.force as boolean | undefined,
|
|
1650
|
+
},
|
|
1651
|
+
});
|
|
1652
|
+
if (nodeValidation.error) return nodeValidation.error;
|
|
1653
|
+
for (const w of nodeValidation.warnings) nodeWarnings.push(`${nodePrefix}: ${w}`);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1558
1656
|
// Build base prompt once
|
|
1559
1657
|
const basePrompt = ctx.getSystemPrompt();
|
|
1560
1658
|
|
|
@@ -1626,6 +1724,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1626
1724
|
};
|
|
1627
1725
|
|
|
1628
1726
|
const merged = mergeSubTaskParams(globalParams, subTaskParams);
|
|
1727
|
+
// Issue #222: the node's OWN validated cwd (pre-pass);
|
|
1728
|
+
// resolvedCwd (the mode-level cwd) is the guaranteed
|
|
1729
|
+
// fallback, so a node that declares none still resolves.
|
|
1730
|
+
const nodeCwd = nodeCwds.get(graphTask.id) ?? resolvedCwd;
|
|
1629
1731
|
|
|
1630
1732
|
// C3: Resolve this step's model override (step.model > global resolved model)
|
|
1631
1733
|
const stepModel = resolveStepModel(ctx, merged.model, subagentModel);
|
|
@@ -1709,7 +1811,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1709
1811
|
const stepOnUpdate = makeLiveOnUpdate(state, runId, onUpdate);
|
|
1710
1812
|
|
|
1711
1813
|
const result = await runSubagent(
|
|
1712
|
-
|
|
1814
|
+
nodeCwd,
|
|
1713
1815
|
subagentPrompt,
|
|
1714
1816
|
stepModel,
|
|
1715
1817
|
merged.thinkingLevel,
|
|
@@ -1760,7 +1862,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1760
1862
|
// finalizeLiveSubagent is idempotent; the rethrow keeps
|
|
1761
1863
|
// Promise.allSettled semantics (a failing node must NOT cancel
|
|
1762
1864
|
// sibling nodes in the wave).
|
|
1763
|
-
finalizeUnitRunCrash(state, pi, run, err, "Graph node",
|
|
1865
|
+
finalizeUnitRunCrash(state, pi, run, err, "Graph node", nodeCwd, log, {
|
|
1764
1866
|
id: graphTask.id,
|
|
1765
1867
|
});
|
|
1766
1868
|
} finally {
|
|
@@ -1857,9 +1959,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1857
1959
|
};
|
|
1858
1960
|
state.persistRun(pi, graphAggregateRun);
|
|
1859
1961
|
|
|
1962
|
+
// Per-node validation warnings ride the result, prefixed with their
|
|
1963
|
+
// node id (issue #222), after the JSON payload — empty renders
|
|
1964
|
+
// nothing so warning-free results stay byte-identical.
|
|
1860
1965
|
return {
|
|
1861
1966
|
content: [
|
|
1862
|
-
{
|
|
1967
|
+
{
|
|
1968
|
+
type: "text" as const,
|
|
1969
|
+
text: JSON.stringify(graphDetails, null, 2) + formatValidationWarnings(nodeWarnings),
|
|
1970
|
+
},
|
|
1863
1971
|
],
|
|
1864
1972
|
details: graphDetails,
|
|
1865
1973
|
};
|
|
@@ -1890,39 +1998,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
1890
1998
|
async function runBackgroundFanOut(
|
|
1891
1999
|
pi: ExtensionAPI,
|
|
1892
2000
|
ctx: ExtensionContext,
|
|
1893
|
-
params:
|
|
2001
|
+
params: DelegateTaskParams,
|
|
1894
2002
|
signal: AbortSignal | undefined,
|
|
1895
2003
|
): Promise<ToolResult<SubagentResult | undefined>> {
|
|
1896
|
-
const taskList = params.tasks
|
|
2004
|
+
const taskList: SubTaskParams[] = params.tasks!;
|
|
1897
2005
|
|
|
1898
2006
|
// Session cost limit gates the WHOLE batch before any spawn — same
|
|
1899
2007
|
// estimate/limit logic as runParallelMode, scaled by task count.
|
|
1900
|
-
const
|
|
1901
|
-
state
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
currentTotal,
|
|
1909
|
-
estimatedCost: perTaskEstimate * taskList.length,
|
|
1910
|
-
limit,
|
|
1911
|
-
});
|
|
1912
|
-
return {
|
|
1913
|
-
content: [
|
|
1914
|
-
{
|
|
1915
|
-
type: "text" as const,
|
|
1916
|
-
text:
|
|
1917
|
-
`Cannot delegate: session cost limit reached ` +
|
|
1918
|
-
`($${currentTotal.toFixed(4)} spent of $${limit.toFixed(2)} limit). ` +
|
|
1919
|
-
`Increase the limit via /brl-subagent costlimit or set to 0 for unlimited.`,
|
|
1920
|
-
},
|
|
1921
|
-
],
|
|
1922
|
-
details: undefined,
|
|
1923
|
-
isError: true,
|
|
1924
|
-
};
|
|
1925
|
-
}
|
|
2008
|
+
const costError = gateSessionCost({
|
|
2009
|
+
state,
|
|
2010
|
+
ctx,
|
|
2011
|
+
log,
|
|
2012
|
+
units: taskList.length,
|
|
2013
|
+
label: "Background fan-out",
|
|
2014
|
+
});
|
|
2015
|
+
if (costError) return costError;
|
|
1926
2016
|
|
|
1927
2017
|
// Resolve global params once — same call shape as runParallelMode.
|
|
1928
2018
|
const globalParams = resolveSubagentParams(
|
|
@@ -1958,16 +2048,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1958
2048
|
// Same rejection as the single-background path: approvalMode 'always'
|
|
1959
2049
|
// cannot work in background — there is no interactive dialog to approve
|
|
1960
2050
|
// the diff, for any of the N agents.
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
content: [{ type: "text" as const, text:
|
|
1964
|
-
`Cannot spawn background agent with approvalMode 'always': background agents run unattended ` +
|
|
1965
|
-
`and cannot present the approval dialog. Use approvalMode 'auto' (default) or 'writes'.`
|
|
1966
|
-
}],
|
|
1967
|
-
details: undefined,
|
|
1968
|
-
isError: true,
|
|
1969
|
-
};
|
|
1970
|
-
}
|
|
2051
|
+
const approvalError = rejectApprovalAlwaysInBackground(globalParams.resolvedApprovalMode);
|
|
2052
|
+
if (approvalError) return approvalError;
|
|
1971
2053
|
// W5 parity with the single-background path: 'writes' silently
|
|
1972
2054
|
// auto-approves in background — warn once so the caller knows none of
|
|
1973
2055
|
// the N agents' diffs will be gated on approval.
|
|
@@ -1981,9 +2063,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1981
2063
|
// git lock is awaited inside the first spawn and held until that agent
|
|
1982
2064
|
// settles, so same-repository branch-mode spawns would serialize and
|
|
1983
2065
|
// block this call for the whole chain. The rule is blanket (not
|
|
1984
|
-
//
|
|
1985
|
-
// repository root
|
|
1986
|
-
// tree
|
|
2066
|
+
// per-repository-checked) because the lock is keyed by the resolved
|
|
2067
|
+
// repository root: units in different directories of one repo share the
|
|
2068
|
+
// working tree and contend for the same lock.
|
|
1987
2069
|
if (globalParams.resolvedGitMode === 'branch') {
|
|
1988
2070
|
return {
|
|
1989
2071
|
content: [{ type: "text" as const, text:
|
|
@@ -2006,11 +2088,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2006
2088
|
cwd: string;
|
|
2007
2089
|
outputFile: string | undefined;
|
|
2008
2090
|
};
|
|
2009
|
-
const displayTaskName = (merged: ReturnType<typeof mergeSubTaskParams>): string =>
|
|
2010
|
-
merged.label ??
|
|
2011
|
-
(merged.task.length > 60 ? `${merged.task.slice(0, 57)}...` : merged.task);
|
|
2012
2091
|
|
|
2013
2092
|
const fanOutTasks: FanOutTask[] = [];
|
|
2093
|
+
// C: per-task warnings collect here to ride the immediate spawn result.
|
|
2094
|
+
const taskWarnings: string[] = [];
|
|
2014
2095
|
for (let i = 0; i < taskList.length; i++) {
|
|
2015
2096
|
const merged = mergeSubTaskParams(globalParams, taskList[i]);
|
|
2016
2097
|
const name = displayTaskName(merged);
|
|
@@ -2018,48 +2099,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
2018
2099
|
// Per-task model override wins; the global model is the fallback.
|
|
2019
2100
|
const stepModel = resolveStepModel(ctx, merged.model, globalModel);
|
|
2020
2101
|
|
|
2021
|
-
const
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
content: [{ type: "text" as const, text: `${taskPrefix}: Invalid cwd: ${cwdResult.error}` }],
|
|
2025
|
-
details: undefined,
|
|
2026
|
-
isError: true,
|
|
2027
|
-
};
|
|
2028
|
-
}
|
|
2029
|
-
let resolvedOutputFile: string | undefined;
|
|
2030
|
-
if (merged.outputFile) {
|
|
2031
|
-
const ofResult = validateOutputFile(merged.outputFile, cwdResult.value);
|
|
2032
|
-
if (!ofResult.ok) {
|
|
2033
|
-
return {
|
|
2034
|
-
content: [{ type: "text" as const, text: `${taskPrefix}: Invalid outputFile: ${ofResult.error}` }],
|
|
2035
|
-
details: undefined,
|
|
2036
|
-
isError: true,
|
|
2037
|
-
};
|
|
2038
|
-
}
|
|
2039
|
-
resolvedOutputFile = ofResult.value;
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
const validation = validatePreTask({
|
|
2043
|
-
task: merged.task,
|
|
2044
|
-
toolOptions: merged.toolOptions,
|
|
2045
|
-
thinkingLevel: merged.thinkingLevel,
|
|
2046
|
-
gitMode: globalParams.resolvedGitMode,
|
|
2102
|
+
const targets = validateDelegationTargets({
|
|
2103
|
+
ctx,
|
|
2104
|
+
effectiveCwd: merged.effectiveCwd,
|
|
2047
2105
|
outputFile: merged.outputFile,
|
|
2106
|
+
prefix: taskPrefix,
|
|
2048
2107
|
});
|
|
2049
|
-
if (
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2108
|
+
if (!targets.ok) return targets.error;
|
|
2109
|
+
|
|
2110
|
+
const preTask = runPreTaskValidation({
|
|
2111
|
+
log,
|
|
2112
|
+
label: "Background fan-out",
|
|
2113
|
+
prefix: taskPrefix,
|
|
2114
|
+
logContext: { task: i + 1 },
|
|
2115
|
+
preTask: {
|
|
2116
|
+
task: merged.task,
|
|
2117
|
+
toolOptions: merged.toolOptions,
|
|
2118
|
+
thinkingLevel: merged.thinkingLevel,
|
|
2119
|
+
gitMode: globalParams.resolvedGitMode,
|
|
2120
|
+
outputFile: merged.outputFile,
|
|
2121
|
+
force: params.force as boolean | undefined,
|
|
2122
|
+
},
|
|
2123
|
+
});
|
|
2124
|
+
if (preTask.error) return preTask.error;
|
|
2125
|
+
for (const w of preTask.warnings) taskWarnings.push(`${taskPrefix}: ${w}`);
|
|
2061
2126
|
|
|
2062
|
-
fanOutTasks.push({ merged, model: stepModel, cwd:
|
|
2127
|
+
fanOutTasks.push({ merged, model: stepModel, cwd: targets.cwd, outputFile: targets.outputFile });
|
|
2063
2128
|
}
|
|
2064
2129
|
|
|
2065
2130
|
// Spawn pass — sequential, in task order, through the shared tail.
|
|
@@ -2112,7 +2177,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
2112
2177
|
timeout: merged.timeout,
|
|
2113
2178
|
gitMode: globalParams.resolvedGitMode,
|
|
2114
2179
|
// Retry parity with createUnitRun: snapshot this unit's resolved
|
|
2115
|
-
// values so a retry restores them.
|
|
2180
|
+
// values so a retry restores them. The execution-shape fields
|
|
2181
|
+
// (background/gitMode/approvalMode/force) are deliberately NOT
|
|
2182
|
+
// passed here — a fan-out unit is spec'd to retry as a single
|
|
2183
|
+
// run, so it must not inherit this spawn's background-ness.
|
|
2184
|
+
// Same omission in unit-run.ts's createUnitRun.
|
|
2116
2185
|
originalParams: snapshotOriginalParams({
|
|
2117
2186
|
systemPrompt: merged.customSP,
|
|
2118
2187
|
inheritSystemPrompt: merged.inheritSP,
|
|
@@ -2149,7 +2218,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2149
2218
|
started.push({ id: agent.id, label: merged.label });
|
|
2150
2219
|
}
|
|
2151
2220
|
|
|
2152
|
-
// Text-only result: ids in task order + the wake hint.
|
|
2221
|
+
// Text-only result: ids in task order + the wake hint. C: per-task
|
|
2222
|
+
// validation warnings ride the immediate SPAWN result (prefixed with
|
|
2223
|
+
// their task) so the conductor can stop/re-dispatch in the same turn.
|
|
2153
2224
|
const lines = started
|
|
2154
2225
|
.map((s, i) => `${i + 1}. ${s.label ? `"${s.label}" ` : ""}— ${s.id}`)
|
|
2155
2226
|
.join("\n");
|
|
@@ -2157,7 +2228,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2157
2228
|
content: [{ type: "text" as const, text:
|
|
2158
2229
|
`Background agents started: ${started.length}\n\n` +
|
|
2159
2230
|
`${lines}\n\n` +
|
|
2160
|
-
`You'll be woken with a completion message as each finishes; use get_subagent_result({ agent_id }) for retrieval and stall checks.`
|
|
2231
|
+
`You'll be woken with a completion message as each finishes; use get_subagent_result({ agent_id }) for retrieval and stall checks.` +
|
|
2232
|
+
formatValidationWarnings(taskWarnings)
|
|
2161
2233
|
}],
|
|
2162
2234
|
details: undefined,
|
|
2163
2235
|
};
|
|
@@ -2167,10 +2239,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2167
2239
|
// startBackgroundAgent — the shared background-spawn tail.
|
|
2168
2240
|
// Used by BOTH spawnBackgroundRun (single task) and runBackgroundFanOut
|
|
2169
2241
|
// (tasks fan-out): the session-manager import, the spawn call, live-monitor
|
|
2170
|
-
// registration, footer counters, the 2s progress poller and the
|
|
2171
|
-
// timer live here exactly once. Callers keep their own prelude,
|
|
2172
|
-
// validations and result formatting. THROWS on spawn failure —
|
|
2173
|
-
// translate the error into their own result shape.
|
|
2242
|
+
// registration, footer counters, the 2s progress poller and the default
|
|
2243
|
+
// deadline timer live here exactly once. Callers keep their own prelude,
|
|
2244
|
+
// checks, validations and result formatting. THROWS on spawn failure —
|
|
2245
|
+
// callers translate the error into their own result shape.
|
|
2174
2246
|
// -------------------------------------------------------------------
|
|
2175
2247
|
|
|
2176
2248
|
async function startBackgroundAgent(
|
|
@@ -2326,7 +2398,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2326
2398
|
}, { deliverAs: "followUp" });
|
|
2327
2399
|
} else if (agent.status === 'stopped') {
|
|
2328
2400
|
// User-initiated stop (stop_subagent) or deadline abort
|
|
2329
|
-
// (timeout/
|
|
2401
|
+
// (timeout/deadline) — not a failure.
|
|
2330
2402
|
updateProgressStatus(state, ctx);
|
|
2331
2403
|
} else {
|
|
2332
2404
|
state.completedSubagents++;
|
|
@@ -2361,20 +2433,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
2361
2433
|
}
|
|
2362
2434
|
}, 2000);
|
|
2363
2435
|
|
|
2364
|
-
//
|
|
2365
|
-
// W2 (issue #28): previously this only stopped the poller —
|
|
2366
|
-
// session kept running forever (orphaned). Now it pre-sets
|
|
2367
|
-
// 'stopped' (so the .then keeps it, per W1) and aborts the
|
|
2368
|
-
//
|
|
2369
|
-
|
|
2370
|
-
|
|
2436
|
+
// Default deadline: stop polling AND abort the session after the
|
|
2437
|
+
// deadline. W2 (issue #28): previously this only stopped the poller —
|
|
2438
|
+
// the pi session kept running forever (orphaned). Now it pre-sets
|
|
2439
|
+
// 'stopped' (so the .then keeps it, per W1) and aborts the session.
|
|
2440
|
+
// Issue #240: an explicit per-agent timeout is HONORED verbatim; this
|
|
2441
|
+
// deadline DEFAULTS to 30m only when no timeout is given. It is a
|
|
2442
|
+
// default, not a ceiling. Issue #244: exactly ONE deadline timer per
|
|
2443
|
+
// run — an explicit timeout is owned by the session-manager W3 timer,
|
|
2444
|
+
// so this index timer arms ONLY on the no-timeout path, mirroring
|
|
2445
|
+
// spawnBackgroundSession's `params.timeout && params.timeout > 0` guard
|
|
2446
|
+
// (with no timeout, it owns the 30m default, #28).
|
|
2447
|
+
const hasExplicitDeadline = spawn.timeout !== undefined && spawn.timeout > 0;
|
|
2448
|
+
const hardCapMs = spawn.timeout ?? DEFAULT_BACKGROUND_DEADLINE_MS;
|
|
2449
|
+
const hardCapHandle: ReturnType<typeof setTimeout> | undefined =
|
|
2450
|
+
hasExplicitDeadline ? undefined : setTimeout(() => {
|
|
2371
2451
|
if (!completed && !agent.completedAt) {
|
|
2372
2452
|
try {
|
|
2373
2453
|
completed = true;
|
|
2374
2454
|
clearInterval(pollInterval);
|
|
2375
2455
|
// Pre-set stopped BEFORE aborting so the .then in
|
|
2376
2456
|
// spawnBackgroundSession keeps the stopped state.
|
|
2377
|
-
updateAgentStatus(agent.id, 'stopped', `Timed out (${hardCapMs}ms
|
|
2457
|
+
updateAgentStatus(agent.id, 'stopped', `Timed out (${hardCapMs}ms deadline)`);
|
|
2378
2458
|
agent._sessionRef?.abort().catch(() => {
|
|
2379
2459
|
// Abort may reject if the session is mid-dispose;
|
|
2380
2460
|
// the status flip above is already recorded.
|
|
@@ -2394,7 +2474,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2394
2474
|
// W3/poller stopped path (no completedSubagents increment).
|
|
2395
2475
|
updateProgressStatus(state, ctx);
|
|
2396
2476
|
} catch (err) {
|
|
2397
|
-
// Defensive: never let the
|
|
2477
|
+
// Defensive: never let the deadline timer throw uncaught — that would
|
|
2398
2478
|
// skip finalizeLiveSubagent, the counter decrement, and the notification.
|
|
2399
2479
|
// Do all fallible work first (output capture), then mutate counters,
|
|
2400
2480
|
// then notify — a throw mid-path can't double-fire mutations.
|
|
@@ -2405,7 +2485,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2405
2485
|
// get_subagent_result doesn't report 'running' and the W3 timer
|
|
2406
2486
|
// guard (!agent.completedAt) can't re-fire later.
|
|
2407
2487
|
try {
|
|
2408
|
-
updateAgentStatus(agent.id, 'stopped', `Timed out (${hardCapMs}ms
|
|
2488
|
+
updateAgentStatus(agent.id, 'stopped', `Timed out (${hardCapMs}ms deadline)`);
|
|
2409
2489
|
} catch { /* ignore */ }
|
|
2410
2490
|
try {
|
|
2411
2491
|
setAgentFinalOutput(agent.id, extractAgentFinalOutput(agent));
|
|
@@ -2447,7 +2527,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
2447
2527
|
// Resolve the preset and its model BEFORE the background branch so the
|
|
2448
2528
|
// preset's model (and system prompt) are honored in background mode.
|
|
2449
2529
|
const bgResolved = resolveSubagentParams({ ...params, task: singleTask }, state, ctx, log);
|
|
2450
|
-
const {
|
|
2530
|
+
const {
|
|
2531
|
+
resolvedPreset: bgResolvedPreset,
|
|
2532
|
+
autoRoutedPreset: bgAutoRoutedPreset,
|
|
2533
|
+
autoRouteKeyword: bgAutoRouteKeyword,
|
|
2534
|
+
} = bgResolved;
|
|
2451
2535
|
const bgModelResult = resolveSubagentModel(ctx, bgResolvedPreset, params.model);
|
|
2452
2536
|
const bgModel = bgModelResult.ok
|
|
2453
2537
|
? `${bgModelResult.model.provider}/${bgModelResult.model.id}`
|
|
@@ -2457,16 +2541,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
2457
2541
|
// W5 (issue #28): approvalMode 'always' cannot work in background —
|
|
2458
2542
|
// there is no interactive dialog to approve the diff. Reject loudly
|
|
2459
2543
|
// instead of silently running unattended with write access.
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
content: [{ type: "text" as const, text:
|
|
2463
|
-
`Cannot spawn background agent with approvalMode 'always': background agents run unattended ` +
|
|
2464
|
-
`and cannot present the approval dialog. Use approvalMode 'auto' (default) or 'writes'.`
|
|
2465
|
-
}],
|
|
2466
|
-
details: undefined,
|
|
2467
|
-
isError: true,
|
|
2468
|
-
};
|
|
2469
|
-
}
|
|
2544
|
+
const approvalError = rejectApprovalAlwaysInBackground(bgResolved.resolvedApprovalMode);
|
|
2545
|
+
if (approvalError) return approvalError;
|
|
2470
2546
|
// W5: 'writes' in background silently auto-approves — warn once so the
|
|
2471
2547
|
// caller knows the diff will NOT be gated on approval.
|
|
2472
2548
|
if (bgResolved.resolvedApprovalMode === 'writes') {
|
|
@@ -2478,79 +2554,42 @@ export default function (pi: ExtensionAPI) {
|
|
|
2478
2554
|
// W6 (issue #28): session cost limit must gate background spawns too —
|
|
2479
2555
|
// the R5 check after the background branch never runs for them.
|
|
2480
2556
|
// Same estimate/limit logic as single mode.
|
|
2481
|
-
const
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
estimatedCost: bgPerTaskEstimate,
|
|
2490
|
-
limit: bgLimit,
|
|
2491
|
-
});
|
|
2492
|
-
return {
|
|
2493
|
-
content: [
|
|
2494
|
-
{
|
|
2495
|
-
type: "text" as const,
|
|
2496
|
-
text:
|
|
2497
|
-
`Cannot delegate: session cost limit reached ` +
|
|
2498
|
-
`($${bgCurrentTotal.toFixed(4)} spent of $${bgLimit.toFixed(2)} limit). ` +
|
|
2499
|
-
`Increase the limit via /brl-subagent costlimit or set to 0 for unlimited.`,
|
|
2500
|
-
},
|
|
2501
|
-
],
|
|
2502
|
-
details: undefined,
|
|
2503
|
-
isError: true,
|
|
2504
|
-
};
|
|
2505
|
-
}
|
|
2557
|
+
const costError = gateSessionCost({
|
|
2558
|
+
state,
|
|
2559
|
+
ctx,
|
|
2560
|
+
log,
|
|
2561
|
+
units: 1,
|
|
2562
|
+
label: "Background delegation",
|
|
2563
|
+
});
|
|
2564
|
+
if (costError) return costError;
|
|
2506
2565
|
|
|
2507
2566
|
// F1: Validate cwd + outputFile the same way foreground single mode does —
|
|
2508
2567
|
// an unvalidated outputFile would reach the prompt and could steer the
|
|
2509
2568
|
// background agent's write tool outside the project root.
|
|
2510
|
-
const
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
details: undefined,
|
|
2515
|
-
isError: true,
|
|
2516
|
-
};
|
|
2517
|
-
}
|
|
2518
|
-
let bgResolvedOutputFile: string | undefined;
|
|
2519
|
-
if (bgResolved.outputFile) {
|
|
2520
|
-
const ofResult = validateOutputFile(bgResolved.outputFile, bgCwdResult.value);
|
|
2521
|
-
if (!ofResult.ok) {
|
|
2522
|
-
return {
|
|
2523
|
-
content: [
|
|
2524
|
-
{ type: "text" as const, text: `Invalid outputFile: ${ofResult.error}` },
|
|
2525
|
-
],
|
|
2526
|
-
details: undefined,
|
|
2527
|
-
isError: true,
|
|
2528
|
-
};
|
|
2529
|
-
}
|
|
2530
|
-
bgResolvedOutputFile = ofResult.value;
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
|
-
// C: H1 validation for background mode — reject outputFile-vs-write
|
|
2534
|
-
// conflicts before spawning (loud failure, same as single mode).
|
|
2535
|
-
const bgValidation = validatePreTask({
|
|
2536
|
-
task: singleTask,
|
|
2537
|
-
toolOptions: bgResolved.toolOptions,
|
|
2538
|
-
thinkingLevel: bgResolved.thinkingLevel,
|
|
2539
|
-
gitMode: bgResolved.resolvedGitMode,
|
|
2540
|
-
outputFile: bgResolvedOutputFile,
|
|
2569
|
+
const targets = validateDelegationTargets({
|
|
2570
|
+
ctx,
|
|
2571
|
+
effectiveCwd: bgResolved.effectiveCwd,
|
|
2572
|
+
outputFile: bgResolved.outputFile,
|
|
2541
2573
|
});
|
|
2542
|
-
if (
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2574
|
+
if (!targets.ok) return targets.error;
|
|
2575
|
+
const bgResolvedOutputFile = targets.outputFile;
|
|
2576
|
+
|
|
2577
|
+
// C: H1 validation for background mode — reject capability blocks and
|
|
2578
|
+
// outputFile-vs-write conflicts before spawning (same as single mode;
|
|
2579
|
+
// `force` honors the same override foreground does).
|
|
2580
|
+
const bgPreTask = runPreTaskValidation({
|
|
2581
|
+
log,
|
|
2582
|
+
label: "Background",
|
|
2583
|
+
preTask: {
|
|
2584
|
+
task: singleTask,
|
|
2585
|
+
toolOptions: bgResolved.toolOptions,
|
|
2586
|
+
thinkingLevel: bgResolved.thinkingLevel,
|
|
2587
|
+
gitMode: bgResolved.resolvedGitMode,
|
|
2588
|
+
outputFile: bgResolvedOutputFile,
|
|
2589
|
+
force: params.force,
|
|
2590
|
+
},
|
|
2591
|
+
});
|
|
2592
|
+
if (bgPreTask.error) return bgPreTask.error;
|
|
2554
2593
|
|
|
2555
2594
|
// Build the full prompt the same way foreground single mode does:
|
|
2556
2595
|
// base prompt (optionally inherited) + custom prompt + preset guidance.
|
|
@@ -2571,7 +2610,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2571
2610
|
thinkingLevel: bgResolved.thinkingLevel,
|
|
2572
2611
|
priority: params.priority,
|
|
2573
2612
|
systemPrompt: bgPrompt,
|
|
2574
|
-
cwd:
|
|
2613
|
+
cwd: targets.cwd,
|
|
2575
2614
|
toolOptions: bgResolved.toolOptions,
|
|
2576
2615
|
timeout: bgResolved.timeout,
|
|
2577
2616
|
gitMode: bgResolved.resolvedGitMode,
|
|
@@ -2579,11 +2618,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
2579
2618
|
sanitizeCwd: bgResolved.effectiveCwd,
|
|
2580
2619
|
});
|
|
2581
2620
|
|
|
2582
|
-
// B2: Surface auto-route decisions in background spawn result too
|
|
2583
|
-
//
|
|
2584
|
-
// per-call overrides)
|
|
2621
|
+
// B2: Surface auto-route decisions in background spawn result too —
|
|
2622
|
+
// preset name + matched-keyword evidence + restriction, derived from
|
|
2623
|
+
// the RESOLVED toolOptions (reflects per-call overrides).
|
|
2585
2624
|
const bgAutoRouteNote = bgAutoRoutedPreset
|
|
2586
|
-
? `\n\n[auto-routed to preset '${bgAutoRoutedPreset.name}' — ${bgResolved.toolOptions ? formatToolRestriction(bgResolved.toolOptions) : formatPresetRestriction(bgAutoRoutedPreset)}]`
|
|
2625
|
+
? `\n\n[auto-routed to preset '${bgAutoRoutedPreset.name}'${bgAutoRouteKeyword ? ` (matched keyword '${bgAutoRouteKeyword}')` : ""} — ${bgResolved.toolOptions ? formatToolRestriction(bgResolved.toolOptions) : formatPresetRestriction(bgAutoRoutedPreset)}]`
|
|
2587
2626
|
: "";
|
|
2588
2627
|
|
|
2589
2628
|
return {
|
|
@@ -2594,7 +2633,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2594
2633
|
`Task: ${agent.task}\n` +
|
|
2595
2634
|
`Status: ${agent.status}\n\n` +
|
|
2596
2635
|
`You'll be woken with a completion message when it finishes; use get_subagent_result({ agent_id: "${agent.id}" }) for retrieval and stall checks.` +
|
|
2597
|
-
bgAutoRouteNote
|
|
2636
|
+
bgAutoRouteNote +
|
|
2637
|
+
// C: warnings ride the immediate SPAWN result so the conductor
|
|
2638
|
+
// can stop/re-dispatch in the same turn.
|
|
2639
|
+
formatValidationWarnings(bgPreTask.warnings),
|
|
2598
2640
|
}],
|
|
2599
2641
|
details: undefined,
|
|
2600
2642
|
};
|
|
@@ -2648,6 +2690,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
2648
2690
|
retry: () => showRetryMenu(ctx, state),
|
|
2649
2691
|
sla: () => showSLAConfig(ctx, state, applyConfig),
|
|
2650
2692
|
"sla-stats": () => showSLAStats(ctx, state),
|
|
2693
|
+
// E5: the compliance report menu (file access / secrets exposure /
|
|
2694
|
+
// full summary) shipped in tui.ts but had no handler key, so
|
|
2695
|
+
// `/brl-subagent compliance` fell through to the config menu and the
|
|
2696
|
+
// feature was unreachable (broken wiring, not dead code).
|
|
2697
|
+
compliance: () => showComplianceMenu(ctx, state),
|
|
2651
2698
|
};
|
|
2652
2699
|
|
|
2653
2700
|
if (trimmed && trimmed in handlers) {
|
|
@@ -2707,13 +2754,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
2707
2754
|
"You can customize per-call via inheritSystemPrompt and systemPrompt: set inheritSystemPrompt: false to save context, provide a systemPrompt for custom instructions, or use both to add instructions on top of inheritance.",
|
|
2708
2755
|
"Set thinkingLevel per call to match task complexity. The level is capped at the user's configured maximum. Map tasks to levels using this heuristic: off = file listing, grep, simple read. minimal = file diff, syntax check, find-and-replace. low = refactoring, test generation, documentation. medium = default — code review, debugging, moderate analysis. high = security audit, architecture review, complex debugging. xhigh = multi-step causal reasoning, research, novel problem solving. Default to 'off' or 'minimal' for trivial tasks — do not waste the user's budget.",
|
|
2709
2756
|
"Use outputFile to have the subagent write full findings to disk and return only a structured summary — saves context tokens for large investigations.",
|
|
2710
|
-
"Set timeout (in ms) to limit how long a subagent can run. Useful for tasks that might hang or get stuck.",
|
|
2757
|
+
"Set timeout (in ms) to limit how long a subagent can run. Useful for tasks that might hang or get stuck. Background runs default to a 30-minute deadline when no timeout is given; an explicit timeout is honored as given (30 minutes is a default, not a ceiling).",
|
|
2711
2758
|
"Set cwd to override the subagent's working directory. Defaults to the current project directory.",
|
|
2712
2759
|
"Set label to give the subagent a human-readable name (e.g., 'security-audit' or 'docs-review'). Labels appear in the status bar and tool call display.",
|
|
2713
2760
|
`Use preset to apply a delegation configuration (built-in or custom via /brl-subagent preset). Preset values are defaults — explicit parameters override them. IMPORTANT: some presets restrict tools — e.g. outputFile requires the subagent's write tool, which security-auditor and code-reviewer exclude. Built-in presets: ${presetRestrictionSummary}. Custom presets are NOT listed here — inspect them via /brl-subagent preset before combining with outputFile or tool-dependent work. When combining a preset with outputFile or tool-dependent work, verify the preset allows the required tools.`,
|
|
2714
2761
|
buildTemplateGuideline(templateSummary),
|
|
2715
|
-
"To retry a failed subagent, pass its run ID as retryRunId. The
|
|
2716
|
-
"Set retryOnTimeout: true to automatically retry a subagent that times out. Only retries once — the second timeout is treated as a final failure.",
|
|
2762
|
+
"To retry a failed subagent, pass its run ID as retryRunId. The retry restores the recorded params - including background, gitMode, approvalMode, force - with explicit values on this call winning. Multi-step runs degrade to a single task. See the retryRunId description for the full field list.",
|
|
2763
|
+
"Set retryOnTimeout: true to automatically retry a FOREGROUND subagent that times out. Only retries once — the second timeout is treated as a final failure. Background runs are not auto-retried; re-dispatch with retryRunId.",
|
|
2717
2764
|
"Set background: true to run the subagent in the background without blocking. The tool returns immediately with an agent ID. With tasks, background fans out: every task starts as its own background agent, the call returns one ID per task in task order, and the conductor is woken once per agent as each finishes; chain and graph cannot be combined with background (rejected). Background runs wake the conductor with a structured completion message when they finish — do not poll: polling is only correct when completion notifications are disabled (completionNotify \"off\"); one status check as a stall check is legitimate.",
|
|
2718
2765
|
"",
|
|
2719
2766
|
"## Conductor Guardrails",
|
|
@@ -2723,309 +2770,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
2723
2770
|
"2. **Thinking level**: Match thinking level to task complexity: off/minimal for trivial tasks (file listing, grep), low for refactoring/docs, medium for code review/debugging, high for security audits/complex debugging, xhigh for multi-step reasoning/novel problems.",
|
|
2724
2771
|
"3. **Git mode**: Use gitMode='branch' for tasks that create commits or PRs. Use gitMode='none' for read-only tasks.",
|
|
2725
2772
|
"4. **Tools**: Verify the subagent has the tools it needs. If the task writes files, ensure write and edit are not excluded. If the task runs commands, ensure bash is not excluded.",
|
|
2726
|
-
"5. **Timeout**: Set timeout based on task complexity. Simple: 30s. Medium: 60s. Complex: 120s+. xhigh thinking: at least 120s.",
|
|
2773
|
+
"5. **Timeout**: Set timeout based on task complexity. Simple: 30s. Medium: 60s. Complex: 120s+. xhigh thinking: at least 120s. Background runs default to a 30-minute deadline when no timeout is set; an explicit timeout is honored.",
|
|
2727
2774
|
"",
|
|
2728
|
-
"These guardrails prevent common misconfigurations. The extension also validates configuration before spawning (H1): tool warnings are
|
|
2775
|
+
"These guardrails prevent common misconfigurations. The extension also validates configuration before spawning (H1): tool warnings are surfaced in the returned result, but two mismatches are HARD errors that reject the delegation — outputFile with the write tool excluded, and a capability-critical task/toolset mismatch (a run/execute/test/compile/benchmark task without bash, or an exploration task with none of find/ls/grep/bash; pass force: true to override the capability class). Getting it right the first time is faster and more efficient.",
|
|
2729
2776
|
"",
|
|
2730
2777
|
"Before delegating, evaluate existing presets to find the best match for the task: tech-writer (documentation), code-reviewer (code review), security-auditor (security analysis), test-engineer (test writing), debugger (debugging), refactorer (refactoring), data-analyst (data analysis), rapid-prototyper (quick prototypes). Use the preset parameter to apply the best match. If no preset fits, use dev-agent for general development tasks.",
|
|
2731
2778
|
"The autoRoutePreset() function can automatically select the best preset based on task keywords. Consider using it for preset selection.",
|
|
2732
2779
|
"Prefer a preset when the delegation matches a standard shape — review/audit/test/docs; override per-call deliberately.",
|
|
2733
2780
|
],
|
|
2734
|
-
parameters:
|
|
2735
|
-
task: Type.Optional(Type.String({
|
|
2736
|
-
description: "Detailed description of the task for the subagent to complete (required for single mode, optional for chain/tasks/graph)",
|
|
2737
|
-
})),
|
|
2738
|
-
systemPrompt: Type.Optional(
|
|
2739
|
-
Type.String({
|
|
2740
|
-
description:
|
|
2741
|
-
"Custom system prompt or additional instructions for the subagent. " +
|
|
2742
|
-
"When inheritSystemPrompt is true (default), this is appended after the inherited prompt. " +
|
|
2743
|
-
"When inheritSystemPrompt is false, this replaces the inherited prompt entirely.",
|
|
2744
|
-
}),
|
|
2745
|
-
),
|
|
2746
|
-
inheritSystemPrompt: Type.Optional(
|
|
2747
|
-
Type.Boolean({
|
|
2748
|
-
description:
|
|
2749
|
-
"Whether to inherit the main agent's system prompt. " +
|
|
2750
|
-
"Default: true. Set to false to use only your custom systemPrompt, " +
|
|
2751
|
-
"or to avoid passing a large inherited prompt to the subagent.",
|
|
2752
|
-
}),
|
|
2753
|
-
),
|
|
2754
|
-
thinkingLevel: Type.Optional(
|
|
2755
|
-
Type.String({
|
|
2756
|
-
description:
|
|
2757
|
-
"Requested thinking level for this subagent call. " +
|
|
2758
|
-
"One of: off, minimal, low, medium, high, xhigh. " +
|
|
2759
|
-
"Capped at the user's configured maximum. If omitted, the user's configured level is used.",
|
|
2760
|
-
}),
|
|
2761
|
-
),
|
|
2762
|
-
outputFile: Type.Optional(
|
|
2763
|
-
Type.String({
|
|
2764
|
-
description:
|
|
2765
|
-
"Project-relative path where the subagent should write its full findings. " +
|
|
2766
|
-
"When provided, the subagent is instructed to write complete output to this file " +
|
|
2767
|
-
"and return only a structured summary.",
|
|
2768
|
-
}),
|
|
2769
|
-
),
|
|
2770
|
-
label: Type.Optional(
|
|
2771
|
-
Type.String({
|
|
2772
|
-
description:
|
|
2773
|
-
"Human-readable label for this subagent (e.g., 'security-audit', 'docs-review'). " +
|
|
2774
|
-
"Appears in the status bar and tool call display. " +
|
|
2775
|
-
"Omit to use the default anonymous counter.",
|
|
2776
|
-
}),
|
|
2777
|
-
),
|
|
2778
|
-
model: Type.Optional(Type.String({ description: "Model override (provider/model-id). Defaults to the global subagent model." })),
|
|
2779
|
-
timeout: Type.Optional(
|
|
2780
|
-
Type.Number({
|
|
2781
|
-
description:
|
|
2782
|
-
"Maximum time in milliseconds the subagent is allowed to run. " +
|
|
2783
|
-
"If exceeded, the subagent is killed and an error is returned.",
|
|
2784
|
-
}),
|
|
2785
|
-
),
|
|
2786
|
-
cwd: Type.Optional(
|
|
2787
|
-
Type.String({
|
|
2788
|
-
description:
|
|
2789
|
-
"Working directory for the subagent. Must be an existing directory. " +
|
|
2790
|
-
"Defaults to the conductor's current working directory.",
|
|
2791
|
-
}),
|
|
2792
|
-
),
|
|
2793
|
-
tools: Type.Optional(
|
|
2794
|
-
Type.Array(Type.String(), {
|
|
2795
|
-
description:
|
|
2796
|
-
"Explicit allowlist of tool names for the subagent. Maps to pi's --tools flag.",
|
|
2797
|
-
}),
|
|
2798
|
-
),
|
|
2799
|
-
excludeTools: Type.Optional(
|
|
2800
|
-
Type.Array(Type.String(), {
|
|
2801
|
-
description:
|
|
2802
|
-
"Tool names to disable for the subagent. Maps to pi's --exclude-tools flag.",
|
|
2803
|
-
}),
|
|
2804
|
-
),
|
|
2805
|
-
noBuiltinTools: Type.Optional(
|
|
2806
|
-
Type.Boolean({
|
|
2807
|
-
description:
|
|
2808
|
-
"Disable all built-in tools for the subagent. Maps to pi's --no-builtin-tools flag.",
|
|
2809
|
-
}),
|
|
2810
|
-
),
|
|
2811
|
-
preset: Type.Optional(
|
|
2812
|
-
Type.String({
|
|
2813
|
-
description:
|
|
2814
|
-
"Name of a saved delegation preset (created via /brl-subagent preset). " +
|
|
2815
|
-
"Preset values are used as defaults; explicit parameters on this call override them.",
|
|
2816
|
-
}),
|
|
2817
|
-
),
|
|
2818
|
-
template: Type.Optional(
|
|
2819
|
-
Type.String({
|
|
2820
|
-
description:
|
|
2821
|
-
"Name of a saved task template. Use with params to fill template slots. " +
|
|
2822
|
-
"Templates are file-backed: 9 builtin templates ship with the extension " +
|
|
2823
|
-
"(browse via /brl-subagent templates); override or add via .md files in " +
|
|
2824
|
-
"~/.pi/agent/brl-subagent/templates/ or .pi/brl-subagent/templates/ " +
|
|
2825
|
-
"(project-local wins over user-global over builtin).",
|
|
2826
|
-
}),
|
|
2827
|
-
),
|
|
2828
|
-
params: Type.Optional(
|
|
2829
|
-
Type.Record(Type.String(), Type.String(), {
|
|
2830
|
-
description:
|
|
2831
|
-
"Parameter values for template ${param} slots. " +
|
|
2832
|
-
"Keys are param names, values are the substitution text.",
|
|
2833
|
-
}),
|
|
2834
|
-
),
|
|
2835
|
-
retryRunId: Type.Optional(
|
|
2836
|
-
Type.String({
|
|
2837
|
-
description:
|
|
2838
|
-
"ID of a previously failed subagent run to retry. " +
|
|
2839
|
-
"The retried run uses the same task and parameters as the original. " +
|
|
2840
|
-
"Only works with runs that ended in failure (exitCode != 0, timeout, error, or abort). " +
|
|
2841
|
-
"Explicit parameters on this call override the original's.",
|
|
2842
|
-
}),
|
|
2843
|
-
),
|
|
2844
|
-
gitMode: Type.Optional(
|
|
2845
|
-
Type.String({
|
|
2846
|
-
description:
|
|
2847
|
-
"Git integration mode for this subagent call. " +
|
|
2848
|
-
"'branch' creates a work branch, captures the diff, and switches back. " +
|
|
2849
|
-
"'none' (default) does nothing. Falls back to the configured default.",
|
|
2850
|
-
}),
|
|
2851
|
-
),
|
|
2852
|
-
retryOnTimeout: Type.Optional(
|
|
2853
|
-
Type.Boolean({
|
|
2854
|
-
description:
|
|
2855
|
-
"If true and the subagent times out, automatically retry with the same parameters. " +
|
|
2856
|
-
"Only retries once — the second timeout is treated as a final failure.",
|
|
2857
|
-
}),
|
|
2858
|
-
),
|
|
2859
|
-
approvalMode: Type.Optional(
|
|
2860
|
-
Type.String({
|
|
2861
|
-
description:
|
|
2862
|
-
"Change approval mode: auto (never ask), writes (ask when files changed), " +
|
|
2863
|
-
"always (ask every time). Default is user config (/brl-subagent approval).",
|
|
2864
|
-
}),
|
|
2865
|
-
),
|
|
2866
|
-
background: Type.Optional(
|
|
2867
|
-
Type.Boolean({
|
|
2868
|
-
description:
|
|
2869
|
-
"Run the subagent in the background without blocking the conductor. " +
|
|
2870
|
-
"When true, the tool returns immediately with an agent ID. " +
|
|
2871
|
-
"The conductor is woken with a completion message; use get_subagent_result for post-wake retrieval and stall checks. " +
|
|
2872
|
-
"Supports a single task or the tasks array (a fan-out that starts one background agent per task; with tasks the call returns one ID per task and the conductor is woken once per agent as each finishes); combining background with chain or graph is rejected. " +
|
|
2873
|
-
"Default: false (blocking mode).",
|
|
2874
|
-
}),
|
|
2875
|
-
),
|
|
2876
|
-
priority: Type.Optional(
|
|
2877
|
-
Type.Union([
|
|
2878
|
-
Type.Literal("critical"),
|
|
2879
|
-
Type.Literal("high"),
|
|
2880
|
-
Type.Literal("normal"),
|
|
2881
|
-
Type.Literal("low"),
|
|
2882
|
-
], {
|
|
2883
|
-
description: "Concurrency priority for this delegation: critical, high, normal, or low. Higher-priority delegations queue ahead. Defaults to normal.",
|
|
2884
|
-
})
|
|
2885
|
-
),
|
|
2886
|
-
// Issue #114: NO per-step `priority` on chain[] — chain steps never
|
|
2887
|
-
// compete for concurrency slots (the chain holds ONE slot for its whole
|
|
2888
|
-
// duration); array order IS the priority.
|
|
2889
|
-
chain: Type.Optional(Type.Array(Type.Object({
|
|
2890
|
-
task: Type.String({ description: "Task description. Use {previous} to reference the previous step output." }),
|
|
2891
|
-
label: Type.Optional(Type.String({})),
|
|
2892
|
-
model: Type.Optional(Type.String({ description: "Model override for this step (provider/model-id). Defaults to the global subagent model." })),
|
|
2893
|
-
thinkingLevel: Type.Optional(Type.String({})),
|
|
2894
|
-
cwd: Type.Optional(Type.String({})),
|
|
2895
|
-
timeout: Type.Optional(Type.Number({})),
|
|
2896
|
-
outputFile: Type.Optional(Type.String({})),
|
|
2897
|
-
tools: Type.Optional(Type.Array(Type.String({}))),
|
|
2898
|
-
excludeTools: Type.Optional(Type.Array(Type.String({}))),
|
|
2899
|
-
noBuiltinTools: Type.Optional(Type.Boolean({})),
|
|
2900
|
-
systemPrompt: Type.Optional(Type.String({})),
|
|
2901
|
-
inheritSystemPrompt: Type.Optional(Type.Boolean({})),
|
|
2902
|
-
}), {
|
|
2903
|
-
description: "Sequential chain of tasks. Each step receives the previous step output via {previous} placeholder in the task string. Chain stops at the first failure. Max " + MAX_CHAIN_STEPS + " steps."
|
|
2904
|
-
})),
|
|
2905
|
-
tasks: Type.Optional(Type.Array(Type.Object({
|
|
2906
|
-
task: Type.String({ description: "Task description for this parallel subtask" }),
|
|
2907
|
-
label: Type.Optional(Type.String({})),
|
|
2908
|
-
model: Type.Optional(Type.String({ description: "Model override for this step (provider/model-id). Defaults to the global subagent model." })),
|
|
2909
|
-
thinkingLevel: Type.Optional(Type.String({})),
|
|
2910
|
-
priority: Type.Optional(
|
|
2911
|
-
Type.Union([
|
|
2912
|
-
Type.Literal("critical"),
|
|
2913
|
-
Type.Literal("high"),
|
|
2914
|
-
Type.Literal("normal"),
|
|
2915
|
-
Type.Literal("low"),
|
|
2916
|
-
], { description: "Concurrency priority for this subtask (overrides the call-level default)." })
|
|
2917
|
-
),
|
|
2918
|
-
cwd: Type.Optional(Type.String({})),
|
|
2919
|
-
timeout: Type.Optional(Type.Number({})),
|
|
2920
|
-
outputFile: Type.Optional(Type.String({})),
|
|
2921
|
-
tools: Type.Optional(Type.Array(Type.String({}))),
|
|
2922
|
-
excludeTools: Type.Optional(Type.Array(Type.String({}))),
|
|
2923
|
-
noBuiltinTools: Type.Optional(Type.Boolean({})),
|
|
2924
|
-
systemPrompt: Type.Optional(Type.String({})),
|
|
2925
|
-
inheritSystemPrompt: Type.Optional(Type.Boolean({})),
|
|
2926
|
-
}), {
|
|
2927
|
-
description: "Parallel tasks to execute concurrently. All tasks run regardless of individual failures. Max " + MAX_PARALLEL_TASKS + " tasks."
|
|
2928
|
-
})),
|
|
2929
|
-
graph: Type.Optional(Type.Array(Type.Object({
|
|
2930
|
-
id: Type.String({ description: "Unique identifier for this task node" }),
|
|
2931
|
-
task: Type.String({ description: "Task description. Use {otherId} to reference output from another task." }),
|
|
2932
|
-
label: Type.Optional(Type.String({})),
|
|
2933
|
-
model: Type.Optional(Type.String({ description: "Model override for this step (provider/model-id). Defaults to the global subagent model." })),
|
|
2934
|
-
dependsOn: Type.Optional(Type.Array(Type.String({}), { description: "IDs of tasks that must complete before this one starts" })),
|
|
2935
|
-
thinkingLevel: Type.Optional(Type.String({})),
|
|
2936
|
-
priority: Type.Optional(
|
|
2937
|
-
Type.Union([
|
|
2938
|
-
Type.Literal("critical"),
|
|
2939
|
-
Type.Literal("high"),
|
|
2940
|
-
Type.Literal("normal"),
|
|
2941
|
-
Type.Literal("low"),
|
|
2942
|
-
], { description: "Concurrency priority for this subtask (overrides the call-level default)." })
|
|
2943
|
-
),
|
|
2944
|
-
cwd: Type.Optional(Type.String({})),
|
|
2945
|
-
timeout: Type.Optional(Type.Number({})),
|
|
2946
|
-
outputFile: Type.Optional(Type.String({})),
|
|
2947
|
-
tools: Type.Optional(Type.Array(Type.String({}))),
|
|
2948
|
-
excludeTools: Type.Optional(Type.Array(Type.String({}))),
|
|
2949
|
-
noBuiltinTools: Type.Optional(Type.Boolean({})),
|
|
2950
|
-
systemPrompt: Type.Optional(Type.String({})),
|
|
2951
|
-
inheritSystemPrompt: Type.Optional(Type.Boolean({})),
|
|
2952
|
-
}), {
|
|
2953
|
-
description: "Declare tasks with dependencies. The scheduler parallelizes independent tasks and sequences dependent ones. Max " + MAX_GRAPH_TASKS + " tasks."
|
|
2954
|
-
})),
|
|
2955
|
-
}),
|
|
2781
|
+
parameters: delegateTaskParamsSchema,
|
|
2956
2782
|
|
|
2957
2783
|
async execute(
|
|
2958
2784
|
_toolCallId: string,
|
|
2959
|
-
params:
|
|
2960
|
-
// Schema registers task as Type.Optional — required for single mode only,
|
|
2961
|
-
// omitted by chain/tasks/graph calls. Aligned with Static<TParams>.
|
|
2962
|
-
task?: string;
|
|
2963
|
-
label?: string;
|
|
2964
|
-
model?: string;
|
|
2965
|
-
preset?: string;
|
|
2966
|
-
systemPrompt?: string;
|
|
2967
|
-
inheritSystemPrompt?: boolean;
|
|
2968
|
-
thinkingLevel?: string;
|
|
2969
|
-
outputFile?: string;
|
|
2970
|
-
timeout?: number;
|
|
2971
|
-
cwd?: string;
|
|
2972
|
-
tools?: string[];
|
|
2973
|
-
excludeTools?: string[];
|
|
2974
|
-
noBuiltinTools?: boolean;
|
|
2975
|
-
template?: string;
|
|
2976
|
-
params?: Record<string, string>;
|
|
2977
|
-
retryRunId?: string;
|
|
2978
|
-
retryOnTimeout?: boolean;
|
|
2979
|
-
background?: boolean;
|
|
2980
|
-
gitMode?: string;
|
|
2981
|
-
priority?: string;
|
|
2982
|
-
chain?: Array<{
|
|
2983
|
-
task: string;
|
|
2984
|
-
label?: string;
|
|
2985
|
-
model?: string;
|
|
2986
|
-
thinkingLevel?: string;
|
|
2987
|
-
cwd?: string;
|
|
2988
|
-
timeout?: number;
|
|
2989
|
-
outputFile?: string;
|
|
2990
|
-
tools?: string[];
|
|
2991
|
-
excludeTools?: string[];
|
|
2992
|
-
noBuiltinTools?: boolean;
|
|
2993
|
-
systemPrompt?: string;
|
|
2994
|
-
inheritSystemPrompt?: boolean;
|
|
2995
|
-
}>;
|
|
2996
|
-
tasks?: Array<{
|
|
2997
|
-
task: string;
|
|
2998
|
-
label?: string;
|
|
2999
|
-
model?: string;
|
|
3000
|
-
thinkingLevel?: string;
|
|
3001
|
-
priority?: string;
|
|
3002
|
-
cwd?: string;
|
|
3003
|
-
timeout?: number;
|
|
3004
|
-
outputFile?: string;
|
|
3005
|
-
tools?: string[];
|
|
3006
|
-
excludeTools?: string[];
|
|
3007
|
-
noBuiltinTools?: boolean;
|
|
3008
|
-
systemPrompt?: string;
|
|
3009
|
-
inheritSystemPrompt?: boolean;
|
|
3010
|
-
}>;
|
|
3011
|
-
graph?: Array<{
|
|
3012
|
-
id: string;
|
|
3013
|
-
task: string;
|
|
3014
|
-
label?: string;
|
|
3015
|
-
model?: string;
|
|
3016
|
-
dependsOn?: string[];
|
|
3017
|
-
thinkingLevel?: string;
|
|
3018
|
-
priority?: string;
|
|
3019
|
-
cwd?: string;
|
|
3020
|
-
timeout?: number;
|
|
3021
|
-
outputFile?: string;
|
|
3022
|
-
tools?: string[];
|
|
3023
|
-
excludeTools?: string[];
|
|
3024
|
-
noBuiltinTools?: boolean;
|
|
3025
|
-
systemPrompt?: string;
|
|
3026
|
-
inheritSystemPrompt?: boolean;
|
|
3027
|
-
}>;
|
|
3028
|
-
},
|
|
2785
|
+
params: DelegateTaskParams,
|
|
3029
2786
|
signal: AbortSignal | undefined,
|
|
3030
2787
|
onUpdate: AgentToolUpdateCallback<DelegateTaskDetails> | undefined,
|
|
3031
2788
|
ctx: ExtensionContext,
|
|
@@ -3133,7 +2890,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3133
2890
|
if (params.retryRunId) {
|
|
3134
2891
|
const runEntry = state.findSpawnRunById(ctx, params.retryRunId);
|
|
3135
2892
|
if (runEntry) {
|
|
3136
|
-
params = resolveRetryParams(params, runEntry);
|
|
2893
|
+
params = resolveRetryParams(params, runEntry) as DelegateTaskParams;
|
|
3137
2894
|
} else {
|
|
3138
2895
|
// Issue #98: a silent no-op here made retries of background runs
|
|
3139
2896
|
// (which previously never wrote run entries) start as FRESH runs
|
|
@@ -3257,31 +3014,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
3257
3014
|
|
|
3258
3015
|
// R5: Check session cost limit before spawning
|
|
3259
3016
|
// Use a default per-task estimate of $0.05 if no perTaskCostEstimate is set
|
|
3260
|
-
const
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
estimatedCost: perTaskEstimate,
|
|
3269
|
-
limit,
|
|
3270
|
-
});
|
|
3271
|
-
return {
|
|
3272
|
-
content: [
|
|
3273
|
-
{
|
|
3274
|
-
type: "text" as const,
|
|
3275
|
-
text:
|
|
3276
|
-
`Cannot delegate: session cost limit reached ` +
|
|
3277
|
-
`($${currentTotal.toFixed(4)} spent of $${limit.toFixed(2)} limit). ` +
|
|
3278
|
-
`Increase the limit via /brl-subagent costlimit or set to 0 for unlimited.`,
|
|
3279
|
-
},
|
|
3280
|
-
],
|
|
3281
|
-
details: undefined,
|
|
3282
|
-
isError: true,
|
|
3283
|
-
};
|
|
3284
|
-
}
|
|
3017
|
+
const costError = gateSessionCost({
|
|
3018
|
+
state,
|
|
3019
|
+
ctx,
|
|
3020
|
+
log,
|
|
3021
|
+
units: 1,
|
|
3022
|
+
label: "Subagent delegation",
|
|
3023
|
+
});
|
|
3024
|
+
if (costError) return costError;
|
|
3285
3025
|
|
|
3286
3026
|
// Reject delegation if recursion depth exceeds configured max.
|
|
3287
3027
|
// This prevents subagents from spawning infinite sub-subagents while
|
|
@@ -3321,6 +3061,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3321
3061
|
resolvedApprovalMode,
|
|
3322
3062
|
resolvedPreset,
|
|
3323
3063
|
autoRoutedPreset,
|
|
3064
|
+
autoRouteKeyword,
|
|
3324
3065
|
} = resolveSubagentParams({ ...params, task: singleTask }, state, ctx, log);
|
|
3325
3066
|
|
|
3326
3067
|
// F1: Validate CWD
|
|
@@ -3362,12 +3103,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
3362
3103
|
}
|
|
3363
3104
|
|
|
3364
3105
|
// H1: Pre-task validation — deterministic check that tools/thinking match task
|
|
3106
|
+
// (`force` degrades capability errors to warnings — B2's override).
|
|
3365
3107
|
const validation = validatePreTask({
|
|
3366
3108
|
task,
|
|
3367
3109
|
toolOptions,
|
|
3368
3110
|
thinkingLevel,
|
|
3369
3111
|
gitMode: resolvedGitMode,
|
|
3370
3112
|
outputFile: resolvedOutputFile,
|
|
3113
|
+
force: params.force,
|
|
3371
3114
|
});
|
|
3372
3115
|
if (validation.warnings.length > 0) {
|
|
3373
3116
|
log.warn("Pre-task validation warnings", { warnings: validation.warnings });
|
|
@@ -3384,6 +3127,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
3384
3127
|
isError: true,
|
|
3385
3128
|
};
|
|
3386
3129
|
}
|
|
3130
|
+
// C: surface warnings in the returned result (not just the log) —
|
|
3131
|
+
// shared renderer so foreground/background wording stays identical.
|
|
3132
|
+
const warningsNote = formatValidationWarnings(validation.warnings);
|
|
3387
3133
|
|
|
3388
3134
|
// Resolve model (per-call top-level model override beats preset)
|
|
3389
3135
|
const modelResult = resolveSubagentModel(ctx, resolvedPreset, params.model);
|
|
@@ -3771,6 +3517,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
3771
3517
|
if (suggestions.length > 0) {
|
|
3772
3518
|
finalMsg += "\n\nSuggestions:\n" + suggestions.map((s) => `- ${s}`).join("\n");
|
|
3773
3519
|
}
|
|
3520
|
+
// C: warnings ride the returned result too — a failed run is
|
|
3521
|
+
// exactly when the conductor most needs the mismatch signal.
|
|
3522
|
+
finalMsg += warningsNote;
|
|
3774
3523
|
|
|
3775
3524
|
completeTranscript(runId, 'failed');
|
|
3776
3525
|
|
|
@@ -3797,16 +3546,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
3797
3546
|
state.recordSuccess();
|
|
3798
3547
|
|
|
3799
3548
|
// B2: Surface auto-route decisions in the tool result so the conductor
|
|
3800
|
-
// sees
|
|
3801
|
-
// restriction from the RESOLVED
|
|
3802
|
-
// overrides) rather than the preset's
|
|
3549
|
+
// sees WHICH preset was auto-selected, the KEYWORD evidence for it,
|
|
3550
|
+
// and what it restricts. Derive the restriction from the RESOLVED
|
|
3551
|
+
// toolOptions (reflects per-call overrides) rather than the preset's
|
|
3552
|
+
// declared values.
|
|
3803
3553
|
const autoRouteNote = autoRoutedPreset
|
|
3804
|
-
? `\n\n[auto-routed to preset '${autoRoutedPreset.name}' — ${toolOptions ? formatToolRestriction(toolOptions) : formatPresetRestriction(autoRoutedPreset)}]`
|
|
3554
|
+
? `\n\n[auto-routed to preset '${autoRoutedPreset.name}'${autoRouteKeyword ? ` (matched keyword '${autoRouteKeyword}')` : ""} — ${toolOptions ? formatToolRestriction(toolOptions) : formatPresetRestriction(autoRoutedPreset)}]`
|
|
3805
3555
|
: "";
|
|
3806
3556
|
|
|
3807
3557
|
return {
|
|
3808
3558
|
content: [
|
|
3809
|
-
{ type: "text" as const, text: (finalOutput || "(no output)") + autoRouteNote },
|
|
3559
|
+
{ type: "text" as const, text: (finalOutput || "(no output)") + autoRouteNote + warningsNote },
|
|
3810
3560
|
],
|
|
3811
3561
|
details: result,
|
|
3812
3562
|
};
|
|
@@ -3886,11 +3636,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
3886
3636
|
agent_id: Type.String({
|
|
3887
3637
|
description: "The agent ID returned by delegate_task when background=true",
|
|
3888
3638
|
}),
|
|
3889
|
-
wait: Type.Optional(
|
|
3890
|
-
Type.Boolean({
|
|
3891
|
-
description: "If true, wait for the agent to complete before returning. Default: false.",
|
|
3892
|
-
}),
|
|
3893
|
-
),
|
|
3894
3639
|
verbose: Type.Optional(
|
|
3895
3640
|
Type.Boolean({
|
|
3896
3641
|
description: "If true, include the full conversation log. Default: false.",
|
|
@@ -3979,7 +3724,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3979
3724
|
label: "Steer Subagent",
|
|
3980
3725
|
description: [
|
|
3981
3726
|
"Send a steering message to a running background agent.",
|
|
3982
|
-
"The message
|
|
3727
|
+
"The message is delivered after the agent's current tool call finishes, before its next model call.",
|
|
3983
3728
|
"Use this to redirect an agent's work without restarting it.",
|
|
3984
3729
|
].join(" "),
|
|
3985
3730
|
parameters: Type.Object({
|
|
@@ -3994,7 +3739,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
3994
3739
|
const { steerAgent } = await import('./session-manager');
|
|
3995
3740
|
|
|
3996
3741
|
try {
|
|
3997
|
-
const agent = steerAgent(params.agent_id, params.message);
|
|
3742
|
+
const agent = await steerAgent(params.agent_id, params.message);
|
|
3998
3743
|
|
|
3999
3744
|
if (!agent) {
|
|
4000
3745
|
return {
|