iosm-cli 0.2.4 → 0.2.5
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 +31 -0
- package/README.md +23 -17
- package/dist/core/agent-profiles.d.ts +1 -0
- package/dist/core/agent-profiles.d.ts.map +1 -1
- package/dist/core/agent-profiles.js +10 -6
- package/dist/core/agent-profiles.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +6 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-teams.d.ts.map +1 -1
- package/dist/core/agent-teams.js +90 -19
- package/dist/core/agent-teams.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +6 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +9 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/parallel-task-agent.d.ts +23 -1
- package/dist/core/parallel-task-agent.d.ts.map +1 -1
- package/dist/core/parallel-task-agent.js +110 -20
- package/dist/core/parallel-task-agent.js.map +1 -1
- package/dist/core/shared-memory.d.ts +2 -2
- package/dist/core/shared-memory.d.ts.map +1 -1
- package/dist/core/shared-memory.js +220 -91
- package/dist/core/shared-memory.js.map +1 -1
- package/dist/core/singular.d.ts.map +1 -1
- package/dist/core/singular.js +3 -1
- package/dist/core/singular.js.map +1 -1
- package/dist/core/subagents.d.ts +1 -1
- package/dist/core/subagents.d.ts.map +1 -1
- package/dist/core/subagents.js +11 -3
- package/dist/core/subagents.js.map +1 -1
- package/dist/core/swarm/planner.d.ts.map +1 -1
- package/dist/core/swarm/planner.js +200 -12
- package/dist/core/swarm/planner.js.map +1 -1
- package/dist/core/swarm/scheduler.d.ts +2 -0
- package/dist/core/swarm/scheduler.d.ts.map +1 -1
- package/dist/core/swarm/scheduler.js +87 -6
- package/dist/core/swarm/scheduler.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -0
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/ast-grep.d.ts.map +1 -1
- package/dist/core/tools/ast-grep.js +2 -0
- package/dist/core/tools/ast-grep.js.map +1 -1
- package/dist/core/tools/shared-memory.d.ts.map +1 -1
- package/dist/core/tools/shared-memory.js +34 -6
- package/dist/core/tools/shared-memory.js.map +1 -1
- package/dist/core/tools/task.d.ts.map +1 -1
- package/dist/core/tools/task.js +464 -73
- package/dist/core/tools/task.js.map +1 -1
- package/dist/core/tools/yq.d.ts.map +1 -1
- package/dist/core/tools/yq.js +2 -0
- package/dist/core/tools/yq.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +2 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +13 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +756 -74
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/cli-reference.md +4 -0
- package/docs/interactive-mode.md +2 -0
- package/docs/orchestration-and-subagents.md +5 -0
- package/package.json +1 -1
package/dist/core/tools/task.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
5
5
|
import { getTeamRun, updateTeamTaskStatus } from "../agent-teams.js";
|
|
6
6
|
import { buildRetrospectiveDirective, classifyFailureCause, formatFailureCauseCounts, isRetrospectiveRetryable, } from "../failure-retrospective.js";
|
|
7
7
|
import { MAX_ORCHESTRATION_AGENTS, MAX_ORCHESTRATION_PARALLEL, MAX_SUBAGENT_DELEGATE_PARALLEL, MAX_SUBAGENT_DELEGATION_DEPTH, MAX_SUBAGENT_DELEGATIONS_PER_TASK, } from "../orchestration-limits.js";
|
|
8
|
+
import { AGENT_PROFILES, isReadOnlyProfileName, isValidProfileName, } from "../agent-profiles.js";
|
|
8
9
|
const taskSchema = Type.Object({
|
|
9
10
|
description: Type.Optional(Type.String({
|
|
10
11
|
description: "Optional short 3-5 word description of what the subagent will do. If omitted, it is derived from prompt.",
|
|
@@ -22,7 +23,7 @@ const taskSchema = Type.Object({
|
|
|
22
23
|
description: "Optional custom subagent name loaded from .iosm/agents or global agents directory.",
|
|
23
24
|
})),
|
|
24
25
|
profile: Type.Optional(Type.String({
|
|
25
|
-
description: "Optional subagent capability profile. Defaults to
|
|
26
|
+
description: "Optional subagent capability profile. Defaults to the current host profile when omitted (or full if host profile is unavailable). Recommended values: explore, plan, iosm, meta, iosm_analyst, iosm_verifier, cycle_planner, full. For custom agents, pass the agent name via `agent`, not `profile`.",
|
|
26
27
|
})),
|
|
27
28
|
cwd: Type.Optional(Type.String({
|
|
28
29
|
description: "Optional working directory for this subagent. Relative paths are resolved from the current workspace.",
|
|
@@ -51,29 +52,26 @@ const taskSchema = Type.Object({
|
|
|
51
52
|
description: "Optional hint for intra-task delegation fan-out. Higher value allows more delegated subtasks to run in parallel inside a single task execution.",
|
|
52
53
|
})),
|
|
53
54
|
});
|
|
54
|
-
/** Tool names available per profile */
|
|
55
|
-
const toolsByProfile = {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
meta: ["read", "bash", "edit", "write", "grep", "find", "ls"],
|
|
60
|
-
iosm_analyst: ["read", "bash", "grep", "find", "ls"],
|
|
61
|
-
iosm_verifier: ["read", "bash", "write"],
|
|
62
|
-
cycle_planner: ["read", "bash", "write"],
|
|
63
|
-
full: ["read", "bash", "edit", "write", "grep", "find", "ls"],
|
|
64
|
-
};
|
|
55
|
+
/** Tool names available per profile (kept in sync with AGENT_PROFILES). */
|
|
56
|
+
const toolsByProfile = Object.values(AGENT_PROFILES).reduce((acc, profile) => {
|
|
57
|
+
acc[profile.name] = [...profile.tools];
|
|
58
|
+
return acc;
|
|
59
|
+
}, {});
|
|
65
60
|
/** System prompt injected per profile */
|
|
66
61
|
const systemPromptByProfile = {
|
|
67
62
|
explore: "You are a fast read-only codebase explorer. Answer concisely. Never write or edit files.",
|
|
68
63
|
plan: "You are a technical architect. Analyze the codebase and produce a clear implementation plan. Do not write or edit files.",
|
|
69
64
|
iosm: "You are an IOSM execution agent. Use IOSM methodology and keep IOSM artifacts synchronized with implementation.",
|
|
70
|
-
meta: "You are a meta orchestration agent. Your main job is to maximize safe parallel execution through delegates, not to personally do most of the implementation. Start with bounded read-only recon, then form a concrete execution graph: subtasks, delegate subtasks, dependencies, lock domains, and verification steps. The parent agent remains responsible for orchestration and synthesis, so decompose work aggressively instead of collapsing complex work into one worker. For any non-trivial task, orchestration is required: after recon, launch multiple focused delegates instead of continuing manual implementation in the parent agent, avoid direct write/edit work in the parent agent before delegation unless the task is clearly trivial, and do not hand the whole task to one specialist child when independent workstreams exist. If a delegated workstream still contains multiple independent slices, split it again with nested <delegate_task> blocks. Default to aggressive safe parallelism. If the user requested a specific degree of parallelism, honor it when feasible or explain the exact blocker. When delegation is not used for non-trivial work, explain why in one line and include DELEGATION_IMPOSSIBLE. Enforce test verification for code changes, complete only after all delegated branches are resolved, and explicitly justify any no-code path where tests are skipped.",
|
|
65
|
+
meta: "You are a meta orchestration agent. Your main job is to maximize safe parallel execution through delegates, not to personally do most of the implementation. Start with bounded read-only recon, then form a concrete execution graph: subtasks, delegate subtasks, dependencies, lock domains, and verification steps. The parent agent remains responsible for orchestration and synthesis, so decompose work aggressively instead of collapsing complex work into one worker. For any non-trivial task, orchestration is required: after recon, launch multiple focused delegates instead of continuing manual implementation in the parent agent, avoid direct write/edit work in the parent agent before delegation unless the task is clearly trivial, and do not hand the whole task to one specialist child when independent workstreams exist. If a delegated workstream still contains multiple independent slices, split it again with nested <delegate_task> blocks. Default to aggressive safe parallelism. If the user requested a specific degree of parallelism, honor it when feasible or explain the exact blocker. Use shared_memory as the default coordination channel between delegates: use stable namespaced keys, prefer read-before-write, and use CAS (if_version) for contested updates; reserve append mode for timeline/log keys. When delegation is not used for non-trivial work, explain why in one line and include DELEGATION_IMPOSSIBLE. Enforce test verification for code changes, complete only after all delegated branches are resolved, and explicitly justify any no-code path where tests are skipped.",
|
|
71
66
|
iosm_analyst: "You are an IOSM metrics analyst. Analyze .iosm/ artifacts and codebase metrics. Be precise and evidence-based.",
|
|
72
67
|
iosm_verifier: "You are an IOSM verifier. Validate checks and update only required IOSM artifacts with deterministic reasoning.",
|
|
73
68
|
cycle_planner: "You are an IOSM cycle planner. Propose and align cycle goals with measurable outcomes and concrete risks.",
|
|
74
69
|
full: "You are a software engineering agent. Execute the task end-to-end.",
|
|
75
70
|
};
|
|
76
|
-
const
|
|
71
|
+
const writeCapableTools = new Set(["bash", "edit", "write"]);
|
|
72
|
+
const backgroundUnsafeTools = new Set(writeCapableTools);
|
|
73
|
+
const writeCapableProfiles = new Set(Object.keys(toolsByProfile).filter((profileName) => toolsByProfile[profileName].some((tool) => writeCapableTools.has(tool))));
|
|
74
|
+
const backgroundSafeProfiles = Object.keys(toolsByProfile).filter((profileName) => toolsByProfile[profileName].every((tool) => !backgroundUnsafeTools.has(tool)));
|
|
77
75
|
const delegationTagName = "delegate_task";
|
|
78
76
|
class Semaphore {
|
|
79
77
|
constructor(limit) {
|
|
@@ -132,7 +130,7 @@ class Mutex {
|
|
|
132
130
|
}
|
|
133
131
|
const maxParallelFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_PARALLEL, MAX_ORCHESTRATION_PARALLEL, 1, MAX_ORCHESTRATION_PARALLEL);
|
|
134
132
|
const subagentSemaphore = new Semaphore(maxParallelFromEnv);
|
|
135
|
-
const maxDelegationDepthFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_DELEGATION_DEPTH,
|
|
133
|
+
const maxDelegationDepthFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_DELEGATION_DEPTH, 2, 0, MAX_SUBAGENT_DELEGATION_DEPTH);
|
|
136
134
|
const maxDelegationsPerTaskFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_DELEGATIONS_PER_TASK, MAX_SUBAGENT_DELEGATIONS_PER_TASK, 0, MAX_SUBAGENT_DELEGATIONS_PER_TASK);
|
|
137
135
|
const maxDelegatedParallelFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_MAX_DELEGATE_PARALLEL, MAX_SUBAGENT_DELEGATE_PARALLEL, 1, MAX_SUBAGENT_DELEGATE_PARALLEL);
|
|
138
136
|
const emptyOutputRetriesFromEnv = parseBoundedInt(process.env.IOSM_SUBAGENT_EMPTY_OUTPUT_RETRIES, 1, 0, 2);
|
|
@@ -179,6 +177,9 @@ function deriveAutoDelegateParallelHint(profile, agentName, hostProfile, descrip
|
|
|
179
177
|
const fileLikeMatches = normalized.match(/\b[A-Za-z0-9_.-]+\.[A-Za-z0-9]{1,8}\b/g) ?? [];
|
|
180
178
|
const listMarkers = text.match(/(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/g)?.length ?? 0;
|
|
181
179
|
const hasCodeBlock = text.includes("```");
|
|
180
|
+
const actionTokenMatches = normalized.match(/\b(?:audit|security|auth|rbac|sqli|sql|injection|fix|implement|refactor|migrat|harden|verify|test|scan|orchestrate|parallel|delegate|bug|vulnerab)\w*/gi) ?? [];
|
|
181
|
+
const strongActionSignal = new Set(actionTokenMatches.map((token) => token.toLowerCase())).size >= 2;
|
|
182
|
+
const metaOrchestratorContext = isMetaProfile || isMetaHost;
|
|
182
183
|
let score = 0;
|
|
183
184
|
if (words >= 40) {
|
|
184
185
|
score += 2;
|
|
@@ -196,15 +197,32 @@ function deriveAutoDelegateParallelHint(profile, agentName, hostProfile, descrip
|
|
|
196
197
|
score += 1;
|
|
197
198
|
}
|
|
198
199
|
const referenceCount = pathLikeMatches.length + fileLikeMatches.length;
|
|
200
|
+
const metaNonTrivialSignal = words >= 12 ||
|
|
201
|
+
clauses >= 3 ||
|
|
202
|
+
listMarkers >= 1 ||
|
|
203
|
+
referenceCount >= 1 ||
|
|
204
|
+
hasCodeBlock ||
|
|
205
|
+
(strongActionSignal && words >= 4);
|
|
199
206
|
if (referenceCount >= 3 || (referenceCount >= 1 && words >= 20)) {
|
|
200
207
|
score += 1;
|
|
201
208
|
}
|
|
202
209
|
if (hasCodeBlock) {
|
|
203
210
|
score += 1;
|
|
204
211
|
}
|
|
205
|
-
if (
|
|
206
|
-
//
|
|
207
|
-
|
|
212
|
+
if (metaOrchestratorContext) {
|
|
213
|
+
// In meta orchestration, require delegation pressure for non-trivial prompts
|
|
214
|
+
// even when lexical scoring is still low.
|
|
215
|
+
if (score === 0) {
|
|
216
|
+
if (strongActionSignal && words >= 4) {
|
|
217
|
+
score = 1;
|
|
218
|
+
}
|
|
219
|
+
else if (metaNonTrivialSignal) {
|
|
220
|
+
score = 2;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else if (score > 0) {
|
|
224
|
+
score += 1;
|
|
225
|
+
}
|
|
208
226
|
}
|
|
209
227
|
if (score >= 6)
|
|
210
228
|
return 10;
|
|
@@ -320,6 +338,9 @@ function buildDelegationProtocolPrompt(depthRemaining, maxDelegations, minDelega
|
|
|
320
338
|
`Keep a brief coordinator note outside the blocks, but do not collapse the full workload into one monolithic answer.`,
|
|
321
339
|
`If safe decomposition is truly impossible, output exactly one line: DELEGATION_IMPOSSIBLE: <precise reason>.`,
|
|
322
340
|
`When shared_memory tools are available, exchange intermediate state through shared_memory_write/shared_memory_read instead of repeating large context.`,
|
|
341
|
+
`Shared-memory protocol: use stable namespaced keys (findings/<stream>, plan/<stream>, risks/<stream>).`,
|
|
342
|
+
`Use scope=run for cross-stream coordination, scope=task for local scratch state, read before overwrite, and use if_version for contested updates.`,
|
|
343
|
+
`Reserve mode=append for timeline/log keys only; avoid append on canonical state keys.`,
|
|
323
344
|
].join("\n");
|
|
324
345
|
}
|
|
325
346
|
return [
|
|
@@ -329,8 +350,155 @@ function buildDelegationProtocolPrompt(depthRemaining, maxDelegations, minDelega
|
|
|
329
350
|
`</${delegationTagName}>`,
|
|
330
351
|
`Only emit blocks when necessary. Keep normal analysis/answer text outside those blocks.`,
|
|
331
352
|
`When shared_memory tools are available, exchange intermediate state through shared_memory_write/shared_memory_read instead of repeating large context.`,
|
|
353
|
+
`Shared-memory protocol: prefer namespaced keys and read-before-write discipline; use CAS (if_version) on shared state updates.`,
|
|
354
|
+
`Reserve mode=append for timeline/log keys only.`,
|
|
332
355
|
].join("\n");
|
|
333
356
|
}
|
|
357
|
+
function truncateForDelegationContext(text, maxChars = 2200) {
|
|
358
|
+
const normalized = normalizeSpacing(text);
|
|
359
|
+
if (normalized.length <= maxChars)
|
|
360
|
+
return normalized;
|
|
361
|
+
return `${normalized.slice(0, Math.max(100, maxChars - 3)).trimEnd()}...`;
|
|
362
|
+
}
|
|
363
|
+
function extractDelegationWorkstreams(text, maxItems) {
|
|
364
|
+
if (maxItems <= 0)
|
|
365
|
+
return [];
|
|
366
|
+
const seen = new Set();
|
|
367
|
+
const pushUnique = (raw) => {
|
|
368
|
+
const cleaned = raw
|
|
369
|
+
.replace(/^[-*]\s+/, "")
|
|
370
|
+
.replace(/^\d+[.)]\s+/, "")
|
|
371
|
+
.replace(/\s+/g, " ")
|
|
372
|
+
.trim();
|
|
373
|
+
if (cleaned.length < 5)
|
|
374
|
+
return;
|
|
375
|
+
const key = cleaned.toLowerCase();
|
|
376
|
+
if (seen.has(key))
|
|
377
|
+
return;
|
|
378
|
+
seen.add(key);
|
|
379
|
+
};
|
|
380
|
+
for (const line of text.split("\n")) {
|
|
381
|
+
if (!/^\s*(?:[-*]|\d+[.)])\s+/.test(line))
|
|
382
|
+
continue;
|
|
383
|
+
pushUnique(line);
|
|
384
|
+
if (seen.size >= maxItems)
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
if (seen.size < maxItems) {
|
|
388
|
+
const fragments = text
|
|
389
|
+
.split(/[\n.;:]+/g)
|
|
390
|
+
.map((fragment) => fragment.trim())
|
|
391
|
+
.filter((fragment) => fragment.length >= 10)
|
|
392
|
+
.slice(0, maxItems * 3);
|
|
393
|
+
for (const fragment of fragments) {
|
|
394
|
+
pushUnique(fragment);
|
|
395
|
+
if (seen.size >= maxItems)
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return Array.from(seen).slice(0, maxItems);
|
|
400
|
+
}
|
|
401
|
+
function deriveAutoDelegateProfile(baseProfile, description, prompt) {
|
|
402
|
+
const signal = `${description}\n${prompt}`.toLowerCase();
|
|
403
|
+
const writeIntent = /\b(?:implement|fix|patch|refactor|rewrite|edit|update|migrate|change|write|apply)\b/.test(signal);
|
|
404
|
+
if (baseProfile === "full")
|
|
405
|
+
return writeIntent ? "full" : "explore";
|
|
406
|
+
if (baseProfile === "meta")
|
|
407
|
+
return writeIntent ? "full" : "explore";
|
|
408
|
+
if (baseProfile === "iosm")
|
|
409
|
+
return writeIntent ? "full" : "explore";
|
|
410
|
+
if (baseProfile === "iosm_verifier")
|
|
411
|
+
return "iosm_verifier";
|
|
412
|
+
if (baseProfile === "cycle_planner")
|
|
413
|
+
return "cycle_planner";
|
|
414
|
+
if (baseProfile === "plan" || baseProfile === "iosm_analyst")
|
|
415
|
+
return "explore";
|
|
416
|
+
return "explore";
|
|
417
|
+
}
|
|
418
|
+
function pickAutoDelegateAgent(workstream, availableCustomNames) {
|
|
419
|
+
if (availableCustomNames.length === 0)
|
|
420
|
+
return undefined;
|
|
421
|
+
const normalizedWorkstream = workstream.toLowerCase();
|
|
422
|
+
const names = availableCustomNames.map((name) => ({ raw: name, normalized: name.toLowerCase() }));
|
|
423
|
+
const findByHint = (hints) => {
|
|
424
|
+
for (const hint of hints) {
|
|
425
|
+
const exact = names.find((item) => item.normalized === hint);
|
|
426
|
+
if (exact)
|
|
427
|
+
return exact.raw;
|
|
428
|
+
const contains = names.find((item) => item.normalized.includes(hint));
|
|
429
|
+
if (contains)
|
|
430
|
+
return contains.raw;
|
|
431
|
+
}
|
|
432
|
+
return undefined;
|
|
433
|
+
};
|
|
434
|
+
if (/\b(?:test|qa|coverage|verification|regression)\b/.test(normalizedWorkstream)) {
|
|
435
|
+
return findByHint(["qa_test_engineer", "qa", "tester", "verification"]);
|
|
436
|
+
}
|
|
437
|
+
if (/\b(?:ui|ux|design|layout|accessibility)\b/.test(normalizedWorkstream)) {
|
|
438
|
+
return findByHint(["uiux_top_senior", "uiux", "ui", "ux", "design"]);
|
|
439
|
+
}
|
|
440
|
+
if (/\b(?:architecture|codebase|refactor|security|rbac|auth|database|api)\b/.test(normalizedWorkstream)) {
|
|
441
|
+
return findByHint(["codebase_auditor", "architect", "security", "backend"]);
|
|
442
|
+
}
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
function buildAutoDelegationPrompt(input) {
|
|
446
|
+
const objective = truncateForDelegationContext(`${input.rootDescription}\n\n${input.rootPrompt}`);
|
|
447
|
+
return normalizeSpacing([
|
|
448
|
+
`Workstream ${input.ordinal}/${input.total}: ${input.streamTitle}`,
|
|
449
|
+
"Scope:",
|
|
450
|
+
`- Own this stream end-to-end and avoid duplicating sibling streams.`,
|
|
451
|
+
`- Produce concrete findings/changes for this stream only.`,
|
|
452
|
+
"Coordinator objective:",
|
|
453
|
+
objective,
|
|
454
|
+
].join("\n"));
|
|
455
|
+
}
|
|
456
|
+
function synthesizeDelegationRequests(input) {
|
|
457
|
+
const desiredTotal = Math.max(0, Math.min(input.maxDelegations, input.minDelegationsPreferred));
|
|
458
|
+
const missing = Math.max(0, desiredTotal - input.currentDelegates);
|
|
459
|
+
if (missing <= 0)
|
|
460
|
+
return [];
|
|
461
|
+
const combined = `${input.description}\n${input.prompt}`.trim();
|
|
462
|
+
const extracted = extractDelegationWorkstreams(combined, Math.max(missing, desiredTotal));
|
|
463
|
+
const fallbackByIndex = [
|
|
464
|
+
"Architecture and structure analysis",
|
|
465
|
+
"Behavioral verification and tests",
|
|
466
|
+
"Risk, regressions, and remediation",
|
|
467
|
+
"Integration and dependency checks",
|
|
468
|
+
"Delivery summary and rollout constraints",
|
|
469
|
+
];
|
|
470
|
+
const titles = [];
|
|
471
|
+
for (const stream of extracted) {
|
|
472
|
+
titles.push(stream);
|
|
473
|
+
if (titles.length >= missing)
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
for (let index = 0; titles.length < missing && index < fallbackByIndex.length; index += 1) {
|
|
477
|
+
titles.push(fallbackByIndex[index]);
|
|
478
|
+
}
|
|
479
|
+
while (titles.length < missing) {
|
|
480
|
+
titles.push(`Independent workstream ${titles.length + 1}`);
|
|
481
|
+
}
|
|
482
|
+
const defaultProfile = deriveAutoDelegateProfile(input.baseProfile, input.description, input.prompt);
|
|
483
|
+
const synthesizedLockKey = writeCapableProfiles.has(defaultProfile) ? "auto-synth-write-lock" : undefined;
|
|
484
|
+
return titles.map((streamTitle, index) => ({
|
|
485
|
+
description: `Auto: ${streamTitle}`,
|
|
486
|
+
profile: defaultProfile,
|
|
487
|
+
agent: pickAutoDelegateAgent(streamTitle, input.availableCustomNames),
|
|
488
|
+
prompt: buildAutoDelegationPrompt({
|
|
489
|
+
streamTitle,
|
|
490
|
+
rootDescription: input.description,
|
|
491
|
+
rootPrompt: input.prompt,
|
|
492
|
+
ordinal: input.currentDelegates + index + 1,
|
|
493
|
+
total: input.currentDelegates + titles.length,
|
|
494
|
+
}),
|
|
495
|
+
cwd: undefined,
|
|
496
|
+
lockKey: synthesizedLockKey,
|
|
497
|
+
model: undefined,
|
|
498
|
+
isolation: undefined,
|
|
499
|
+
dependsOn: undefined,
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
334
502
|
function withDelegationPrompt(basePrompt, depthRemaining, maxDelegations, minDelegationsPreferred = 0) {
|
|
335
503
|
const protocol = buildDelegationProtocolPrompt(depthRemaining, maxDelegations, minDelegationsPreferred);
|
|
336
504
|
return `${basePrompt}\n\n${protocol}`;
|
|
@@ -348,7 +516,10 @@ function buildSharedMemoryGuidance(runId, taskId) {
|
|
|
348
516
|
"Guidelines:",
|
|
349
517
|
"- Use scope=run for cross-agent data and scope=task for task-local notes.",
|
|
350
518
|
"- Keep entries compact and key-based (for example: findings/auth, plan/step-1, risks/session).",
|
|
519
|
+
"- Prefer one canonical key per stream and deduplicate updates; avoid redundant writes in loops.",
|
|
351
520
|
"- Read before overwrite when collaborating on the same key.",
|
|
521
|
+
"- Use if_version CAS for contested updates on shared keys.",
|
|
522
|
+
"- Use mode=append only for log/timeline keys; use mode=set for canonical state.",
|
|
352
523
|
"[/SHARED_MEMORY]",
|
|
353
524
|
].join("\n");
|
|
354
525
|
}
|
|
@@ -366,19 +537,21 @@ function parseDelegationRequests(output, maxRequests) {
|
|
|
366
537
|
return "";
|
|
367
538
|
}
|
|
368
539
|
const attrs = {};
|
|
369
|
-
for (const match of attrsRaw.matchAll(/([A-Za-z_][A-Za-z0-9_-]*)
|
|
370
|
-
attrs[match[1].toLowerCase()] = match[2];
|
|
540
|
+
for (const match of attrsRaw.matchAll(/([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
|
|
541
|
+
attrs[match[1].toLowerCase()] = (match[2] ?? match[3] ?? "").trim();
|
|
371
542
|
}
|
|
372
543
|
const prompt = normalizeSpacing(bodyRaw ?? "");
|
|
373
544
|
if (!prompt) {
|
|
374
545
|
warnings.push(`Ignored delegation block with empty prompt.`);
|
|
375
546
|
return "";
|
|
376
547
|
}
|
|
377
|
-
const
|
|
378
|
-
if (!
|
|
379
|
-
warnings.push(`Ignored delegation block with
|
|
548
|
+
const profileRaw = (attrs.profile ?? "explore").trim();
|
|
549
|
+
if (!profileRaw) {
|
|
550
|
+
warnings.push(`Ignored delegation block with empty profile.`);
|
|
380
551
|
return "";
|
|
381
552
|
}
|
|
553
|
+
const normalizedProfile = profileRaw.toLowerCase();
|
|
554
|
+
const profile = isValidProfileName(normalizedProfile) ? normalizedProfile : profileRaw;
|
|
382
555
|
const isolationRaw = (attrs.isolation ?? "").trim().toLowerCase();
|
|
383
556
|
const isolation = isolationRaw === "worktree" ? "worktree" : isolationRaw === "none" ? "none" : undefined;
|
|
384
557
|
if (isolationRaw && !isolation) {
|
|
@@ -409,6 +582,9 @@ function parseDelegationRequests(output, maxRequests) {
|
|
|
409
582
|
cleanedOutput: normalizeSpacing(cleaned),
|
|
410
583
|
};
|
|
411
584
|
}
|
|
585
|
+
function isBackgroundSafeToolset(tools) {
|
|
586
|
+
return tools.every((toolName) => !backgroundUnsafeTools.has(toolName));
|
|
587
|
+
}
|
|
412
588
|
function getCwdLockKey(cwd) {
|
|
413
589
|
// Normalize lock key to keep behavior consistent across path aliases.
|
|
414
590
|
return path.resolve(cwd).toLowerCase();
|
|
@@ -694,12 +870,26 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
694
870
|
const resolveCustom = (name) => {
|
|
695
871
|
if (!name || !options?.resolveCustomSubagent)
|
|
696
872
|
return undefined;
|
|
697
|
-
|
|
873
|
+
const trimmed = name.trim();
|
|
874
|
+
if (!trimmed)
|
|
875
|
+
return undefined;
|
|
876
|
+
const resolved = options.resolveCustomSubagent(trimmed);
|
|
877
|
+
if (resolved)
|
|
878
|
+
return resolved;
|
|
879
|
+
const lowered = trimmed.toLowerCase();
|
|
880
|
+
if (lowered !== trimmed) {
|
|
881
|
+
return options.resolveCustomSubagent(lowered);
|
|
882
|
+
}
|
|
883
|
+
return undefined;
|
|
698
884
|
};
|
|
699
885
|
let normalizedAgentName = agentName?.trim() || undefined;
|
|
700
886
|
let customSubagent = resolveCustom(normalizedAgentName);
|
|
701
|
-
|
|
887
|
+
const requestedProfileRaw = profile?.trim() || undefined;
|
|
702
888
|
const normalizedHostProfile = options?.getHostProfileName?.()?.trim().toLowerCase() ?? options?.hostProfileName?.trim().toLowerCase();
|
|
889
|
+
const hostProfileFallback = normalizedHostProfile && isValidProfileName(normalizedHostProfile) ? normalizedHostProfile : "full";
|
|
890
|
+
let normalizedProfile = requestedProfileRaw?.toLowerCase() ||
|
|
891
|
+
customSubagent?.profile?.trim().toLowerCase() ||
|
|
892
|
+
hostProfileFallback;
|
|
703
893
|
if (normalizedAgentName && !customSubagent) {
|
|
704
894
|
const available = availableCustomNames.length > 0 ? ` Available custom agents: ${availableCustomNames.join(", ")}.` : "";
|
|
705
895
|
throw new Error(`Unknown subagent: ${normalizedAgentName}.${available}`);
|
|
@@ -713,17 +903,24 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
713
903
|
normalizedProfile = (profileAsAgent.profile ?? "full").trim().toLowerCase();
|
|
714
904
|
}
|
|
715
905
|
}
|
|
716
|
-
if (!
|
|
717
|
-
normalizedProfile
|
|
906
|
+
if (!customSubagent && !isValidProfileName(normalizedProfile)) {
|
|
907
|
+
throw new Error(`Unknown profile "${requestedProfileRaw ?? normalizedProfile}". Valid profiles: ${Object.keys(toolsByProfile).join(", ")}.`);
|
|
908
|
+
}
|
|
909
|
+
const effectiveProfileCandidate = (customSubagent?.profile ?? normalizedProfile).trim().toLowerCase();
|
|
910
|
+
if (!isValidProfileName(effectiveProfileCandidate)) {
|
|
911
|
+
throw new Error(`Invalid resolved profile "${effectiveProfileCandidate}". Valid profiles: ${Object.keys(toolsByProfile).join(", ")}.`);
|
|
718
912
|
}
|
|
719
|
-
const effectiveProfile =
|
|
913
|
+
const effectiveProfile = effectiveProfileCandidate;
|
|
720
914
|
let tools = customSubagent?.tools
|
|
721
915
|
? [...customSubagent.tools]
|
|
722
|
-
: [...
|
|
916
|
+
: [...toolsByProfile[effectiveProfile]];
|
|
723
917
|
if (customSubagent?.disallowedTools?.length) {
|
|
724
918
|
const blocked = new Set(customSubagent.disallowedTools);
|
|
725
919
|
tools = tools.filter((tool) => !blocked.has(tool));
|
|
726
920
|
}
|
|
921
|
+
if (isReadOnlyProfileName(normalizedHostProfile) && tools.some((tool) => writeCapableTools.has(tool))) {
|
|
922
|
+
throw new Error(`Host profile "${normalizedHostProfile}" is read-only. Switch to full/meta/iosm to launch write-capable subtasks.`);
|
|
923
|
+
}
|
|
727
924
|
const delegationDepth = maxDelegationDepthFromEnv;
|
|
728
925
|
const requestedDelegateParallelHint = typeof delegateParallelHint === "number" && Number.isInteger(delegateParallelHint)
|
|
729
926
|
? Math.max(1, Math.min(MAX_SUBAGENT_DELEGATE_PARALLEL, delegateParallelHint))
|
|
@@ -735,13 +932,22 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
735
932
|
const effectiveDelegationDepth = effectiveProfile === "meta" || normalizedHostProfile === "meta" || normalizedAgentName?.toLowerCase().includes("orchestrator")
|
|
736
933
|
? Math.max(delegationDepth, 2)
|
|
737
934
|
: delegationDepth;
|
|
935
|
+
const orchestratedRunContext = !!(orchestrationRunId && orchestrationTaskId);
|
|
936
|
+
const strictDelegationContract = effectiveProfile === "meta" ||
|
|
937
|
+
normalizedHostProfile === "meta" ||
|
|
938
|
+
normalizedAgentName?.toLowerCase().includes("orchestrator") ||
|
|
939
|
+
(orchestratedRunContext && (effectiveDelegateParallelHint ?? 0) >= 2);
|
|
738
940
|
let effectiveMaxDelegations = Math.max(0, Math.min(maxDelegationsPerTaskFromEnv, effectiveDelegateParallelHint ?? maxDelegationsPerTaskFromEnv));
|
|
739
941
|
let effectiveMaxDelegateParallel = Math.max(1, Math.min(maxDelegatedParallelFromEnv, effectiveDelegateParallelHint ?? maxDelegatedParallelFromEnv));
|
|
740
|
-
const
|
|
942
|
+
const isMetaDelegationContext = effectiveProfile === "meta" || normalizedHostProfile === "meta";
|
|
943
|
+
const preferredDelegationFloorBase = isMetaDelegationContext ? 3 : 2;
|
|
944
|
+
const preferredDelegationFloorMin = isMetaDelegationContext ? 2 : 1;
|
|
945
|
+
const metaDelegationCapacityFloor = 3;
|
|
946
|
+
const preferredDelegationFloor = Math.max(preferredDelegationFloorMin, Math.min(preferredDelegationFloorBase, effectiveDelegateParallelHint ?? preferredDelegationFloorBase));
|
|
741
947
|
const applyMetaDelegationFloor = requestedDelegateParallelHint === undefined &&
|
|
742
948
|
(effectiveProfile === "meta" || normalizedHostProfile === "meta");
|
|
743
949
|
if (applyMetaDelegationFloor) {
|
|
744
|
-
effectiveMaxDelegations = Math.max(effectiveMaxDelegations, Math.min(maxDelegationsPerTaskFromEnv,
|
|
950
|
+
effectiveMaxDelegations = Math.max(effectiveMaxDelegations, Math.min(maxDelegationsPerTaskFromEnv, metaDelegationCapacityFloor));
|
|
745
951
|
effectiveMaxDelegateParallel = Math.max(effectiveMaxDelegateParallel, Math.min(maxDelegatedParallelFromEnv, preferredDelegationFloor));
|
|
746
952
|
}
|
|
747
953
|
const minDelegationsPreferred = (effectiveDelegateParallelHint ?? 0) >= 2 && effectiveMaxDelegations >= 2
|
|
@@ -764,8 +970,10 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
764
970
|
if (!existsSync(requestedSubagentCwd) || !statSync(requestedSubagentCwd).isDirectory()) {
|
|
765
971
|
throw new Error(`Subagent cwd does not exist or is not a directory: ${requestedSubagentCwd}`);
|
|
766
972
|
}
|
|
767
|
-
if (runInBackground &&
|
|
768
|
-
throw new Error(`Background policy violation: profile "${effectiveProfile}"
|
|
973
|
+
if (runInBackground && !isBackgroundSafeToolset(tools)) {
|
|
974
|
+
throw new Error(`Background policy violation: profile "${effectiveProfile}" has mutable tools (${tools
|
|
975
|
+
.filter((toolName) => backgroundUnsafeTools.has(toolName))
|
|
976
|
+
.join(", ")}). Background mode requires read-only toolsets. Safe baseline profiles: ${backgroundSafeProfiles.join(", ")}.`);
|
|
769
977
|
}
|
|
770
978
|
const useWorktree = isolation === "worktree";
|
|
771
979
|
const queuedAt = Date.now();
|
|
@@ -821,6 +1029,42 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
821
1029
|
let subagentCwd = requestedSubagentCwd;
|
|
822
1030
|
let worktreePath;
|
|
823
1031
|
let runStats;
|
|
1032
|
+
const heldWriteLocks = new Map();
|
|
1033
|
+
const acquireLocalWriteLock = async (rawLockKey) => {
|
|
1034
|
+
const trimmed = rawLockKey?.trim();
|
|
1035
|
+
if (!trimmed)
|
|
1036
|
+
return undefined;
|
|
1037
|
+
const normalizedKey = getCwdLockKey(trimmed);
|
|
1038
|
+
const existing = heldWriteLocks.get(normalizedKey);
|
|
1039
|
+
if (existing) {
|
|
1040
|
+
existing.count += 1;
|
|
1041
|
+
return () => {
|
|
1042
|
+
const current = heldWriteLocks.get(normalizedKey);
|
|
1043
|
+
if (!current)
|
|
1044
|
+
return;
|
|
1045
|
+
current.count -= 1;
|
|
1046
|
+
if (current.count <= 0) {
|
|
1047
|
+
heldWriteLocks.delete(normalizedKey);
|
|
1048
|
+
current.release();
|
|
1049
|
+
cleanupWriteLock(trimmed);
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
const lock = getOrCreateWriteLock(trimmed);
|
|
1054
|
+
const release = await lock.acquire();
|
|
1055
|
+
heldWriteLocks.set(normalizedKey, { count: 1, release });
|
|
1056
|
+
return () => {
|
|
1057
|
+
const current = heldWriteLocks.get(normalizedKey);
|
|
1058
|
+
if (!current)
|
|
1059
|
+
return;
|
|
1060
|
+
current.count -= 1;
|
|
1061
|
+
if (current.count <= 0) {
|
|
1062
|
+
heldWriteLocks.delete(normalizedKey);
|
|
1063
|
+
current.release();
|
|
1064
|
+
cleanupWriteLock(trimmed);
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
};
|
|
824
1068
|
try {
|
|
825
1069
|
throwIfAborted();
|
|
826
1070
|
if (orchestrationRunId && orchestrationTaskId) {
|
|
@@ -882,8 +1126,7 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
882
1126
|
// Parallel orchestration should remain truly parallel by default.
|
|
883
1127
|
// Serialize write-capable agents only when an explicit lock_key is provided.
|
|
884
1128
|
if (explicitRootLockKey) {
|
|
885
|
-
|
|
886
|
-
releaseWriteLock = await lock.acquire();
|
|
1129
|
+
releaseWriteLock = await acquireLocalWriteLock(explicitRootLockKey);
|
|
887
1130
|
}
|
|
888
1131
|
}
|
|
889
1132
|
if (useWorktree) {
|
|
@@ -1052,8 +1295,33 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
1052
1295
|
runStats = secondPass.stats ?? runStats;
|
|
1053
1296
|
parsedDelegation = parseDelegationRequests(output, effectiveDelegationDepth > 0 ? effectiveMaxDelegations : 0);
|
|
1054
1297
|
}
|
|
1298
|
+
if (minDelegationsPreferred > 0 &&
|
|
1299
|
+
parsedDelegation.requests.length === 0 &&
|
|
1300
|
+
strictDelegationContract) {
|
|
1301
|
+
const impossibleMatch = output.match(/^\s*DELEGATION_IMPOSSIBLE\s*:\s*(.+)$/im);
|
|
1302
|
+
if (!impossibleMatch) {
|
|
1303
|
+
const synthesizedRequests = synthesizeDelegationRequests({
|
|
1304
|
+
description,
|
|
1305
|
+
prompt,
|
|
1306
|
+
baseProfile: effectiveProfile,
|
|
1307
|
+
currentDelegates: parsedDelegation.requests.length,
|
|
1308
|
+
minDelegationsPreferred,
|
|
1309
|
+
maxDelegations: effectiveMaxDelegations,
|
|
1310
|
+
availableCustomNames,
|
|
1311
|
+
});
|
|
1312
|
+
if (synthesizedRequests.length > 0) {
|
|
1313
|
+
parsedDelegation.requests.push(...synthesizedRequests);
|
|
1314
|
+
delegationWarnings.push(`Delegation auto-fanout: synthesized ${synthesizedRequests.length} delegate(s) to satisfy parallelism contract.`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1055
1318
|
if (minDelegationsPreferred > 0 && parsedDelegation.requests.length < minDelegationsPreferred) {
|
|
1056
|
-
const
|
|
1319
|
+
const impossibleMatch = output.match(/^\s*DELEGATION_IMPOSSIBLE\s*:\s*(.+)$/im);
|
|
1320
|
+
const impossibleReason = impossibleMatch?.[1]?.trim() ?? "not provided";
|
|
1321
|
+
if (strictDelegationContract && parsedDelegation.requests.length === 0 && !impossibleMatch) {
|
|
1322
|
+
throw new Error(`Delegation contract violated: expected >=${minDelegationsPreferred} delegates, got ${parsedDelegation.requests.length}. ` +
|
|
1323
|
+
`Provide nested <delegate_task> fan-out or an explicit "DELEGATION_IMPOSSIBLE: <reason>" line.`);
|
|
1324
|
+
}
|
|
1057
1325
|
delegationWarnings.push(`Delegation fallback: kept single-agent execution (preferred >=${minDelegationsPreferred} delegates, got ${parsedDelegation.requests.length}). Reason: ${impossibleReason}.`);
|
|
1058
1326
|
}
|
|
1059
1327
|
output = parsedDelegation.cleanedOutput;
|
|
@@ -1127,25 +1395,60 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
1127
1395
|
return { sections: [], warnings: [] };
|
|
1128
1396
|
}
|
|
1129
1397
|
const nestedWarnings = [];
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1398
|
+
const sectionsByIndex = new Array(requests.length);
|
|
1399
|
+
const statuses = Array.from({ length: requests.length }, () => "pending");
|
|
1400
|
+
const totalNested = requests.length;
|
|
1401
|
+
const normalizedDependsOn = requests.map((request, index) => {
|
|
1402
|
+
const current = index + 1;
|
|
1403
|
+
const raw = request.dependsOn ?? [];
|
|
1404
|
+
const unique = new Set();
|
|
1405
|
+
for (const dep of raw) {
|
|
1406
|
+
if (!Number.isInteger(dep) || dep <= 0 || dep > totalNested || dep === current) {
|
|
1407
|
+
nestedWarnings.push(`Nested delegated task ${lineage}${current} has invalid depends_on reference "${dep}" and it was ignored.`);
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
unique.add(dep);
|
|
1411
|
+
}
|
|
1412
|
+
return Array.from(unique).sort((a, b) => a - b);
|
|
1413
|
+
});
|
|
1414
|
+
const statusOf = (index) => statuses[index] ?? "pending";
|
|
1415
|
+
const markNestedFailed = (nestedIndex, requestLabel, nestedRequest, nestedProfileLabel, message, cause) => {
|
|
1416
|
+
statuses[nestedIndex] = "failed";
|
|
1417
|
+
recordFailureCause(cause);
|
|
1418
|
+
delegatedTasks += 1;
|
|
1419
|
+
delegatedFailed += 1;
|
|
1420
|
+
sectionsByIndex[nestedIndex] =
|
|
1421
|
+
`###### ${requestLabel}. ${nestedRequest.description} (${nestedProfileLabel})\nERROR [cause=${cause}]: ${message}`;
|
|
1422
|
+
};
|
|
1423
|
+
const runNestedDelegate = async (nestedIndex) => {
|
|
1424
|
+
const nestedRequest = requests[nestedIndex];
|
|
1132
1425
|
const requestLabel = `${lineage}${nestedIndex + 1}`;
|
|
1133
|
-
|
|
1134
|
-
|
|
1426
|
+
statuses[nestedIndex] = "running";
|
|
1427
|
+
let requestedNestedAgent = nestedRequest.agent?.trim() || undefined;
|
|
1428
|
+
let nestedCustomSubagent = resolveCustom(requestedNestedAgent);
|
|
1429
|
+
if (!nestedCustomSubagent && !requestedNestedAgent) {
|
|
1430
|
+
const profileAsAgent = resolveCustom(nestedRequest.profile);
|
|
1431
|
+
if (profileAsAgent) {
|
|
1432
|
+
nestedCustomSubagent = profileAsAgent;
|
|
1433
|
+
requestedNestedAgent = profileAsAgent.name;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1135
1436
|
if (requestedNestedAgent && !nestedCustomSubagent) {
|
|
1136
1437
|
nestedWarnings.push(`Nested delegated task "${nestedRequest.description}" requested unknown agent "${requestedNestedAgent}". Falling back to profile "${nestedRequest.profile}".`);
|
|
1137
1438
|
}
|
|
1138
|
-
const
|
|
1139
|
-
const normalizedNestedProfile =
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1439
|
+
const nestedProfileCandidate = (nestedCustomSubagent?.profile ?? nestedRequest.profile).trim();
|
|
1440
|
+
const normalizedNestedProfile = nestedProfileCandidate.toLowerCase();
|
|
1441
|
+
if (!isValidProfileName(normalizedNestedProfile)) {
|
|
1442
|
+
markNestedFailed(nestedIndex, requestLabel, nestedRequest, nestedProfileCandidate || "unknown", `nested delegate skipped: unknown profile "${nestedProfileCandidate || nestedRequest.profile}"`, "logic_error");
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const nestedProfile = normalizedNestedProfile;
|
|
1143
1446
|
const nestedProfileLabel = nestedCustomSubagent?.name
|
|
1144
1447
|
? `${nestedCustomSubagent.name}/${nestedProfile}`
|
|
1145
1448
|
: nestedProfile;
|
|
1146
1449
|
let nestedTools = nestedCustomSubagent?.tools
|
|
1147
1450
|
? [...nestedCustomSubagent.tools]
|
|
1148
|
-
: [...
|
|
1451
|
+
: [...toolsByProfile[nestedProfile]];
|
|
1149
1452
|
if (nestedCustomSubagent?.disallowedTools?.length) {
|
|
1150
1453
|
const blocked = new Set(nestedCustomSubagent.disallowedTools);
|
|
1151
1454
|
nestedTools = nestedTools.filter((tool) => !blocked.has(tool));
|
|
@@ -1158,19 +1461,15 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
1158
1461
|
? path.resolve(parentCwd, nestedRequest.cwd)
|
|
1159
1462
|
: nestedCustomSubagent?.cwd ?? parentCwd;
|
|
1160
1463
|
if (!existsSync(requestedNestedCwd) || !statSync(requestedNestedCwd).isDirectory()) {
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
delegatedTasks += 1;
|
|
1164
|
-
sections.push(`###### ${requestLabel}. ${nestedRequest.description} (${nestedProfileLabel})\nERROR [cause=dependency_env]: nested delegate skipped: missing cwd`);
|
|
1165
|
-
continue;
|
|
1464
|
+
markNestedFailed(nestedIndex, requestLabel, nestedRequest, nestedProfileLabel, "nested delegate skipped: missing cwd", "dependency_env");
|
|
1465
|
+
return;
|
|
1166
1466
|
}
|
|
1167
1467
|
let nestedReleaseLock;
|
|
1168
1468
|
let nestedReleaseIsolation;
|
|
1169
1469
|
let nestedCwd = requestedNestedCwd;
|
|
1170
1470
|
try {
|
|
1171
1471
|
if (writeCapableProfiles.has(nestedProfile) && nestedRequest.lockKey?.trim()) {
|
|
1172
|
-
|
|
1173
|
-
nestedReleaseLock = await lock.acquire();
|
|
1472
|
+
nestedReleaseLock = await acquireLocalWriteLock(nestedRequest.lockKey.trim());
|
|
1174
1473
|
}
|
|
1175
1474
|
if (nestedRequest.isolation === "worktree") {
|
|
1176
1475
|
const isolated = provisionWorktree(cwd, requestedNestedCwd, `${runId}_nested_${requestLabel.replace(/\./g, "_")}`);
|
|
@@ -1212,6 +1511,7 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
1212
1511
|
const nestedStats = typeof nestedResult === "string" ? undefined : nestedResult.stats;
|
|
1213
1512
|
delegatedTasks += 1;
|
|
1214
1513
|
delegatedSucceeded += 1;
|
|
1514
|
+
statuses[nestedIndex] = "done";
|
|
1215
1515
|
delegatedStats.toolCallsStarted += nestedStats?.toolCallsStarted ?? 0;
|
|
1216
1516
|
delegatedStats.toolCallsCompleted += nestedStats?.toolCallsCompleted ?? 0;
|
|
1217
1517
|
delegatedStats.assistantMessages += nestedStats?.assistantMessages ?? 0;
|
|
@@ -1226,37 +1526,105 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
1226
1526
|
nestedSection = `${nestedSection}\n\n##### Nested Delegated Subtasks\n\n${deeper.sections.join("\n\n")}`;
|
|
1227
1527
|
}
|
|
1228
1528
|
}
|
|
1229
|
-
|
|
1529
|
+
sectionsByIndex[nestedIndex] = nestedSection;
|
|
1230
1530
|
}
|
|
1231
1531
|
catch (error) {
|
|
1232
1532
|
const message = error instanceof Error ? error.message : String(error);
|
|
1233
1533
|
const cause = classifyFailureCause(message);
|
|
1234
|
-
|
|
1235
|
-
delegatedTasks += 1;
|
|
1236
|
-
delegatedFailed += 1;
|
|
1237
|
-
sections.push(`###### ${requestLabel}. ${nestedRequest.description} (${nestedProfileLabel})\nERROR [cause=${cause}]: ${message}`);
|
|
1534
|
+
markNestedFailed(nestedIndex, requestLabel, nestedRequest, nestedProfileLabel, message, cause);
|
|
1238
1535
|
}
|
|
1239
1536
|
finally {
|
|
1240
1537
|
nestedReleaseIsolation?.();
|
|
1241
1538
|
nestedReleaseLock?.();
|
|
1242
|
-
cleanupWriteLock(nestedRequest.lockKey?.trim());
|
|
1243
1539
|
}
|
|
1540
|
+
};
|
|
1541
|
+
const pendingIndices = new Set(Array.from({ length: totalNested }, (_v, i) => i));
|
|
1542
|
+
const runningNested = new Map();
|
|
1543
|
+
const maxNestedParallel = Math.max(1, Math.min(totalNested, effectiveMaxDelegateParallel));
|
|
1544
|
+
const resolveBlockedByFailedDependencies = () => {
|
|
1545
|
+
let changed = false;
|
|
1546
|
+
for (const nestedIndex of Array.from(pendingIndices)) {
|
|
1547
|
+
const deps = normalizedDependsOn[nestedIndex] ?? [];
|
|
1548
|
+
if (deps.length === 0)
|
|
1549
|
+
continue;
|
|
1550
|
+
const failedDep = deps.find((dep) => statusOf(dep - 1) === "failed");
|
|
1551
|
+
if (!failedDep)
|
|
1552
|
+
continue;
|
|
1553
|
+
pendingIndices.delete(nestedIndex);
|
|
1554
|
+
const nestedRequest = requests[nestedIndex];
|
|
1555
|
+
const requestLabel = `${lineage}${nestedIndex + 1}`;
|
|
1556
|
+
markNestedFailed(nestedIndex, requestLabel, nestedRequest, nestedRequest.profile, `nested delegate skipped: dependency ${lineage}${failedDep} failed`, "logic_error");
|
|
1557
|
+
changed = true;
|
|
1558
|
+
}
|
|
1559
|
+
return changed;
|
|
1560
|
+
};
|
|
1561
|
+
const launchReadyNested = () => {
|
|
1562
|
+
let launched = false;
|
|
1563
|
+
while (runningNested.size < maxNestedParallel) {
|
|
1564
|
+
let nextIndex;
|
|
1565
|
+
for (const nestedIndex of pendingIndices) {
|
|
1566
|
+
const deps = normalizedDependsOn[nestedIndex] ?? [];
|
|
1567
|
+
const allDone = deps.every((dep) => statusOf(dep - 1) === "done");
|
|
1568
|
+
if (allDone) {
|
|
1569
|
+
nextIndex = nestedIndex;
|
|
1570
|
+
break;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
if (nextIndex === undefined)
|
|
1574
|
+
break;
|
|
1575
|
+
pendingIndices.delete(nextIndex);
|
|
1576
|
+
const promise = runNestedDelegate(nextIndex).finally(() => {
|
|
1577
|
+
runningNested.delete(nextIndex);
|
|
1578
|
+
});
|
|
1579
|
+
runningNested.set(nextIndex, promise);
|
|
1580
|
+
launched = true;
|
|
1581
|
+
}
|
|
1582
|
+
return launched;
|
|
1583
|
+
};
|
|
1584
|
+
while (pendingIndices.size > 0 || runningNested.size > 0) {
|
|
1585
|
+
throwIfAborted();
|
|
1586
|
+
const changed = resolveBlockedByFailedDependencies();
|
|
1587
|
+
const launched = launchReadyNested();
|
|
1588
|
+
if (runningNested.size === 0) {
|
|
1589
|
+
if (pendingIndices.size > 0 && !changed && !launched) {
|
|
1590
|
+
for (const nestedIndex of Array.from(pendingIndices)) {
|
|
1591
|
+
pendingIndices.delete(nestedIndex);
|
|
1592
|
+
const nestedRequest = requests[nestedIndex];
|
|
1593
|
+
const deps = normalizedDependsOn[nestedIndex] ?? [];
|
|
1594
|
+
const requestLabel = `${lineage}${nestedIndex + 1}`;
|
|
1595
|
+
markNestedFailed(nestedIndex, requestLabel, nestedRequest, nestedRequest.profile, `nested delegate blocked: unresolved depends_on (${deps.join(", ") || "unknown"})`, "logic_error");
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
await Promise.race(Array.from(runningNested.values()));
|
|
1244
1601
|
}
|
|
1602
|
+
const sections = sectionsByIndex.filter((section) => typeof section === "string" && section.trim().length > 0);
|
|
1245
1603
|
return { sections, warnings: nestedWarnings };
|
|
1246
1604
|
};
|
|
1247
1605
|
const runDelegate = async (index) => {
|
|
1248
1606
|
throwIfAborted();
|
|
1249
1607
|
const request = parsedDelegation.requests[index];
|
|
1250
|
-
|
|
1251
|
-
|
|
1608
|
+
let requestedChildAgent = request.agent?.trim() || undefined;
|
|
1609
|
+
let childCustomSubagent = resolveCustom(requestedChildAgent);
|
|
1610
|
+
if (!childCustomSubagent && !requestedChildAgent) {
|
|
1611
|
+
const profileAsAgent = resolveCustom(request.profile);
|
|
1612
|
+
if (profileAsAgent) {
|
|
1613
|
+
childCustomSubagent = profileAsAgent;
|
|
1614
|
+
requestedChildAgent = profileAsAgent.name;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1252
1617
|
if (requestedChildAgent && !childCustomSubagent) {
|
|
1253
1618
|
delegationWarnings.push(`Delegated task "${request.description}" requested unknown agent "${requestedChildAgent}". Falling back to profile "${request.profile}".`);
|
|
1254
1619
|
}
|
|
1255
|
-
const childProfileRaw = childCustomSubagent?.profile ?? request.profile;
|
|
1256
|
-
const normalizedChildProfile = childProfileRaw.
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
: "
|
|
1620
|
+
const childProfileRaw = (childCustomSubagent?.profile ?? request.profile).trim();
|
|
1621
|
+
const normalizedChildProfile = childProfileRaw.toLowerCase();
|
|
1622
|
+
if (!isValidProfileName(normalizedChildProfile)) {
|
|
1623
|
+
recordFailureCause("logic_error");
|
|
1624
|
+
markDelegateFailed(index, `delegate ${index + 1}/${delegateTotal} skipped: unknown profile "${childProfileRaw || request.profile}"`, `Delegated task "${request.description}" requested unknown profile "${childProfileRaw || request.profile}".`, "logic_error");
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
const childProfile = normalizedChildProfile;
|
|
1260
1628
|
const childProfileLabel = childCustomSubagent?.name
|
|
1261
1629
|
? `${childCustomSubagent.name}/${childProfile}`
|
|
1262
1630
|
: childProfile;
|
|
@@ -1277,7 +1645,7 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
1277
1645
|
});
|
|
1278
1646
|
let childTools = childCustomSubagent?.tools
|
|
1279
1647
|
? [...childCustomSubagent.tools]
|
|
1280
|
-
: [...
|
|
1648
|
+
: [...toolsByProfile[childProfile]];
|
|
1281
1649
|
if (childCustomSubagent?.disallowedTools?.length) {
|
|
1282
1650
|
const blocked = new Set(childCustomSubagent.disallowedTools);
|
|
1283
1651
|
childTools = childTools.filter((tool) => !blocked.has(tool));
|
|
@@ -1306,8 +1674,7 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
1306
1674
|
try {
|
|
1307
1675
|
throwIfAborted();
|
|
1308
1676
|
if (writeCapableProfiles.has(childProfile) && explicitChildLock) {
|
|
1309
|
-
|
|
1310
|
-
childReleaseLock = await lock.acquire();
|
|
1677
|
+
childReleaseLock = await acquireLocalWriteLock(explicitChildLock);
|
|
1311
1678
|
throwIfAborted();
|
|
1312
1679
|
}
|
|
1313
1680
|
if (request.isolation === "worktree") {
|
|
@@ -1477,10 +1844,36 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
1477
1844
|
childOutput = await runChildPass(enforcedChildPrompt);
|
|
1478
1845
|
parsedChildDelegation = parseDelegationRequests(childOutput, effectiveDelegationDepth > 1 ? effectiveMaxDelegations : 0);
|
|
1479
1846
|
}
|
|
1847
|
+
if (childMinDelegationsPreferred > 0 &&
|
|
1848
|
+
parsedChildDelegation.requests.length === 0 &&
|
|
1849
|
+
strictDelegationContract) {
|
|
1850
|
+
const impossibleMatch = childOutput.match(/^\s*DELEGATION_IMPOSSIBLE\s*:\s*(.+)$/im);
|
|
1851
|
+
if (!impossibleMatch) {
|
|
1852
|
+
const synthesizedChildRequests = synthesizeDelegationRequests({
|
|
1853
|
+
description: request.description,
|
|
1854
|
+
prompt: request.prompt,
|
|
1855
|
+
baseProfile: childProfile,
|
|
1856
|
+
currentDelegates: parsedChildDelegation.requests.length,
|
|
1857
|
+
minDelegationsPreferred: childMinDelegationsPreferred,
|
|
1858
|
+
maxDelegations: effectiveMaxDelegations,
|
|
1859
|
+
availableCustomNames,
|
|
1860
|
+
});
|
|
1861
|
+
if (synthesizedChildRequests.length > 0) {
|
|
1862
|
+
parsedChildDelegation.requests.push(...synthesizedChildRequests);
|
|
1863
|
+
delegationWarnings.push(`Child ${index + 1}: delegation auto-fanout synthesized ${synthesizedChildRequests.length} nested delegate(s).`);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1480
1867
|
if (childMinDelegationsPreferred > 0 &&
|
|
1481
1868
|
parsedChildDelegation.requests.length < childMinDelegationsPreferred) {
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1869
|
+
const impossibleMatch = childOutput.match(/^\s*DELEGATION_IMPOSSIBLE\s*:\s*(.+)$/im);
|
|
1870
|
+
const impossibleReason = impossibleMatch?.[1]?.trim() ?? "not provided";
|
|
1871
|
+
if (strictDelegationContract &&
|
|
1872
|
+
parsedChildDelegation.requests.length === 0 &&
|
|
1873
|
+
!impossibleMatch) {
|
|
1874
|
+
throw new Error(`Delegation contract violated for child ${index + 1}: expected >=${childMinDelegationsPreferred} nested delegates, got ${parsedChildDelegation.requests.length}. ` +
|
|
1875
|
+
`Provide nested <delegate_task> fan-out or "DELEGATION_IMPOSSIBLE: <reason>".`);
|
|
1876
|
+
}
|
|
1484
1877
|
delegationWarnings.push(`Child ${index + 1}: delegation fallback (preferred >=${childMinDelegationsPreferred}, got ${parsedChildDelegation.requests.length}). Reason: ${impossibleReason}.`);
|
|
1485
1878
|
}
|
|
1486
1879
|
childOutput = parsedChildDelegation.cleanedOutput;
|
|
@@ -1535,7 +1928,6 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
1535
1928
|
finally {
|
|
1536
1929
|
childReleaseIsolation?.();
|
|
1537
1930
|
childReleaseLock?.();
|
|
1538
|
-
cleanupWriteLock(explicitChildLock);
|
|
1539
1931
|
}
|
|
1540
1932
|
};
|
|
1541
1933
|
const resolveBlockedByFailedDependencies = () => {
|
|
@@ -1760,7 +2152,6 @@ export function createTaskTool(cwd, runner, options) {
|
|
|
1760
2152
|
finally {
|
|
1761
2153
|
releaseIsolation?.();
|
|
1762
2154
|
releaseWriteLock?.();
|
|
1763
|
-
cleanupWriteLock(explicitRootLockKey);
|
|
1764
2155
|
releaseSlot?.();
|
|
1765
2156
|
releaseRunSlot?.();
|
|
1766
2157
|
if (orchestrationRunId) {
|