@kolisachint/hoocode-agent 0.4.8 → 0.4.10
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 +2 -0
- package/dist/core/dispatch-evaluator.d.ts +31 -0
- package/dist/core/dispatch-evaluator.d.ts.map +1 -0
- package/dist/core/dispatch-evaluator.js +300 -0
- package/dist/core/dispatch-evaluator.js.map +1 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/subagent-pool.d.ts +35 -0
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +147 -7
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/subagent.d.ts +9 -1
- package/dist/core/subagent.d.ts.map +1 -1
- package/dist/core/subagent.js +23 -2
- package/dist/core/subagent.js.map +1 -1
- package/dist/core/task-store.d.ts +9 -1
- package/dist/core/task-store.d.ts.map +1 -1
- package/dist/core/task-store.js +2 -0
- package/dist/core/task-store.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +5 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +49 -8
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/extensions/core/hoo-core.d.ts +5 -2
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +3 -1
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/init-templates.generated.d.ts +1 -0
- package/dist/init-templates.generated.d.ts.map +1 -1
- package/dist/init-templates.generated.js +8 -0
- package/dist/init-templates.generated.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +2 -2
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +9 -14
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +1 -6
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +1 -5
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +4 -2
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +69 -6
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -10
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +59 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +5 -5
- package/dist/modes/interactive/theme/light.json +5 -5
- package/docs/routing.md +57 -0
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
- package/templates/agents/doc.md +35 -0
- package/templates/agents/edit.md +37 -0
- package/templates/agents/explore.md +35 -0
- package/templates/agents/review.md +36 -0
- package/templates/agents/test.md +35 -0
- package/templates/subagent/doc.md +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic subagent dispatch evaluator.
|
|
3
|
+
*
|
|
4
|
+
* Decides whether a task should be handled inline or delegated to a subagent,
|
|
5
|
+
* which subagent type to use, and whether a task should be split across
|
|
6
|
+
* multiple subagents. No LLM call — keyword + heuristic only.
|
|
7
|
+
*/
|
|
8
|
+
export type AgentType = "explore" | "edit" | "test" | "review" | "doc";
|
|
9
|
+
export interface TaskAnalysis {
|
|
10
|
+
should_delegate: boolean;
|
|
11
|
+
agent_type: AgentType | null;
|
|
12
|
+
reason: string;
|
|
13
|
+
estimated_complexity: "low" | "medium" | "high";
|
|
14
|
+
parallelizable: boolean;
|
|
15
|
+
context_needed: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface Subtask {
|
|
18
|
+
agent_type: AgentType;
|
|
19
|
+
prompt: string;
|
|
20
|
+
estimated_files: number;
|
|
21
|
+
}
|
|
22
|
+
export declare class DispatchEvaluator {
|
|
23
|
+
evaluate(task: string): TaskAnalysis;
|
|
24
|
+
shouldSplit(task: string): {
|
|
25
|
+
split: boolean;
|
|
26
|
+
subtasks: Subtask[];
|
|
27
|
+
};
|
|
28
|
+
canHandleInline(task: string): boolean;
|
|
29
|
+
getReason(analysis: TaskAnalysis): string;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=dispatch-evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch-evaluator.d.ts","sourceRoot":"","sources":["../../src/core/dispatch-evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEvE,MAAM,WAAW,YAAY;IAC5B,eAAe,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,SAAS,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAChD,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,OAAO;IACvB,UAAU,EAAE,SAAS,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;CACxB;AA8OD,qBAAa,iBAAiB;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAsCnC;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;KAAE,CAwCjE;IAED,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErC;IAED,SAAS,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAExC;CACD","sourcesContent":["/**\n * Deterministic subagent dispatch evaluator.\n *\n * Decides whether a task should be handled inline or delegated to a subagent,\n * which subagent type to use, and whether a task should be split across\n * multiple subagents. No LLM call — keyword + heuristic only.\n */\n\nexport type AgentType = \"explore\" | \"edit\" | \"test\" | \"review\" | \"doc\";\n\nexport interface TaskAnalysis {\n\tshould_delegate: boolean;\n\tagent_type: AgentType | null;\n\treason: string;\n\testimated_complexity: \"low\" | \"medium\" | \"high\";\n\tparallelizable: boolean;\n\tcontext_needed: string[];\n}\n\nexport interface Subtask {\n\tagent_type: AgentType;\n\tprompt: string;\n\testimated_files: number;\n}\n\n/* ------------------------------------------------------------------ */\n// Keyword routing tables\n\nconst EXPLORE_KEYWORDS = [\n\t\"explore\",\n\t\"understand\",\n\t\"scout\",\n\t\"investigate\",\n\t\"trace\",\n\t\"find\",\n\t\"where\",\n\t\"how does\",\n\t\"what is\",\n\t\"lookup\",\n\t\"search\",\n\t\"navigate\",\n\t\"discover\",\n\t\"map out\",\n\t\"get familiar\",\n];\n\nconst EDIT_KEYWORDS = [\n\t\"create\",\n\t\"implement\",\n\t\"refactor\",\n\t\"add\",\n\t\"build\",\n\t\"change\",\n\t\"update\",\n\t\"modify\",\n\t\"fix\",\n\t\"repair\",\n\t\"correct\",\n\t\"migrate\",\n\t\"rename\",\n\t\"remove\",\n\t\"delete\",\n\t\"write\",\n];\n\nconst TEST_KEYWORDS = [\n\t\"test\",\n\t\"validate\",\n\t\"assert\",\n\t\"coverage\",\n\t\"jest\",\n\t\"vitest\",\n\t\"mocha\",\n\t\"pytest\",\n\t\"unit test\",\n\t\"integration test\",\n\t\"e2e test\",\n\t\"regression test\",\n];\n\nconst REVIEW_KEYWORDS = [\n\t\"review\",\n\t\"audit\",\n\t\"critique\",\n\t\"security\",\n\t\"check\",\n\t\"inspect\",\n\t\"verify\",\n\t\"assess\",\n\t\"evaluate\",\n\t\"analyze for\",\n\t\"vulnerab\",\n\t\"perf audit\",\n];\n\nconst DOC_KEYWORDS = [\n\t\"readme\",\n\t\"documentation\",\n\t\"document\",\n\t\"comment\",\n\t\"explain\",\n\t\"docs\",\n\t\"guide\",\n\t\"tutorial\",\n\t\"changelog\",\n\t\"api docs\",\n];\n\nconst CROSS_DOMAIN_MARKERS = [\n\t\" and \",\n\t\" as well as \",\n\t\" plus \",\n\t\" then \",\n\t\" after that \",\n\t\" followed by \",\n\t\" in addition \",\n\t\" simultaneously \",\n];\n\n/* ------------------------------------------------------------------ */\n// Helpers\n\nfunction countMatches(text: string, keywords: readonly string[]): number {\n\tconst lower = text.toLowerCase();\n\treturn keywords.reduce((count, kw) => count + (lower.includes(kw) ? 1 : 0), 0);\n}\n\nfunction detectAgentType(task: string): AgentType | null {\n\tconst lower = task.toLowerCase();\n\tconst scores: Record<AgentType, number> = {\n\t\texplore: countMatches(task, EXPLORE_KEYWORDS),\n\t\tedit: countMatches(task, EDIT_KEYWORDS),\n\t\ttest: countMatches(task, TEST_KEYWORDS),\n\t\treview: countMatches(task, REVIEW_KEYWORDS),\n\t\tdoc: countMatches(task, DOC_KEYWORDS),\n\t};\n\n\t// Boost doc when the task is clearly about documentation\n\tif (scores.doc > 0 && (lower.includes(\"readme\") || lower.includes(\"documentation\") || lower.includes(\"document \"))) {\n\t\tscores.doc += 2;\n\t}\n\n\t// Boost test when the task is clearly about testing\n\tif (scores.test > 0 && (lower.includes(\"test\") || lower.includes(\"tests\"))) {\n\t\tscores.test += 2;\n\t}\n\n\t// Boost review for security-related tasks\n\tif (scores.review > 0 && lower.includes(\"security\")) {\n\t\tscores.review += 2;\n\t}\n\n\tlet best: AgentType | null = null;\n\tlet bestScore = 0;\n\tfor (const [type, score] of Object.entries(scores)) {\n\t\tif (score > bestScore) {\n\t\t\tbestScore = score;\n\t\t\tbest = type as AgentType;\n\t\t}\n\t}\n\treturn bestScore > 0 ? best : null;\n}\n\nfunction estimateComplexity(task: string): \"low\" | \"medium\" | \"high\" {\n\tconst fileMatches = task.match(/\\b[\\w/-]+\\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\\b/g);\n\tconst fileCount = fileMatches ? fileMatches.length : 0;\n\n\tconst lineMatch = task.match(/(\\d+)\\s*(lines?|loc)\\b/i);\n\tconst lineCount = lineMatch ? Number.parseInt(lineMatch[1], 10) : 0;\n\n\tconst highScope = /\\b(across|multiple|many|several|all files|rearchitect|redesign|migrate|restructure)\\b/i.test(\n\t\ttask,\n\t);\n\tconst mediumScope = /\\b(2|3|4|5)\\s*files?\\b/i.test(task) || /\\b(few|some|couple)\\b/i.test(task);\n\n\tif (lineCount > 200 || fileCount >= 4 || highScope) return \"high\";\n\tif (lineCount > 50 || fileCount >= 2 || mediumScope) return \"medium\";\n\treturn \"low\";\n}\n\nfunction canHandleInline(task: string): boolean {\n\tif (estimateComplexity(task) !== \"low\") return false;\n\n\tconst fileMatches = task.match(/\\b[\\w/-]+\\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\\b/g);\n\tif (fileMatches && fileMatches.length > 1) return false;\n\n\tconst hasCrossDomain = CROSS_DOMAIN_MARKERS.some((m) => task.toLowerCase().includes(m));\n\tif (hasCrossDomain) return false;\n\n\t// Exploration: broad tasks delegate; simple lookups can be inline\n\tconst isExplore = detectAgentType(task) === \"explore\";\n\tif (isExplore) {\n\t\tconst broadExplore = /\\b(understand|investigate|trace|how does|how is|scout|map out|get familiar)\\b/i.test(task);\n\t\treturn !broadExplore;\n\t}\n\n\t// Documentation tasks always delegate to the doc subagent\n\tif (detectAgentType(task) === \"doc\") {\n\t\treturn false;\n\t}\n\n\tconst isReadOnly = countMatches(task, EXPLORE_KEYWORDS) > 0 && countMatches(task, EDIT_KEYWORDS) === 0;\n\tconst isTrivialEdit =\n\t\tcountMatches(task, EDIT_KEYWORDS) > 0 && !/\\b(create|implement|build|refactor|migrate|restructure)\\b/i.test(task);\n\n\treturn isReadOnly || isTrivialEdit;\n}\n\nfunction extractSubtasks(task: string): Subtask[] {\n\t// Split on sentence boundaries and conjunctions, then classify each segment.\n\tconst segments = task\n\t\t.split(/(?:[,;]|\\.(?:\\s+|$))\\s*/)\n\t\t.map((s) => s.trim())\n\t\t.filter((s) => s.length > 10);\n\n\tif (segments.length < 2) {\n\t\t// No obvious sentence split — try cross-domain markers\n\t\tconst parts: string[] = [];\n\t\tlet remaining = task;\n\t\tfor (const marker of CROSS_DOMAIN_MARKERS) {\n\t\t\tconst idx = remaining.toLowerCase().indexOf(marker);\n\t\t\tif (idx !== -1) {\n\t\t\t\tparts.push(remaining.slice(0, idx).trim());\n\t\t\t\tremaining = remaining.slice(idx + marker.length).trim();\n\t\t\t}\n\t\t}\n\t\tif (parts.length > 0) {\n\t\t\tparts.push(remaining);\n\t\t\treturn parts\n\t\t\t\t.map((p) => {\n\t\t\t\t\tconst type = detectAgentType(p);\n\t\t\t\t\tif (!type) return null;\n\t\t\t\t\tconst est = estimateComplexity(p);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tagent_type: type,\n\t\t\t\t\t\tprompt: p,\n\t\t\t\t\t\testimated_files: est === \"high\" ? 4 : est === \"medium\" ? 2 : 1,\n\t\t\t\t\t};\n\t\t\t\t})\n\t\t\t\t.filter((s): s is Subtask => s !== null);\n\t\t}\n\t\treturn [];\n\t}\n\n\treturn segments\n\t\t.map((segment) => {\n\t\t\tconst type = detectAgentType(segment);\n\t\t\tif (!type) return null;\n\t\t\tconst est = estimateComplexity(segment);\n\t\t\treturn {\n\t\t\t\tagent_type: type,\n\t\t\t\tprompt: segment,\n\t\t\t\testimated_files: est === \"high\" ? 4 : est === \"medium\" ? 2 : 1,\n\t\t\t};\n\t\t})\n\t\t.filter((s): s is Subtask => s !== null);\n}\n\n/* ------------------------------------------------------------------ */\n// Evaluator\n\nexport class DispatchEvaluator {\n\tevaluate(task: string): TaskAnalysis {\n\t\tconst depth = Number.parseInt(process.env.HOOCODE_SUBAGENT_DEPTH ?? \"0\", 10);\n\t\tif (depth >= 1) {\n\t\t\treturn {\n\t\t\t\tshould_delegate: false,\n\t\t\t\tagent_type: null,\n\t\t\t\treason: \"Subagents cannot spawn subagents\",\n\t\t\t\testimated_complexity: \"low\",\n\t\t\t\tparallelizable: false,\n\t\t\t\tcontext_needed: [],\n\t\t\t};\n\t\t}\n\n\t\tconst agentType = detectAgentType(task);\n\t\tconst complexity = estimateComplexity(task);\n\t\tconst inline = canHandleInline(task);\n\t\tconst subtasks = extractSubtasks(task);\n\t\tconst parallelizable = subtasks.length > 1 || (complexity === \"high\" && subtasks.length > 0);\n\n\t\tif (inline) {\n\t\t\treturn {\n\t\t\t\tshould_delegate: false,\n\t\t\t\tagent_type: null,\n\t\t\t\treason: `Simple ${agentType ?? \"task\"} suitable for inline handling (<50 lines, 1 file, no cross-domain)`,\n\t\t\t\testimated_complexity: complexity,\n\t\t\t\tparallelizable: false,\n\t\t\t\tcontext_needed: [],\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tshould_delegate: true,\n\t\t\tagent_type: agentType,\n\t\t\treason: `${agentType ?? \"general\"} task with ${complexity} complexity requires isolated subagent`,\n\t\t\testimated_complexity: complexity,\n\t\t\tparallelizable,\n\t\t\tcontext_needed: parallelizable ? subtasks.map((st) => st.prompt) : [task],\n\t\t};\n\t}\n\n\tshouldSplit(task: string): { split: boolean; subtasks: Subtask[] } {\n\t\tconst subtasks = extractSubtasks(task);\n\t\tif (subtasks.length >= 2) {\n\t\t\treturn { split: true, subtasks };\n\t\t}\n\n\t\t// Check for explicit multi-domain keywords even when sentence splitting failed\n\t\tconst multiDomain =\n\t\t\t/\\b(implement|write|create|refactor|fix|test|review|document|explore)\\b.*\\b(and|also|plus|then|followed by)\\b.*\\b(test|review|document|explore|implement|write|create|refactor|fix)\\b/i.test(\n\t\t\t\ttask,\n\t\t\t);\n\t\tif (!multiDomain) return { split: false, subtasks: [] };\n\n\t\t// Force split using cross-domain markers\n\t\tconst parts: string[] = [];\n\t\tlet remaining = task;\n\t\tfor (const marker of CROSS_DOMAIN_MARKERS) {\n\t\t\tconst idx = remaining.toLowerCase().indexOf(marker);\n\t\t\tif (idx !== -1) {\n\t\t\t\tparts.push(remaining.slice(0, idx).trim());\n\t\t\t\tremaining = remaining.slice(idx + marker.length).trim();\n\t\t\t}\n\t\t}\n\t\tif (parts.length === 0) return { split: false, subtasks: [] };\n\t\tparts.push(remaining);\n\n\t\tconst forcedSubtasks = parts\n\t\t\t.map((p) => {\n\t\t\t\tconst type = detectAgentType(p);\n\t\t\t\tif (!type) return null;\n\t\t\t\tconst est = estimateComplexity(p);\n\t\t\t\treturn {\n\t\t\t\t\tagent_type: type,\n\t\t\t\t\tprompt: p,\n\t\t\t\t\testimated_files: est === \"high\" ? 4 : est === \"medium\" ? 2 : 1,\n\t\t\t\t};\n\t\t\t})\n\t\t\t.filter((s): s is Subtask => s !== null);\n\n\t\treturn forcedSubtasks.length >= 2 ? { split: true, subtasks: forcedSubtasks } : { split: false, subtasks: [] };\n\t}\n\n\tcanHandleInline(task: string): boolean {\n\t\treturn canHandleInline(task);\n\t}\n\n\tgetReason(analysis: TaskAnalysis): string {\n\t\treturn analysis.reason;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic subagent dispatch evaluator.
|
|
3
|
+
*
|
|
4
|
+
* Decides whether a task should be handled inline or delegated to a subagent,
|
|
5
|
+
* which subagent type to use, and whether a task should be split across
|
|
6
|
+
* multiple subagents. No LLM call — keyword + heuristic only.
|
|
7
|
+
*/
|
|
8
|
+
/* ------------------------------------------------------------------ */
|
|
9
|
+
// Keyword routing tables
|
|
10
|
+
const EXPLORE_KEYWORDS = [
|
|
11
|
+
"explore",
|
|
12
|
+
"understand",
|
|
13
|
+
"scout",
|
|
14
|
+
"investigate",
|
|
15
|
+
"trace",
|
|
16
|
+
"find",
|
|
17
|
+
"where",
|
|
18
|
+
"how does",
|
|
19
|
+
"what is",
|
|
20
|
+
"lookup",
|
|
21
|
+
"search",
|
|
22
|
+
"navigate",
|
|
23
|
+
"discover",
|
|
24
|
+
"map out",
|
|
25
|
+
"get familiar",
|
|
26
|
+
];
|
|
27
|
+
const EDIT_KEYWORDS = [
|
|
28
|
+
"create",
|
|
29
|
+
"implement",
|
|
30
|
+
"refactor",
|
|
31
|
+
"add",
|
|
32
|
+
"build",
|
|
33
|
+
"change",
|
|
34
|
+
"update",
|
|
35
|
+
"modify",
|
|
36
|
+
"fix",
|
|
37
|
+
"repair",
|
|
38
|
+
"correct",
|
|
39
|
+
"migrate",
|
|
40
|
+
"rename",
|
|
41
|
+
"remove",
|
|
42
|
+
"delete",
|
|
43
|
+
"write",
|
|
44
|
+
];
|
|
45
|
+
const TEST_KEYWORDS = [
|
|
46
|
+
"test",
|
|
47
|
+
"validate",
|
|
48
|
+
"assert",
|
|
49
|
+
"coverage",
|
|
50
|
+
"jest",
|
|
51
|
+
"vitest",
|
|
52
|
+
"mocha",
|
|
53
|
+
"pytest",
|
|
54
|
+
"unit test",
|
|
55
|
+
"integration test",
|
|
56
|
+
"e2e test",
|
|
57
|
+
"regression test",
|
|
58
|
+
];
|
|
59
|
+
const REVIEW_KEYWORDS = [
|
|
60
|
+
"review",
|
|
61
|
+
"audit",
|
|
62
|
+
"critique",
|
|
63
|
+
"security",
|
|
64
|
+
"check",
|
|
65
|
+
"inspect",
|
|
66
|
+
"verify",
|
|
67
|
+
"assess",
|
|
68
|
+
"evaluate",
|
|
69
|
+
"analyze for",
|
|
70
|
+
"vulnerab",
|
|
71
|
+
"perf audit",
|
|
72
|
+
];
|
|
73
|
+
const DOC_KEYWORDS = [
|
|
74
|
+
"readme",
|
|
75
|
+
"documentation",
|
|
76
|
+
"document",
|
|
77
|
+
"comment",
|
|
78
|
+
"explain",
|
|
79
|
+
"docs",
|
|
80
|
+
"guide",
|
|
81
|
+
"tutorial",
|
|
82
|
+
"changelog",
|
|
83
|
+
"api docs",
|
|
84
|
+
];
|
|
85
|
+
const CROSS_DOMAIN_MARKERS = [
|
|
86
|
+
" and ",
|
|
87
|
+
" as well as ",
|
|
88
|
+
" plus ",
|
|
89
|
+
" then ",
|
|
90
|
+
" after that ",
|
|
91
|
+
" followed by ",
|
|
92
|
+
" in addition ",
|
|
93
|
+
" simultaneously ",
|
|
94
|
+
];
|
|
95
|
+
/* ------------------------------------------------------------------ */
|
|
96
|
+
// Helpers
|
|
97
|
+
function countMatches(text, keywords) {
|
|
98
|
+
const lower = text.toLowerCase();
|
|
99
|
+
return keywords.reduce((count, kw) => count + (lower.includes(kw) ? 1 : 0), 0);
|
|
100
|
+
}
|
|
101
|
+
function detectAgentType(task) {
|
|
102
|
+
const lower = task.toLowerCase();
|
|
103
|
+
const scores = {
|
|
104
|
+
explore: countMatches(task, EXPLORE_KEYWORDS),
|
|
105
|
+
edit: countMatches(task, EDIT_KEYWORDS),
|
|
106
|
+
test: countMatches(task, TEST_KEYWORDS),
|
|
107
|
+
review: countMatches(task, REVIEW_KEYWORDS),
|
|
108
|
+
doc: countMatches(task, DOC_KEYWORDS),
|
|
109
|
+
};
|
|
110
|
+
// Boost doc when the task is clearly about documentation
|
|
111
|
+
if (scores.doc > 0 && (lower.includes("readme") || lower.includes("documentation") || lower.includes("document "))) {
|
|
112
|
+
scores.doc += 2;
|
|
113
|
+
}
|
|
114
|
+
// Boost test when the task is clearly about testing
|
|
115
|
+
if (scores.test > 0 && (lower.includes("test") || lower.includes("tests"))) {
|
|
116
|
+
scores.test += 2;
|
|
117
|
+
}
|
|
118
|
+
// Boost review for security-related tasks
|
|
119
|
+
if (scores.review > 0 && lower.includes("security")) {
|
|
120
|
+
scores.review += 2;
|
|
121
|
+
}
|
|
122
|
+
let best = null;
|
|
123
|
+
let bestScore = 0;
|
|
124
|
+
for (const [type, score] of Object.entries(scores)) {
|
|
125
|
+
if (score > bestScore) {
|
|
126
|
+
bestScore = score;
|
|
127
|
+
best = type;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return bestScore > 0 ? best : null;
|
|
131
|
+
}
|
|
132
|
+
function estimateComplexity(task) {
|
|
133
|
+
const fileMatches = task.match(/\b[\w/-]+\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\b/g);
|
|
134
|
+
const fileCount = fileMatches ? fileMatches.length : 0;
|
|
135
|
+
const lineMatch = task.match(/(\d+)\s*(lines?|loc)\b/i);
|
|
136
|
+
const lineCount = lineMatch ? Number.parseInt(lineMatch[1], 10) : 0;
|
|
137
|
+
const highScope = /\b(across|multiple|many|several|all files|rearchitect|redesign|migrate|restructure)\b/i.test(task);
|
|
138
|
+
const mediumScope = /\b(2|3|4|5)\s*files?\b/i.test(task) || /\b(few|some|couple)\b/i.test(task);
|
|
139
|
+
if (lineCount > 200 || fileCount >= 4 || highScope)
|
|
140
|
+
return "high";
|
|
141
|
+
if (lineCount > 50 || fileCount >= 2 || mediumScope)
|
|
142
|
+
return "medium";
|
|
143
|
+
return "low";
|
|
144
|
+
}
|
|
145
|
+
function canHandleInline(task) {
|
|
146
|
+
if (estimateComplexity(task) !== "low")
|
|
147
|
+
return false;
|
|
148
|
+
const fileMatches = task.match(/\b[\w/-]+\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\b/g);
|
|
149
|
+
if (fileMatches && fileMatches.length > 1)
|
|
150
|
+
return false;
|
|
151
|
+
const hasCrossDomain = CROSS_DOMAIN_MARKERS.some((m) => task.toLowerCase().includes(m));
|
|
152
|
+
if (hasCrossDomain)
|
|
153
|
+
return false;
|
|
154
|
+
// Exploration: broad tasks delegate; simple lookups can be inline
|
|
155
|
+
const isExplore = detectAgentType(task) === "explore";
|
|
156
|
+
if (isExplore) {
|
|
157
|
+
const broadExplore = /\b(understand|investigate|trace|how does|how is|scout|map out|get familiar)\b/i.test(task);
|
|
158
|
+
return !broadExplore;
|
|
159
|
+
}
|
|
160
|
+
// Documentation tasks always delegate to the doc subagent
|
|
161
|
+
if (detectAgentType(task) === "doc") {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const isReadOnly = countMatches(task, EXPLORE_KEYWORDS) > 0 && countMatches(task, EDIT_KEYWORDS) === 0;
|
|
165
|
+
const isTrivialEdit = countMatches(task, EDIT_KEYWORDS) > 0 && !/\b(create|implement|build|refactor|migrate|restructure)\b/i.test(task);
|
|
166
|
+
return isReadOnly || isTrivialEdit;
|
|
167
|
+
}
|
|
168
|
+
function extractSubtasks(task) {
|
|
169
|
+
// Split on sentence boundaries and conjunctions, then classify each segment.
|
|
170
|
+
const segments = task
|
|
171
|
+
.split(/(?:[,;]|\.(?:\s+|$))\s*/)
|
|
172
|
+
.map((s) => s.trim())
|
|
173
|
+
.filter((s) => s.length > 10);
|
|
174
|
+
if (segments.length < 2) {
|
|
175
|
+
// No obvious sentence split — try cross-domain markers
|
|
176
|
+
const parts = [];
|
|
177
|
+
let remaining = task;
|
|
178
|
+
for (const marker of CROSS_DOMAIN_MARKERS) {
|
|
179
|
+
const idx = remaining.toLowerCase().indexOf(marker);
|
|
180
|
+
if (idx !== -1) {
|
|
181
|
+
parts.push(remaining.slice(0, idx).trim());
|
|
182
|
+
remaining = remaining.slice(idx + marker.length).trim();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (parts.length > 0) {
|
|
186
|
+
parts.push(remaining);
|
|
187
|
+
return parts
|
|
188
|
+
.map((p) => {
|
|
189
|
+
const type = detectAgentType(p);
|
|
190
|
+
if (!type)
|
|
191
|
+
return null;
|
|
192
|
+
const est = estimateComplexity(p);
|
|
193
|
+
return {
|
|
194
|
+
agent_type: type,
|
|
195
|
+
prompt: p,
|
|
196
|
+
estimated_files: est === "high" ? 4 : est === "medium" ? 2 : 1,
|
|
197
|
+
};
|
|
198
|
+
})
|
|
199
|
+
.filter((s) => s !== null);
|
|
200
|
+
}
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
return segments
|
|
204
|
+
.map((segment) => {
|
|
205
|
+
const type = detectAgentType(segment);
|
|
206
|
+
if (!type)
|
|
207
|
+
return null;
|
|
208
|
+
const est = estimateComplexity(segment);
|
|
209
|
+
return {
|
|
210
|
+
agent_type: type,
|
|
211
|
+
prompt: segment,
|
|
212
|
+
estimated_files: est === "high" ? 4 : est === "medium" ? 2 : 1,
|
|
213
|
+
};
|
|
214
|
+
})
|
|
215
|
+
.filter((s) => s !== null);
|
|
216
|
+
}
|
|
217
|
+
/* ------------------------------------------------------------------ */
|
|
218
|
+
// Evaluator
|
|
219
|
+
export class DispatchEvaluator {
|
|
220
|
+
evaluate(task) {
|
|
221
|
+
const depth = Number.parseInt(process.env.HOOCODE_SUBAGENT_DEPTH ?? "0", 10);
|
|
222
|
+
if (depth >= 1) {
|
|
223
|
+
return {
|
|
224
|
+
should_delegate: false,
|
|
225
|
+
agent_type: null,
|
|
226
|
+
reason: "Subagents cannot spawn subagents",
|
|
227
|
+
estimated_complexity: "low",
|
|
228
|
+
parallelizable: false,
|
|
229
|
+
context_needed: [],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const agentType = detectAgentType(task);
|
|
233
|
+
const complexity = estimateComplexity(task);
|
|
234
|
+
const inline = canHandleInline(task);
|
|
235
|
+
const subtasks = extractSubtasks(task);
|
|
236
|
+
const parallelizable = subtasks.length > 1 || (complexity === "high" && subtasks.length > 0);
|
|
237
|
+
if (inline) {
|
|
238
|
+
return {
|
|
239
|
+
should_delegate: false,
|
|
240
|
+
agent_type: null,
|
|
241
|
+
reason: `Simple ${agentType ?? "task"} suitable for inline handling (<50 lines, 1 file, no cross-domain)`,
|
|
242
|
+
estimated_complexity: complexity,
|
|
243
|
+
parallelizable: false,
|
|
244
|
+
context_needed: [],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
should_delegate: true,
|
|
249
|
+
agent_type: agentType,
|
|
250
|
+
reason: `${agentType ?? "general"} task with ${complexity} complexity requires isolated subagent`,
|
|
251
|
+
estimated_complexity: complexity,
|
|
252
|
+
parallelizable,
|
|
253
|
+
context_needed: parallelizable ? subtasks.map((st) => st.prompt) : [task],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
shouldSplit(task) {
|
|
257
|
+
const subtasks = extractSubtasks(task);
|
|
258
|
+
if (subtasks.length >= 2) {
|
|
259
|
+
return { split: true, subtasks };
|
|
260
|
+
}
|
|
261
|
+
// Check for explicit multi-domain keywords even when sentence splitting failed
|
|
262
|
+
const multiDomain = /\b(implement|write|create|refactor|fix|test|review|document|explore)\b.*\b(and|also|plus|then|followed by)\b.*\b(test|review|document|explore|implement|write|create|refactor|fix)\b/i.test(task);
|
|
263
|
+
if (!multiDomain)
|
|
264
|
+
return { split: false, subtasks: [] };
|
|
265
|
+
// Force split using cross-domain markers
|
|
266
|
+
const parts = [];
|
|
267
|
+
let remaining = task;
|
|
268
|
+
for (const marker of CROSS_DOMAIN_MARKERS) {
|
|
269
|
+
const idx = remaining.toLowerCase().indexOf(marker);
|
|
270
|
+
if (idx !== -1) {
|
|
271
|
+
parts.push(remaining.slice(0, idx).trim());
|
|
272
|
+
remaining = remaining.slice(idx + marker.length).trim();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (parts.length === 0)
|
|
276
|
+
return { split: false, subtasks: [] };
|
|
277
|
+
parts.push(remaining);
|
|
278
|
+
const forcedSubtasks = parts
|
|
279
|
+
.map((p) => {
|
|
280
|
+
const type = detectAgentType(p);
|
|
281
|
+
if (!type)
|
|
282
|
+
return null;
|
|
283
|
+
const est = estimateComplexity(p);
|
|
284
|
+
return {
|
|
285
|
+
agent_type: type,
|
|
286
|
+
prompt: p,
|
|
287
|
+
estimated_files: est === "high" ? 4 : est === "medium" ? 2 : 1,
|
|
288
|
+
};
|
|
289
|
+
})
|
|
290
|
+
.filter((s) => s !== null);
|
|
291
|
+
return forcedSubtasks.length >= 2 ? { split: true, subtasks: forcedSubtasks } : { split: false, subtasks: [] };
|
|
292
|
+
}
|
|
293
|
+
canHandleInline(task) {
|
|
294
|
+
return canHandleInline(task);
|
|
295
|
+
}
|
|
296
|
+
getReason(analysis) {
|
|
297
|
+
return analysis.reason;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=dispatch-evaluator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch-evaluator.js","sourceRoot":"","sources":["../../src/core/dispatch-evaluator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Deterministic subagent dispatch evaluator.\n *\n * Decides whether a task should be handled inline or delegated to a subagent,\n * which subagent type to use, and whether a task should be split across\n * multiple subagents. No LLM call — keyword + heuristic only.\n */\n\nexport type AgentType = \"explore\" | \"edit\" | \"test\" | \"review\" | \"doc\";\n\nexport interface TaskAnalysis {\n\tshould_delegate: boolean;\n\tagent_type: AgentType | null;\n\treason: string;\n\testimated_complexity: \"low\" | \"medium\" | \"high\";\n\tparallelizable: boolean;\n\tcontext_needed: string[];\n}\n\nexport interface Subtask {\n\tagent_type: AgentType;\n\tprompt: string;\n\testimated_files: number;\n}\n\n/* ------------------------------------------------------------------ */\n// Keyword routing tables\n\nconst EXPLORE_KEYWORDS = [\n\t\"explore\",\n\t\"understand\",\n\t\"scout\",\n\t\"investigate\",\n\t\"trace\",\n\t\"find\",\n\t\"where\",\n\t\"how does\",\n\t\"what is\",\n\t\"lookup\",\n\t\"search\",\n\t\"navigate\",\n\t\"discover\",\n\t\"map out\",\n\t\"get familiar\",\n];\n\nconst EDIT_KEYWORDS = [\n\t\"create\",\n\t\"implement\",\n\t\"refactor\",\n\t\"add\",\n\t\"build\",\n\t\"change\",\n\t\"update\",\n\t\"modify\",\n\t\"fix\",\n\t\"repair\",\n\t\"correct\",\n\t\"migrate\",\n\t\"rename\",\n\t\"remove\",\n\t\"delete\",\n\t\"write\",\n];\n\nconst TEST_KEYWORDS = [\n\t\"test\",\n\t\"validate\",\n\t\"assert\",\n\t\"coverage\",\n\t\"jest\",\n\t\"vitest\",\n\t\"mocha\",\n\t\"pytest\",\n\t\"unit test\",\n\t\"integration test\",\n\t\"e2e test\",\n\t\"regression test\",\n];\n\nconst REVIEW_KEYWORDS = [\n\t\"review\",\n\t\"audit\",\n\t\"critique\",\n\t\"security\",\n\t\"check\",\n\t\"inspect\",\n\t\"verify\",\n\t\"assess\",\n\t\"evaluate\",\n\t\"analyze for\",\n\t\"vulnerab\",\n\t\"perf audit\",\n];\n\nconst DOC_KEYWORDS = [\n\t\"readme\",\n\t\"documentation\",\n\t\"document\",\n\t\"comment\",\n\t\"explain\",\n\t\"docs\",\n\t\"guide\",\n\t\"tutorial\",\n\t\"changelog\",\n\t\"api docs\",\n];\n\nconst CROSS_DOMAIN_MARKERS = [\n\t\" and \",\n\t\" as well as \",\n\t\" plus \",\n\t\" then \",\n\t\" after that \",\n\t\" followed by \",\n\t\" in addition \",\n\t\" simultaneously \",\n];\n\n/* ------------------------------------------------------------------ */\n// Helpers\n\nfunction countMatches(text: string, keywords: readonly string[]): number {\n\tconst lower = text.toLowerCase();\n\treturn keywords.reduce((count, kw) => count + (lower.includes(kw) ? 1 : 0), 0);\n}\n\nfunction detectAgentType(task: string): AgentType | null {\n\tconst lower = task.toLowerCase();\n\tconst scores: Record<AgentType, number> = {\n\t\texplore: countMatches(task, EXPLORE_KEYWORDS),\n\t\tedit: countMatches(task, EDIT_KEYWORDS),\n\t\ttest: countMatches(task, TEST_KEYWORDS),\n\t\treview: countMatches(task, REVIEW_KEYWORDS),\n\t\tdoc: countMatches(task, DOC_KEYWORDS),\n\t};\n\n\t// Boost doc when the task is clearly about documentation\n\tif (scores.doc > 0 && (lower.includes(\"readme\") || lower.includes(\"documentation\") || lower.includes(\"document \"))) {\n\t\tscores.doc += 2;\n\t}\n\n\t// Boost test when the task is clearly about testing\n\tif (scores.test > 0 && (lower.includes(\"test\") || lower.includes(\"tests\"))) {\n\t\tscores.test += 2;\n\t}\n\n\t// Boost review for security-related tasks\n\tif (scores.review > 0 && lower.includes(\"security\")) {\n\t\tscores.review += 2;\n\t}\n\n\tlet best: AgentType | null = null;\n\tlet bestScore = 0;\n\tfor (const [type, score] of Object.entries(scores)) {\n\t\tif (score > bestScore) {\n\t\t\tbestScore = score;\n\t\t\tbest = type as AgentType;\n\t\t}\n\t}\n\treturn bestScore > 0 ? best : null;\n}\n\nfunction estimateComplexity(task: string): \"low\" | \"medium\" | \"high\" {\n\tconst fileMatches = task.match(/\\b[\\w/-]+\\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\\b/g);\n\tconst fileCount = fileMatches ? fileMatches.length : 0;\n\n\tconst lineMatch = task.match(/(\\d+)\\s*(lines?|loc)\\b/i);\n\tconst lineCount = lineMatch ? Number.parseInt(lineMatch[1], 10) : 0;\n\n\tconst highScope = /\\b(across|multiple|many|several|all files|rearchitect|redesign|migrate|restructure)\\b/i.test(\n\t\ttask,\n\t);\n\tconst mediumScope = /\\b(2|3|4|5)\\s*files?\\b/i.test(task) || /\\b(few|some|couple)\\b/i.test(task);\n\n\tif (lineCount > 200 || fileCount >= 4 || highScope) return \"high\";\n\tif (lineCount > 50 || fileCount >= 2 || mediumScope) return \"medium\";\n\treturn \"low\";\n}\n\nfunction canHandleInline(task: string): boolean {\n\tif (estimateComplexity(task) !== \"low\") return false;\n\n\tconst fileMatches = task.match(/\\b[\\w/-]+\\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\\b/g);\n\tif (fileMatches && fileMatches.length > 1) return false;\n\n\tconst hasCrossDomain = CROSS_DOMAIN_MARKERS.some((m) => task.toLowerCase().includes(m));\n\tif (hasCrossDomain) return false;\n\n\t// Exploration: broad tasks delegate; simple lookups can be inline\n\tconst isExplore = detectAgentType(task) === \"explore\";\n\tif (isExplore) {\n\t\tconst broadExplore = /\\b(understand|investigate|trace|how does|how is|scout|map out|get familiar)\\b/i.test(task);\n\t\treturn !broadExplore;\n\t}\n\n\t// Documentation tasks always delegate to the doc subagent\n\tif (detectAgentType(task) === \"doc\") {\n\t\treturn false;\n\t}\n\n\tconst isReadOnly = countMatches(task, EXPLORE_KEYWORDS) > 0 && countMatches(task, EDIT_KEYWORDS) === 0;\n\tconst isTrivialEdit =\n\t\tcountMatches(task, EDIT_KEYWORDS) > 0 && !/\\b(create|implement|build|refactor|migrate|restructure)\\b/i.test(task);\n\n\treturn isReadOnly || isTrivialEdit;\n}\n\nfunction extractSubtasks(task: string): Subtask[] {\n\t// Split on sentence boundaries and conjunctions, then classify each segment.\n\tconst segments = task\n\t\t.split(/(?:[,;]|\\.(?:\\s+|$))\\s*/)\n\t\t.map((s) => s.trim())\n\t\t.filter((s) => s.length > 10);\n\n\tif (segments.length < 2) {\n\t\t// No obvious sentence split — try cross-domain markers\n\t\tconst parts: string[] = [];\n\t\tlet remaining = task;\n\t\tfor (const marker of CROSS_DOMAIN_MARKERS) {\n\t\t\tconst idx = remaining.toLowerCase().indexOf(marker);\n\t\t\tif (idx !== -1) {\n\t\t\t\tparts.push(remaining.slice(0, idx).trim());\n\t\t\t\tremaining = remaining.slice(idx + marker.length).trim();\n\t\t\t}\n\t\t}\n\t\tif (parts.length > 0) {\n\t\t\tparts.push(remaining);\n\t\t\treturn parts\n\t\t\t\t.map((p) => {\n\t\t\t\t\tconst type = detectAgentType(p);\n\t\t\t\t\tif (!type) return null;\n\t\t\t\t\tconst est = estimateComplexity(p);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tagent_type: type,\n\t\t\t\t\t\tprompt: p,\n\t\t\t\t\t\testimated_files: est === \"high\" ? 4 : est === \"medium\" ? 2 : 1,\n\t\t\t\t\t};\n\t\t\t\t})\n\t\t\t\t.filter((s): s is Subtask => s !== null);\n\t\t}\n\t\treturn [];\n\t}\n\n\treturn segments\n\t\t.map((segment) => {\n\t\t\tconst type = detectAgentType(segment);\n\t\t\tif (!type) return null;\n\t\t\tconst est = estimateComplexity(segment);\n\t\t\treturn {\n\t\t\t\tagent_type: type,\n\t\t\t\tprompt: segment,\n\t\t\t\testimated_files: est === \"high\" ? 4 : est === \"medium\" ? 2 : 1,\n\t\t\t};\n\t\t})\n\t\t.filter((s): s is Subtask => s !== null);\n}\n\n/* ------------------------------------------------------------------ */\n// Evaluator\n\nexport class DispatchEvaluator {\n\tevaluate(task: string): TaskAnalysis {\n\t\tconst depth = Number.parseInt(process.env.HOOCODE_SUBAGENT_DEPTH ?? \"0\", 10);\n\t\tif (depth >= 1) {\n\t\t\treturn {\n\t\t\t\tshould_delegate: false,\n\t\t\t\tagent_type: null,\n\t\t\t\treason: \"Subagents cannot spawn subagents\",\n\t\t\t\testimated_complexity: \"low\",\n\t\t\t\tparallelizable: false,\n\t\t\t\tcontext_needed: [],\n\t\t\t};\n\t\t}\n\n\t\tconst agentType = detectAgentType(task);\n\t\tconst complexity = estimateComplexity(task);\n\t\tconst inline = canHandleInline(task);\n\t\tconst subtasks = extractSubtasks(task);\n\t\tconst parallelizable = subtasks.length > 1 || (complexity === \"high\" && subtasks.length > 0);\n\n\t\tif (inline) {\n\t\t\treturn {\n\t\t\t\tshould_delegate: false,\n\t\t\t\tagent_type: null,\n\t\t\t\treason: `Simple ${agentType ?? \"task\"} suitable for inline handling (<50 lines, 1 file, no cross-domain)`,\n\t\t\t\testimated_complexity: complexity,\n\t\t\t\tparallelizable: false,\n\t\t\t\tcontext_needed: [],\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tshould_delegate: true,\n\t\t\tagent_type: agentType,\n\t\t\treason: `${agentType ?? \"general\"} task with ${complexity} complexity requires isolated subagent`,\n\t\t\testimated_complexity: complexity,\n\t\t\tparallelizable,\n\t\t\tcontext_needed: parallelizable ? subtasks.map((st) => st.prompt) : [task],\n\t\t};\n\t}\n\n\tshouldSplit(task: string): { split: boolean; subtasks: Subtask[] } {\n\t\tconst subtasks = extractSubtasks(task);\n\t\tif (subtasks.length >= 2) {\n\t\t\treturn { split: true, subtasks };\n\t\t}\n\n\t\t// Check for explicit multi-domain keywords even when sentence splitting failed\n\t\tconst multiDomain =\n\t\t\t/\\b(implement|write|create|refactor|fix|test|review|document|explore)\\b.*\\b(and|also|plus|then|followed by)\\b.*\\b(test|review|document|explore|implement|write|create|refactor|fix)\\b/i.test(\n\t\t\t\ttask,\n\t\t\t);\n\t\tif (!multiDomain) return { split: false, subtasks: [] };\n\n\t\t// Force split using cross-domain markers\n\t\tconst parts: string[] = [];\n\t\tlet remaining = task;\n\t\tfor (const marker of CROSS_DOMAIN_MARKERS) {\n\t\t\tconst idx = remaining.toLowerCase().indexOf(marker);\n\t\t\tif (idx !== -1) {\n\t\t\t\tparts.push(remaining.slice(0, idx).trim());\n\t\t\t\tremaining = remaining.slice(idx + marker.length).trim();\n\t\t\t}\n\t\t}\n\t\tif (parts.length === 0) return { split: false, subtasks: [] };\n\t\tparts.push(remaining);\n\n\t\tconst forcedSubtasks = parts\n\t\t\t.map((p) => {\n\t\t\t\tconst type = detectAgentType(p);\n\t\t\t\tif (!type) return null;\n\t\t\t\tconst est = estimateComplexity(p);\n\t\t\t\treturn {\n\t\t\t\t\tagent_type: type,\n\t\t\t\t\tprompt: p,\n\t\t\t\t\testimated_files: est === \"high\" ? 4 : est === \"medium\" ? 2 : 1,\n\t\t\t\t};\n\t\t\t})\n\t\t\t.filter((s): s is Subtask => s !== null);\n\n\t\treturn forcedSubtasks.length >= 2 ? { split: true, subtasks: forcedSubtasks } : { split: false, subtasks: [] };\n\t}\n\n\tcanHandleInline(task: string): boolean {\n\t\treturn canHandleInline(task);\n\t}\n\n\tgetReason(analysis: TaskAnalysis): string {\n\t\treturn analysis.reason;\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,
|
|
1
|
+
{"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,CAuBrE,CAAC","sourcesContent":["import { APP_NAME } from \"../config.js\";\nimport type { SourceInfo } from \"./source-info.js\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message\" },\n\t{ name: \"clone\", description: \"Duplicate the current session at the current position\" },\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n\t{ name: \"subagent\", description: \"Spawn a subagent directly: /subagent <mode> <task>\" },\n];\n"]}
|
|
@@ -21,5 +21,6 @@ export const BUILTIN_SLASH_COMMANDS = [
|
|
|
21
21
|
{ name: "resume", description: "Resume a different session" },
|
|
22
22
|
{ name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes" },
|
|
23
23
|
{ name: "quit", description: `Quit ${APP_NAME}` },
|
|
24
|
+
{ name: "subagent", description: "Spawn a subagent directly: /subagent <mode> <task>" },
|
|
24
25
|
];
|
|
25
26
|
//# sourceMappingURL=slash-commands.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAiBxC,MAAM,CAAC,MAAM,sBAAsB,GAAuC;IACzE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oBAAoB,EAAE;IACvD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,kCAAkC,EAAE;IAClE,EAAE,IAAI,EAAE,eAAe,EAAE,WAAW,EAAE,0CAA0C,EAAE;IAClF,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,8DAA8D,EAAE;IAC/F,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAChF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACvE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACzD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE;IAC5D,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,gDAAgD,EAAE;IAC/E,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uDAAuD,EAAE;IACvF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACxE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,mCAAmC,EAAE;IACnE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gCAAgC,EAAE;IACjE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE;IACnD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACxE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAC7D,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,6DAA6D,EAAE;IAC9F,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,QAAQ,EAAE,EAAE;
|
|
1
|
+
{"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAiBxC,MAAM,CAAC,MAAM,sBAAsB,GAAuC;IACzE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oBAAoB,EAAE;IACvD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,kCAAkC,EAAE;IAClE,EAAE,IAAI,EAAE,eAAe,EAAE,WAAW,EAAE,0CAA0C,EAAE;IAClF,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,8DAA8D,EAAE;IAC/F,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAChF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACvE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACzD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE;IAC5D,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,gDAAgD,EAAE;IAC/E,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uDAAuD,EAAE;IACvF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACxE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,mCAAmC,EAAE;IACnE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gCAAgC,EAAE;IACjE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE;IACnD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACxE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAC7D,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,6DAA6D,EAAE;IAC9F,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,QAAQ,EAAE,EAAE;IACjD,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oDAAoD,EAAE;CACvF,CAAC","sourcesContent":["import { APP_NAME } from \"../config.js\";\nimport type { SourceInfo } from \"./source-info.js\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message\" },\n\t{ name: \"clone\", description: \"Duplicate the current session at the current position\" },\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n\t{ name: \"subagent\", description: \"Spawn a subagent directly: /subagent <mode> <task>\" },\n];\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
+
import { type AgentType } from "./dispatch-evaluator.js";
|
|
3
4
|
import { type SubagentMode } from "./subagent.js";
|
|
4
5
|
export interface SubagentPoolTask {
|
|
5
6
|
task_id: string;
|
|
@@ -33,6 +34,18 @@ export interface SubagentResult {
|
|
|
33
34
|
/** Parsed result.json content when available (e.g. on partial completion). */
|
|
34
35
|
result_data?: Record<string, unknown>;
|
|
35
36
|
}
|
|
37
|
+
export interface TaskResult {
|
|
38
|
+
/** True when the evaluator decided the task is simple enough for inline handling. */
|
|
39
|
+
handled_inline: boolean;
|
|
40
|
+
/** Present when the task was delegated. */
|
|
41
|
+
task_id?: string;
|
|
42
|
+
agent_type?: string;
|
|
43
|
+
reason?: string;
|
|
44
|
+
/** Subagent result when delegated. */
|
|
45
|
+
result?: SubagentResult;
|
|
46
|
+
/** Duration in milliseconds when delegated. */
|
|
47
|
+
duration?: number;
|
|
48
|
+
}
|
|
36
49
|
export interface SubagentPoolOptions {
|
|
37
50
|
/** Path to the hoocode executable. */
|
|
38
51
|
executable: string;
|
|
@@ -87,6 +100,28 @@ export declare class SubagentPool extends EventEmitter {
|
|
|
87
100
|
running_count(): number;
|
|
88
101
|
/** Number of tasks waiting in the queue. */
|
|
89
102
|
queued_count(): number;
|
|
103
|
+
/**
|
|
104
|
+
* Dispatch a task through the evaluator.
|
|
105
|
+
*
|
|
106
|
+
* - If `forceAgent` is provided, skip evaluation and spawn directly.
|
|
107
|
+
* - Otherwise evaluate the task. If it should be handled inline, return
|
|
108
|
+
* `{ handled_inline: true }` immediately.
|
|
109
|
+
* - If delegating, spawn the subagent, wait for completion, write
|
|
110
|
+
* `output.json`, and return the result.
|
|
111
|
+
*/
|
|
112
|
+
dispatch(task: string, forceAgent?: AgentType): Promise<TaskResult>;
|
|
113
|
+
/**
|
|
114
|
+
* Dispatch a batch of subtasks concurrently.
|
|
115
|
+
*
|
|
116
|
+
* Spawns up to `maxConcurrency` at once; overflow is queued with FIFO.
|
|
117
|
+
* Returns aggregated results in the same order as the input.
|
|
118
|
+
*/
|
|
119
|
+
dispatchBatch(tasks: Array<{
|
|
120
|
+
agent_type: AgentType;
|
|
121
|
+
prompt: string;
|
|
122
|
+
}>): Promise<TaskResult[]>;
|
|
123
|
+
private writeDispatchLog;
|
|
124
|
+
private writeOutputJson;
|
|
90
125
|
/** Kill all running processes, clear the queue, and reject pending waiters. */
|
|
91
126
|
dispose(): void;
|
|
92
127
|
/** Pull tasks from the queue while slots are available. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent-pool.d.ts","sourceRoot":"","sources":["../../src/core/subagent-pool.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { EventEmitter } from \"node:events\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { CONFIG_DIR_NAME } from \"../config.js\";\nimport { waitForChildProcess } from \"../utils/child-process.js\";\nimport { SubagentLifeguard } from \"./lifeguard.js\";\nimport { OutputVerifier } from \"./output-verifier.js\";\nimport { getSubagentSystemPrompt, type SubagentMode } from \"./subagent.js\";\nimport { TokenBudget } from \"./token-budget.js\";\n\nexport interface SubagentPoolTask {\n\ttask_id: string;\n\tagent_type: SubagentMode | string;\n\ttask: string;\n\tcontext?: string;\n\ttoken_budget?: number;\n\tcwd?: string;\n\tmodel?: string;\n\tprovider?: string;\n}\n\nexport interface SubagentSlot {\n\tpid: number;\n\tagent_type: string;\n\ttask_id: string;\n\tspawned_at: number;\n\ttoken_budget: number;\n\tprocess: ReturnType<typeof spawn>;\n}\n\nexport interface SubagentResult {\n\ttask_id: string;\n\tok: boolean;\n\tstdout: string;\n\tstderr: string;\n\texit_code: number | null;\n\terror?: string;\n\t/** True when the task exceeded its token budget and was hard-stopped. */\n\tbudget_exceeded?: boolean;\n\t/** Terminal status derived from how the task finished. */\n\tstatus?: \"complete\" | \"partial\" | \"failed\" | \"stalled\" | \"timeout\";\n\t/** Parsed result.json content when available (e.g. on partial completion). */\n\tresult_data?: Record<string, unknown>;\n}\n\nexport interface SubagentPoolOptions {\n\t/** Path to the hoocode executable. */\n\texecutable: string;\n\t/** Maximum concurrent child processes. Defaults to 5. */\n\tmaxConcurrency?: number;\n\t/** Working directory for spawned processes. Defaults to process.cwd(). */\n\tcwd?: string;\n\t/** Environment variables. Defaults to process.env. */\n\tenv?: NodeJS.ProcessEnv;\n\t/** Default token budget per task. Defaults to 0. */\n\tdefaultTokenBudget?: number;\n}\n\n/**\n * Pool for running hoocode subagents as child processes with bounded concurrency,\n * FIFO queuing with priority support, and automatic slot refill.\n *\n * Events:\n * - \"task_done\" – task completed successfully and output was verified\n * - \"task_failed\" – task failed (spawn error, bad exit code, verification failure)\n * - \"task_stalled\" – heartbeat missed for 60s, process was SIGKILLed\n * - \"task_timeout\" – hard timeout exceeded, process was SIGKILLed\n * - \"budget_warning\" – token usage crossed 80% threshold\n */\nexport class SubagentPool extends EventEmitter {\n\tprivate readonly maxConcurrency: number;\n\tprivate readonly executable: string;\n\tprivate readonly cwd: string;\n\tprivate readonly env: NodeJS.ProcessEnv;\n\tprivate readonly defaultTokenBudget: number;\n\n\tprivate slots = new Map<string, SubagentSlot>();\n\tprivate queue: SubagentPoolTask[] = [];\n\tprivate completed = new Map<string, SubagentResult>();\n\tprivate waiters = new Map<string, { resolve: (result: SubagentResult) => void; reject: (err: Error) => void }>();\n\tprivate budgets = new Map<string, TokenBudget>();\n\tprivate verifier = new OutputVerifier();\n\tprivate lifeguard: SubagentLifeguard;\n\tprivate disposed = false;\n\t/** Tracks why a task was killed (stalled / timeout) before exit handler fires. */\n\tprivate killReasons = new Map<string, \"stalled\" | \"timeout\">();\n\t/** Persistent terminal status map, survives wait_for consumption. */\n\tprivate taskStatus = new Map<string, \"done\" | \"failed\" | \"stalled\" | \"timeout\">();\n\n\tconstructor(options: SubagentPoolOptions) {\n\t\tsuper();\n\t\tthis.maxConcurrency = options.maxConcurrency ?? 5;\n\t\tthis.executable = options.executable;\n\t\tthis.cwd = options.cwd ?? process.cwd();\n\t\tthis.env = options.env ?? process.env;\n\t\tthis.defaultTokenBudget = options.defaultTokenBudget ?? 0;\n\t\tthis.verifier = new OutputVerifier(this.cwd);\n\t\tthis.lifeguard = new SubagentLifeguard(this.cwd);\n\t\tthis.lifeguard.on(\"stalled\", (data: { task_id: string; pid: number }) => {\n\t\t\tthis.killReasons.set(data.task_id, \"stalled\");\n\t\t\tthis.emit(\"task_stalled\", data);\n\t\t});\n\t\tthis.lifeguard.on(\"timeout\", (data: { task_id: string; pid: number }) => {\n\t\t\tthis.killReasons.set(data.task_id, \"timeout\");\n\t\t\tthis.emit(\"task_timeout\", data);\n\t\t});\n\t}\n\n\t/** Priority value: higher numbers run first. */\n\tprivate priorityOf(agent_type: string): number {\n\t\tswitch (agent_type) {\n\t\t\tcase \"explore\":\n\t\t\t\treturn 2;\n\t\t\tcase \"doc\":\n\t\t\t\treturn 0;\n\t\t\tdefault:\n\t\t\t\treturn 1;\n\t\t}\n\t}\n\n\t/** Queue a task. It will run when a slot is free. */\n\tspawn(task: SubagentPoolTask): void {\n\t\tif (this.disposed) {\n\t\t\tthrow new Error(\"SubagentPool has been disposed\");\n\t\t}\n\t\tif (\n\t\t\tthis.slots.has(task.task_id) ||\n\t\t\tthis.queue.some((t) => t.task_id === task.task_id) ||\n\t\t\tthis.completed.has(task.task_id)\n\t\t) {\n\t\t\tthrow new Error(`Duplicate task_id: ${task.task_id}`);\n\t\t}\n\n\t\tconst p = this.priorityOf(task.agent_type);\n\t\tconst idx = this.queue.findIndex((t) => this.priorityOf(t.agent_type) < p);\n\t\tif (idx === -1) {\n\t\t\tthis.queue.push(task);\n\t\t} else {\n\t\t\tthis.queue.splice(idx, 0, task);\n\t\t}\n\t\tthis.pull();\n\t}\n\n\t/** Current status of a task. */\n\tget_status(task_id: string): \"running\" | \"queued\" | \"done\" | \"failed\" | \"stalled\" | \"timeout\" {\n\t\tif (this.slots.has(task_id)) return \"running\";\n\t\tif (this.queue.some((t) => t.task_id === task_id)) return \"queued\";\n\t\tconst persisted = this.taskStatus.get(task_id);\n\t\tif (persisted) return persisted;\n\t\tconst result = this.completed.get(task_id);\n\t\tif (result) {\n\t\t\tif (result.status === \"stalled\") return \"stalled\";\n\t\t\tif (result.status === \"timeout\") return \"timeout\";\n\t\t\tif (result.ok) return \"done\";\n\t\t\treturn \"failed\";\n\t\t}\n\t\treturn \"failed\";\n\t}\n\n\t/** Wait for a task to complete and return its result. */\n\twait_for(task_id: string): Promise<SubagentResult> {\n\t\tif (this.disposed) {\n\t\t\treturn Promise.reject(new Error(\"SubagentPool has been disposed\"));\n\t\t}\n\n\t\tconst existing = this.completed.get(task_id);\n\t\tif (existing) {\n\t\t\tthis.completed.delete(task_id);\n\t\t\treturn Promise.resolve(existing);\n\t\t}\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.waiters.set(task_id, { resolve, reject });\n\t\t});\n\t}\n\n\t/** Number of currently running subagents. */\n\trunning_count(): number {\n\t\treturn this.slots.size;\n\t}\n\n\t/** Number of tasks waiting in the queue. */\n\tqueued_count(): number {\n\t\treturn this.queue.length;\n\t}\n\n\t/** Kill all running processes, clear the queue, and reject pending waiters. */\n\tdispose(): void {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\n\t\tfor (const slot of this.slots.values()) {\n\t\t\tif (!slot.process.killed) {\n\t\t\t\tslot.process.kill(\"SIGTERM\");\n\t\t\t}\n\t\t}\n\t\tthis.slots.clear();\n\t\tthis.queue = [];\n\n\t\tfor (const [task_id, waiter] of this.waiters) {\n\t\t\twaiter.reject(new Error(\"SubagentPool disposed\"));\n\t\t\tthis.waiters.delete(task_id);\n\t\t}\n\t\tthis.completed.clear();\n\t\tthis.budgets.clear();\n\t\tthis.killReasons.clear();\n\t\tthis.taskStatus.clear();\n\t\tthis.lifeguard.dispose();\n\t\tthis.removeAllListeners();\n\t}\n\n\t/** Pull tasks from the queue while slots are available. */\n\tprivate pull(): void {\n\t\twhile (this.slots.size < this.maxConcurrency && this.queue.length > 0) {\n\t\t\tconst task = this.queue.shift()!;\n\t\t\tthis.startTask(task, false);\n\t\t}\n\t}\n\n\t/** Build CLI arguments for a task. */\n\tprivate buildArgs(task: SubagentPoolTask): string[] {\n\t\tconst args: string[] = [\"--mode\", \"json\", \"--no-session\"];\n\n\t\tif (task.agent_type) {\n\t\t\ttry {\n\t\t\t\tconst systemPrompt = getSubagentSystemPrompt(task.agent_type as SubagentMode);\n\t\t\t\targs.push(\"--system-prompt\", systemPrompt);\n\t\t\t} catch {\n\t\t\t\t// Unknown mode, skip custom system prompt\n\t\t\t}\n\t\t}\n\n\t\tif (task.model) {\n\t\t\targs.push(\"--model\", task.model);\n\t\t}\n\t\tif (task.provider) {\n\t\t\targs.push(\"--provider\", task.provider);\n\t\t}\n\n\t\tconst prompt = task.context?.trim()\n\t\t\t? `Context from the calling agent:\\n\\n${task.context.trim()}\\n\\nTask: ${task.task.trim()}`\n\t\t\t: `Task: ${task.task.trim()}`;\n\t\targs.push(prompt);\n\n\t\treturn args;\n\t}\n\n\t/** Start a task in a child process, with one retry on failure. */\n\tprivate startTask(task: SubagentPoolTask, isRetry: boolean): void {\n\t\t// Get or create a TokenBudget tracker. On retry, reuse the existing one\n\t\t// so cumulative usage persists across retries.\n\t\tlet budget = this.budgets.get(task.task_id);\n\t\tif (!budget) {\n\t\t\tbudget = new TokenBudget(task.task_id, task.agent_type, {\n\t\t\t\tlimit: task.token_budget,\n\t\t\t\tcwd: task.cwd ?? this.cwd,\n\t\t\t});\n\t\t\tbudget.on(\"budget_warning\", (data: { task_id: string; message: string; used: number; limit: number }) => {\n\t\t\t\tthis.emit(\"budget_warning\", data);\n\t\t\t});\n\t\t\tbudget.on(\"budget_exceeded\", () => {\n\t\t\t\tconst slot = this.slots.get(task.task_id);\n\t\t\t\tif (slot && !slot.process.killed) {\n\t\t\t\t\tslot.process.kill(\"SIGTERM\");\n\t\t\t\t}\n\t\t\t});\n\t\t\tthis.budgets.set(task.task_id, budget);\n\t\t}\n\n\t\tlet proc: ReturnType<typeof spawn>;\n\t\ttry {\n\t\t\tproc = spawn(this.executable, this.buildArgs(task), {\n\t\t\t\tcwd: task.cwd ?? this.cwd,\n\t\t\t\tenv: this.env,\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\t\t} catch {\n\t\t\tif (!isRetry) {\n\t\t\t\tthis.startTask(task, true);\n\t\t\t} else {\n\t\t\t\tthis.emit(\"task_failed\", {\n\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\terror: \"Spawn failed synchronously\",\n\t\t\t\t});\n\t\t\t\tthis.resolveWaiter(task.task_id, {\n\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\tok: false,\n\t\t\t\t\tstdout: \"\",\n\t\t\t\t\tstderr: \"\",\n\t\t\t\t\texit_code: null,\n\t\t\t\t\terror: \"Spawn failed synchronously\",\n\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t});\n\t\t\t\tthis.pull();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst slot: SubagentSlot = {\n\t\t\tpid: proc.pid ?? 0,\n\t\t\tagent_type: task.agent_type,\n\t\t\ttask_id: task.task_id,\n\t\t\tspawned_at: Date.now(),\n\t\t\ttoken_budget: task.token_budget ?? this.defaultTokenBudget,\n\t\t\tprocess: proc,\n\t\t};\n\n\t\tthis.slots.set(task.task_id, slot);\n\t\tthis.lifeguard.monitor(task.task_id, task.agent_type, proc);\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet budgetExceeded = false;\n\n\t\tproc.stdout?.on(\"data\", (data: Buffer) => {\n\t\t\tconst chunk = data.toString();\n\t\t\tstdout += chunk;\n\t\t\tbudget.processStdout(chunk);\n\n\t\t\t// Heartbeat detection: look for {\"ping\":true} JSON lines\n\t\t\tfor (const raw of chunk.split(\"\\n\")) {\n\t\t\t\tconst line = raw.trim();\n\t\t\t\tif (!line.startsWith(\"{\")) continue;\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed = JSON.parse(line) as Record<string, unknown>;\n\t\t\t\t\tif (parsed.ping === true) {\n\t\t\t\t\t\tthis.lifeguard.recordHeartbeat(task.task_id);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Not a ping line, ignore\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tproc.stderr?.on(\"data\", (data: Buffer) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tbudget.once(\"budget_exceeded\", () => {\n\t\t\tbudgetExceeded = true;\n\t\t});\n\n\t\twaitForChildProcess(proc)\n\t\t\t.then((code) => {\n\t\t\t\tthis.slots.delete(task.task_id);\n\t\t\t\tbudget.flush();\n\n\t\t\t\tconst killReason = this.killReasons.get(task.task_id);\n\t\t\t\tthis.killReasons.delete(task.task_id);\n\n\t\t\t\t// If killed by lifeguard, override exit handling\n\t\t\t\tif (killReason === \"stalled\" || killReason === \"timeout\") {\n\t\t\t\t\tconst result: SubagentResult = {\n\t\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\t\tok: false,\n\t\t\t\t\t\tstdout,\n\t\t\t\t\t\tstderr,\n\t\t\t\t\t\texit_code: code,\n\t\t\t\t\t\tstatus: killReason,\n\t\t\t\t\t};\n\t\t\t\t\tthis.emit(`task_${killReason}`, { task_id: task.task_id });\n\t\t\t\t\tthis.resolveWaiter(task.task_id, result);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst result: SubagentResult = {\n\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\tok: code === 0 && !budgetExceeded,\n\t\t\t\t\tstdout,\n\t\t\t\t\tstderr,\n\t\t\t\t\texit_code: code,\n\t\t\t\t\tbudget_exceeded: budgetExceeded,\n\t\t\t\t\tstatus: code === 0 && !budgetExceeded ? \"complete\" : \"failed\",\n\t\t\t\t};\n\n\t\t\t\tif (budgetExceeded) {\n\t\t\t\t\t// Force-return whatever exists in result.json, mark partial\n\t\t\t\t\tconst resultData = this.tryReadResultJson(task.task_id, task.cwd ?? this.cwd);\n\t\t\t\t\tresult.status = resultData ? \"partial\" : \"failed\";\n\t\t\t\t\tresult.result_data = resultData;\n\t\t\t\t\tif (resultData) {\n\t\t\t\t\t\tresult.ok = true; // partial is considered success with data\n\t\t\t\t\t}\n\t\t\t\t\tthis.emit(\"task_done\", { task_id: task.task_id, status: \"partial\" });\n\t\t\t\t\tthis.resolveWaiter(task.task_id, result);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (result.ok) {\n\t\t\t\t\tconst verification = this.verifier.verify(task.task_id, task.cwd ?? this.cwd);\n\t\t\t\t\tif (!verification.valid) {\n\t\t\t\t\t\tresult.ok = false;\n\t\t\t\t\t\tresult.error = verification.reason;\n\t\t\t\t\t\tresult.status = \"failed\";\n\t\t\t\t\t\tthis.emit(\"task_failed\", {\n\t\t\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\t\t\terror: verification.reason,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tthis.resolveWaiter(task.task_id, result);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (result.ok) {\n\t\t\t\t\tthis.emit(\"task_done\", { task_id: task.task_id, status: \"complete\" });\n\t\t\t\t} else {\n\t\t\t\t\tthis.emit(\"task_failed\", {\n\t\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\t\terror: result.error ?? `Exited with code ${code}`,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tthis.resolveWaiter(task.task_id, result);\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tthis.slots.delete(task.task_id);\n\t\t\t\tbudget.flush();\n\t\t\t\tif (!isRetry) {\n\t\t\t\t\tthis.startTask(task, true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst error = err instanceof Error ? err.message : String(err);\n\t\t\t\tthis.emit(\"task_failed\", { task_id: task.task_id, error });\n\t\t\t\tthis.resolveWaiter(task.task_id, {\n\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\tok: false,\n\t\t\t\t\tstdout,\n\t\t\t\t\tstderr,\n\t\t\t\t\texit_code: null,\n\t\t\t\t\terror,\n\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t});\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tthis.budgets.delete(task.task_id);\n\t\t\t\tthis.pull();\n\t\t\t});\n\t}\n\n\tprivate tryReadResultJson(task_id: string, cwd: string): Record<string, unknown> | undefined {\n\t\tconst path = join(cwd, CONFIG_DIR_NAME, \"agents\", task_id, \"result.json\");\n\t\tif (!existsSync(path)) return undefined;\n\t\ttry {\n\t\t\tconst raw = readFileSync(path, \"utf-8\");\n\t\t\treturn JSON.parse(raw) as Record<string, unknown>;\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate resolveWaiter(task_id: string, result: SubagentResult): void {\n\t\t// Persist terminal status for get_status() even after wait_for consumes the result\n\t\tif (result.status === \"stalled\") this.taskStatus.set(task_id, \"stalled\");\n\t\telse if (result.status === \"timeout\") this.taskStatus.set(task_id, \"timeout\");\n\t\telse if (result.ok) this.taskStatus.set(task_id, \"done\");\n\t\telse this.taskStatus.set(task_id, \"failed\");\n\n\t\tconst waiter = this.waiters.get(task_id);\n\t\tif (waiter) {\n\t\t\twaiter.resolve(result);\n\t\t\tthis.waiters.delete(task_id);\n\t\t\treturn;\n\t\t}\n\t\tthis.completed.set(task_id, result);\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"subagent-pool.d.ts","sourceRoot":"","sources":["../../src/core/subagent-pool.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { EventEmitter } from \"node:events\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { CONFIG_DIR_NAME } from \"../config.js\";\nimport { waitForChildProcess } from \"../utils/child-process.js\";\nimport { type AgentType, DispatchEvaluator } from \"./dispatch-evaluator.js\";\nimport { SubagentLifeguard } from \"./lifeguard.js\";\nimport { OutputVerifier } from \"./output-verifier.js\";\nimport { getSubagentSystemPrompt, type SubagentMode } from \"./subagent.js\";\nimport { TokenBudget } from \"./token-budget.js\";\n\nexport interface SubagentPoolTask {\n\ttask_id: string;\n\tagent_type: SubagentMode | string;\n\ttask: string;\n\tcontext?: string;\n\ttoken_budget?: number;\n\tcwd?: string;\n\tmodel?: string;\n\tprovider?: string;\n}\n\nexport interface SubagentSlot {\n\tpid: number;\n\tagent_type: string;\n\ttask_id: string;\n\tspawned_at: number;\n\ttoken_budget: number;\n\tprocess: ReturnType<typeof spawn>;\n}\n\nexport interface SubagentResult {\n\ttask_id: string;\n\tok: boolean;\n\tstdout: string;\n\tstderr: string;\n\texit_code: number | null;\n\terror?: string;\n\t/** True when the task exceeded its token budget and was hard-stopped. */\n\tbudget_exceeded?: boolean;\n\t/** Terminal status derived from how the task finished. */\n\tstatus?: \"complete\" | \"partial\" | \"failed\" | \"stalled\" | \"timeout\";\n\t/** Parsed result.json content when available (e.g. on partial completion). */\n\tresult_data?: Record<string, unknown>;\n}\n\nexport interface TaskResult {\n\t/** True when the evaluator decided the task is simple enough for inline handling. */\n\thandled_inline: boolean;\n\t/** Present when the task was delegated. */\n\ttask_id?: string;\n\tagent_type?: string;\n\treason?: string;\n\t/** Subagent result when delegated. */\n\tresult?: SubagentResult;\n\t/** Duration in milliseconds when delegated. */\n\tduration?: number;\n}\n\nexport interface SubagentPoolOptions {\n\t/** Path to the hoocode executable. */\n\texecutable: string;\n\t/** Maximum concurrent child processes. Defaults to 5. */\n\tmaxConcurrency?: number;\n\t/** Working directory for spawned processes. Defaults to process.cwd(). */\n\tcwd?: string;\n\t/** Environment variables. Defaults to process.env. */\n\tenv?: NodeJS.ProcessEnv;\n\t/** Default token budget per task. Defaults to 0. */\n\tdefaultTokenBudget?: number;\n}\n\n/**\n * Pool for running hoocode subagents as child processes with bounded concurrency,\n * FIFO queuing with priority support, and automatic slot refill.\n *\n * Events:\n * - \"task_done\" – task completed successfully and output was verified\n * - \"task_failed\" – task failed (spawn error, bad exit code, verification failure)\n * - \"task_stalled\" – heartbeat missed for 60s, process was SIGKILLed\n * - \"task_timeout\" – hard timeout exceeded, process was SIGKILLed\n * - \"budget_warning\" – token usage crossed 80% threshold\n */\nexport class SubagentPool extends EventEmitter {\n\tprivate readonly maxConcurrency: number;\n\tprivate readonly executable: string;\n\tprivate readonly cwd: string;\n\tprivate readonly env: NodeJS.ProcessEnv;\n\tprivate readonly defaultTokenBudget: number;\n\n\tprivate slots = new Map<string, SubagentSlot>();\n\tprivate queue: SubagentPoolTask[] = [];\n\tprivate completed = new Map<string, SubagentResult>();\n\tprivate waiters = new Map<string, { resolve: (result: SubagentResult) => void; reject: (err: Error) => void }>();\n\tprivate budgets = new Map<string, TokenBudget>();\n\tprivate verifier = new OutputVerifier();\n\tprivate lifeguard: SubagentLifeguard;\n\tprivate disposed = false;\n\t/** Tracks why a task was killed (stalled / timeout) before exit handler fires. */\n\tprivate killReasons = new Map<string, \"stalled\" | \"timeout\">();\n\t/** Persistent terminal status map, survives wait_for consumption. */\n\tprivate taskStatus = new Map<string, \"done\" | \"failed\" | \"stalled\" | \"timeout\">();\n\n\tconstructor(options: SubagentPoolOptions) {\n\t\tsuper();\n\t\tthis.maxConcurrency = options.maxConcurrency ?? 5;\n\t\tthis.executable = options.executable;\n\t\tthis.cwd = options.cwd ?? process.cwd();\n\t\tthis.env = options.env ?? process.env;\n\t\tthis.defaultTokenBudget = options.defaultTokenBudget ?? 0;\n\t\tthis.verifier = new OutputVerifier(this.cwd);\n\t\tthis.lifeguard = new SubagentLifeguard(this.cwd);\n\t\tthis.lifeguard.on(\"stalled\", (data: { task_id: string; pid: number }) => {\n\t\t\tthis.killReasons.set(data.task_id, \"stalled\");\n\t\t\tthis.emit(\"task_stalled\", data);\n\t\t});\n\t\tthis.lifeguard.on(\"timeout\", (data: { task_id: string; pid: number }) => {\n\t\t\tthis.killReasons.set(data.task_id, \"timeout\");\n\t\t\tthis.emit(\"task_timeout\", data);\n\t\t});\n\t}\n\n\t/** Priority value: higher numbers run first. */\n\tprivate priorityOf(agent_type: string): number {\n\t\tswitch (agent_type) {\n\t\t\tcase \"explore\":\n\t\t\tcase \"review\":\n\t\t\t\treturn 2;\n\t\t\tcase \"doc\":\n\t\t\t\treturn 0;\n\t\t\tdefault:\n\t\t\t\treturn 1;\n\t\t}\n\t}\n\n\t/** Queue a task. It will run when a slot is free. */\n\tspawn(task: SubagentPoolTask): void {\n\t\tif (this.disposed) {\n\t\t\tthrow new Error(\"SubagentPool has been disposed\");\n\t\t}\n\t\tif (\n\t\t\tthis.slots.has(task.task_id) ||\n\t\t\tthis.queue.some((t) => t.task_id === task.task_id) ||\n\t\t\tthis.completed.has(task.task_id)\n\t\t) {\n\t\t\tthrow new Error(`Duplicate task_id: ${task.task_id}`);\n\t\t}\n\n\t\tconst p = this.priorityOf(task.agent_type);\n\t\tconst idx = this.queue.findIndex((t) => this.priorityOf(t.agent_type) < p);\n\t\tif (idx === -1) {\n\t\t\tthis.queue.push(task);\n\t\t} else {\n\t\t\tthis.queue.splice(idx, 0, task);\n\t\t}\n\t\tthis.pull();\n\t}\n\n\t/** Current status of a task. */\n\tget_status(task_id: string): \"running\" | \"queued\" | \"done\" | \"failed\" | \"stalled\" | \"timeout\" {\n\t\tif (this.slots.has(task_id)) return \"running\";\n\t\tif (this.queue.some((t) => t.task_id === task_id)) return \"queued\";\n\t\tconst persisted = this.taskStatus.get(task_id);\n\t\tif (persisted) return persisted;\n\t\tconst result = this.completed.get(task_id);\n\t\tif (result) {\n\t\t\tif (result.status === \"stalled\") return \"stalled\";\n\t\t\tif (result.status === \"timeout\") return \"timeout\";\n\t\t\tif (result.ok) return \"done\";\n\t\t\treturn \"failed\";\n\t\t}\n\t\treturn \"failed\";\n\t}\n\n\t/** Wait for a task to complete and return its result. */\n\twait_for(task_id: string): Promise<SubagentResult> {\n\t\tif (this.disposed) {\n\t\t\treturn Promise.reject(new Error(\"SubagentPool has been disposed\"));\n\t\t}\n\n\t\tconst existing = this.completed.get(task_id);\n\t\tif (existing) {\n\t\t\tthis.completed.delete(task_id);\n\t\t\treturn Promise.resolve(existing);\n\t\t}\n\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.waiters.set(task_id, { resolve, reject });\n\t\t});\n\t}\n\n\t/** Number of currently running subagents. */\n\trunning_count(): number {\n\t\treturn this.slots.size;\n\t}\n\n\t/** Number of tasks waiting in the queue. */\n\tqueued_count(): number {\n\t\treturn this.queue.length;\n\t}\n\n\t/**\n\t * Dispatch a task through the evaluator.\n\t *\n\t * - If `forceAgent` is provided, skip evaluation and spawn directly.\n\t * - Otherwise evaluate the task. If it should be handled inline, return\n\t * `{ handled_inline: true }` immediately.\n\t * - If delegating, spawn the subagent, wait for completion, write\n\t * `output.json`, and return the result.\n\t */\n\tasync dispatch(task: string, forceAgent?: AgentType): Promise<TaskResult> {\n\t\tif (this.disposed) {\n\t\t\treturn Promise.reject(new Error(\"SubagentPool has been disposed\"));\n\t\t}\n\n\t\tconst evaluator = new DispatchEvaluator();\n\t\tconst analysis = evaluator.evaluate(task);\n\n\t\tif (!forceAgent && !analysis.should_delegate) {\n\t\t\treturn { handled_inline: true, reason: analysis.reason };\n\t\t}\n\n\t\tconst agent_type: AgentType = forceAgent ?? (analysis.agent_type as AgentType) ?? \"explore\";\n\t\tconst task_id = `dispatch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n\t\tconst reason = forceAgent ? \"user_override\" : analysis.reason;\n\t\tconst complexity = analysis.estimated_complexity;\n\n\t\t// Pre-dispatch logging\n\t\tconst logLine = `[DISPATCH] agent=${agent_type} reason=${reason} complexity=${complexity} task_id=${task_id}`;\n\t\tconsole.log(logLine);\n\t\tthis.writeDispatchLog(task_id, agent_type, reason, complexity, task);\n\n\t\tconst poolTask: SubagentPoolTask = {\n\t\t\ttask_id,\n\t\t\tagent_type,\n\t\t\ttask,\n\t\t\tcwd: this.cwd,\n\t\t};\n\n\t\tconst startTime = Date.now();\n\t\tthis.spawn(poolTask);\n\t\tconst result = await this.wait_for(task_id);\n\t\tconst duration = Date.now() - startTime;\n\n\t\treturn {\n\t\t\thandled_inline: false,\n\t\t\ttask_id,\n\t\t\tagent_type,\n\t\t\treason,\n\t\t\tresult,\n\t\t\tduration,\n\t\t};\n\t}\n\n\t/**\n\t * Dispatch a batch of subtasks concurrently.\n\t *\n\t * Spawns up to `maxConcurrency` at once; overflow is queued with FIFO.\n\t * Returns aggregated results in the same order as the input.\n\t */\n\tasync dispatchBatch(tasks: Array<{ agent_type: AgentType; prompt: string }>): Promise<TaskResult[]> {\n\t\tif (this.disposed) {\n\t\t\treturn Promise.reject(new Error(\"SubagentPool has been disposed\"));\n\t\t}\n\n\t\tconst promises = tasks.map(async ({ agent_type, prompt }) => {\n\t\t\treturn this.dispatch(prompt, agent_type);\n\t\t});\n\n\t\treturn Promise.all(promises);\n\t}\n\n\tprivate writeDispatchLog(\n\t\ttask_id: string,\n\t\tagent_type: string,\n\t\treason: string,\n\t\tcomplexity: string,\n\t\ttask: string,\n\t): void {\n\t\tconst log = {\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\ttask_id,\n\t\t\tagent_type,\n\t\t\treason,\n\t\t\tcomplexity,\n\t\t\ttask,\n\t\t};\n\t\tconst path = join(this.cwd, CONFIG_DIR_NAME, \"agents\", task_id, \"dispatch-log.json\");\n\t\ttry {\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, JSON.stringify(log, null, 2));\n\t\t} catch {\n\t\t\t// Best-effort persistence\n\t\t}\n\t}\n\n\tprivate writeOutputJson(task_id: string, result: SubagentResult): void {\n\t\tconst output = {\n\t\t\ttask_id: result.task_id,\n\t\t\tok: result.ok,\n\t\t\texit_code: result.exit_code,\n\t\t\tstatus: result.status,\n\t\t\tstdout: result.stdout,\n\t\t\tstderr: result.stderr,\n\t\t\terror: result.error,\n\t\t\tbudget_exceeded: result.budget_exceeded,\n\t\t\tresult_data: result.result_data,\n\t\t};\n\t\tconst path = join(this.cwd, CONFIG_DIR_NAME, \"agents\", task_id, \"output.json\");\n\t\ttry {\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, JSON.stringify(output, null, 2));\n\t\t} catch {\n\t\t\t// Best-effort persistence\n\t\t}\n\t}\n\n\t/** Kill all running processes, clear the queue, and reject pending waiters. */\n\tdispose(): void {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\n\t\tfor (const slot of this.slots.values()) {\n\t\t\tif (!slot.process.killed) {\n\t\t\t\tslot.process.kill(\"SIGTERM\");\n\t\t\t}\n\t\t}\n\t\tthis.slots.clear();\n\t\tthis.queue = [];\n\n\t\tfor (const [task_id, waiter] of this.waiters) {\n\t\t\twaiter.reject(new Error(\"SubagentPool disposed\"));\n\t\t\tthis.waiters.delete(task_id);\n\t\t}\n\t\tthis.completed.clear();\n\t\tthis.budgets.clear();\n\t\tthis.killReasons.clear();\n\t\tthis.taskStatus.clear();\n\t\tthis.lifeguard.dispose();\n\t\tthis.removeAllListeners();\n\t}\n\n\t/** Pull tasks from the queue while slots are available. */\n\tprivate pull(): void {\n\t\twhile (this.slots.size < this.maxConcurrency && this.queue.length > 0) {\n\t\t\tconst task = this.queue.shift()!;\n\t\t\tthis.startTask(task, false);\n\t\t}\n\t}\n\n\t/** Build CLI arguments for a task. */\n\tprivate buildArgs(task: SubagentPoolTask): string[] {\n\t\tconst args: string[] = [\"--mode\", \"json\", \"--no-session\"];\n\n\t\tif (task.agent_type) {\n\t\t\ttry {\n\t\t\t\tconst systemPrompt = getSubagentSystemPrompt(task.agent_type as SubagentMode);\n\t\t\t\targs.push(\"--system-prompt\", systemPrompt);\n\t\t\t} catch {\n\t\t\t\t// Unknown mode, skip custom system prompt\n\t\t\t}\n\t\t}\n\n\t\tif (task.model) {\n\t\t\targs.push(\"--model\", task.model);\n\t\t}\n\t\tif (task.provider) {\n\t\t\targs.push(\"--provider\", task.provider);\n\t\t}\n\n\t\tconst prompt = task.context?.trim()\n\t\t\t? `Context from the calling agent:\\n\\n${task.context.trim()}\\n\\nTask: ${task.task.trim()}`\n\t\t\t: `Task: ${task.task.trim()}`;\n\t\targs.push(prompt);\n\n\t\treturn args;\n\t}\n\n\t/** Start a task in a child process, with one retry on failure. */\n\tprivate startTask(task: SubagentPoolTask, isRetry: boolean): void {\n\t\t// Get or create a TokenBudget tracker. On retry, reuse the existing one\n\t\t// so cumulative usage persists across retries.\n\t\tlet budget = this.budgets.get(task.task_id);\n\t\tif (!budget) {\n\t\t\tbudget = new TokenBudget(task.task_id, task.agent_type, {\n\t\t\t\tlimit: task.token_budget,\n\t\t\t\tcwd: task.cwd ?? this.cwd,\n\t\t\t});\n\t\t\tbudget.on(\"budget_warning\", (data: { task_id: string; message: string; used: number; limit: number }) => {\n\t\t\t\tthis.emit(\"budget_warning\", data);\n\t\t\t});\n\t\t\tbudget.on(\"budget_exceeded\", () => {\n\t\t\t\tconst slot = this.slots.get(task.task_id);\n\t\t\t\tif (slot && !slot.process.killed) {\n\t\t\t\t\tslot.process.kill(\"SIGTERM\");\n\t\t\t\t}\n\t\t\t});\n\t\t\tthis.budgets.set(task.task_id, budget);\n\t\t}\n\n\t\tlet proc: ReturnType<typeof spawn>;\n\t\ttry {\n\t\t\tproc = spawn(this.executable, this.buildArgs(task), {\n\t\t\t\tcwd: task.cwd ?? this.cwd,\n\t\t\t\tenv: this.env,\n\t\t\t\tshell: false,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\t\t} catch {\n\t\t\tif (!isRetry) {\n\t\t\t\tthis.startTask(task, true);\n\t\t\t} else {\n\t\t\t\tthis.emit(\"task_failed\", {\n\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\terror: \"Spawn failed synchronously\",\n\t\t\t\t});\n\t\t\t\tthis.resolveWaiter(task.task_id, {\n\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\tok: false,\n\t\t\t\t\tstdout: \"\",\n\t\t\t\t\tstderr: \"\",\n\t\t\t\t\texit_code: null,\n\t\t\t\t\terror: \"Spawn failed synchronously\",\n\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t});\n\t\t\t\tthis.pull();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst slot: SubagentSlot = {\n\t\t\tpid: proc.pid ?? 0,\n\t\t\tagent_type: task.agent_type,\n\t\t\ttask_id: task.task_id,\n\t\t\tspawned_at: Date.now(),\n\t\t\ttoken_budget: task.token_budget ?? this.defaultTokenBudget,\n\t\t\tprocess: proc,\n\t\t};\n\n\t\tthis.slots.set(task.task_id, slot);\n\t\tthis.lifeguard.monitor(task.task_id, task.agent_type, proc);\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\t\tlet budgetExceeded = false;\n\n\t\tproc.stdout?.on(\"data\", (data: Buffer) => {\n\t\t\tconst chunk = data.toString();\n\t\t\tstdout += chunk;\n\t\t\tbudget.processStdout(chunk);\n\n\t\t\t// Heartbeat detection: look for {\"ping\":true} JSON lines\n\t\t\tfor (const raw of chunk.split(\"\\n\")) {\n\t\t\t\tconst line = raw.trim();\n\t\t\t\tif (!line.startsWith(\"{\")) continue;\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed = JSON.parse(line) as Record<string, unknown>;\n\t\t\t\t\tif (parsed.ping === true) {\n\t\t\t\t\t\tthis.lifeguard.recordHeartbeat(task.task_id);\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Not a ping line, ignore\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tproc.stderr?.on(\"data\", (data: Buffer) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tbudget.once(\"budget_exceeded\", () => {\n\t\t\tbudgetExceeded = true;\n\t\t});\n\n\t\twaitForChildProcess(proc)\n\t\t\t.then((code) => {\n\t\t\t\tthis.slots.delete(task.task_id);\n\t\t\t\tbudget.flush();\n\n\t\t\t\tconst killReason = this.killReasons.get(task.task_id);\n\t\t\t\tthis.killReasons.delete(task.task_id);\n\n\t\t\t\tconst duration = Date.now() - slot.spawned_at;\n\t\t\t\tconst tokens_used = budget.getUsed();\n\n\t\t\t\t// If killed by lifeguard, override exit handling\n\t\t\t\tif (killReason === \"stalled\" || killReason === \"timeout\") {\n\t\t\t\t\tconst result: SubagentResult = {\n\t\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\t\tok: false,\n\t\t\t\t\t\tstdout,\n\t\t\t\t\t\tstderr,\n\t\t\t\t\t\texit_code: code,\n\t\t\t\t\t\tstatus: killReason,\n\t\t\t\t\t};\n\t\t\t\t\tthis.writeOutputJson(task.task_id, result);\n\t\t\t\t\tthis.emit(`task_${killReason}`, {\n\t\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\t\tagent_type: task.agent_type,\n\t\t\t\t\t\tduration,\n\t\t\t\t\t\ttokens_used,\n\t\t\t\t\t});\n\t\t\t\t\tthis.resolveWaiter(task.task_id, result);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst result: SubagentResult = {\n\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\tok: code === 0 && !budgetExceeded,\n\t\t\t\t\tstdout,\n\t\t\t\t\tstderr,\n\t\t\t\t\texit_code: code,\n\t\t\t\t\tbudget_exceeded: budgetExceeded,\n\t\t\t\t\tstatus: code === 0 && !budgetExceeded ? \"complete\" : \"failed\",\n\t\t\t\t};\n\n\t\t\t\tif (budgetExceeded) {\n\t\t\t\t\t// Force-return whatever exists in result.json, mark partial\n\t\t\t\t\tconst resultData = this.tryReadResultJson(task.task_id, task.cwd ?? this.cwd);\n\t\t\t\t\tresult.status = resultData ? \"partial\" : \"failed\";\n\t\t\t\t\tresult.result_data = resultData;\n\t\t\t\t\tif (resultData) {\n\t\t\t\t\t\tresult.ok = true; // partial is considered success with data\n\t\t\t\t\t}\n\t\t\t\t\tthis.writeOutputJson(task.task_id, result);\n\t\t\t\t\tthis.emit(\"task_done\", {\n\t\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\t\tagent_type: task.agent_type,\n\t\t\t\t\t\tduration,\n\t\t\t\t\t\ttokens_used,\n\t\t\t\t\t\tstatus: \"partial\",\n\t\t\t\t\t});\n\t\t\t\t\tthis.resolveWaiter(task.task_id, result);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (result.ok) {\n\t\t\t\t\tconst verification = this.verifier.verify(task.task_id, task.cwd ?? this.cwd);\n\t\t\t\t\tif (!verification.valid) {\n\t\t\t\t\t\tresult.ok = false;\n\t\t\t\t\t\tresult.error = verification.reason;\n\t\t\t\t\t\tresult.status = \"failed\";\n\t\t\t\t\t\tthis.writeOutputJson(task.task_id, result);\n\t\t\t\t\t\tthis.emit(\"task_failed\", {\n\t\t\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\t\t\tagent_type: task.agent_type,\n\t\t\t\t\t\t\tduration,\n\t\t\t\t\t\t\ttokens_used,\n\t\t\t\t\t\t\terror: verification.reason,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tthis.resolveWaiter(task.task_id, result);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.writeOutputJson(task.task_id, result);\n\n\t\t\t\tif (result.ok) {\n\t\t\t\t\tthis.emit(\"task_done\", {\n\t\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\t\tagent_type: task.agent_type,\n\t\t\t\t\t\tduration,\n\t\t\t\t\t\ttokens_used,\n\t\t\t\t\t\tstatus: \"complete\",\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tthis.emit(\"task_failed\", {\n\t\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\t\tagent_type: task.agent_type,\n\t\t\t\t\t\tduration,\n\t\t\t\t\t\ttokens_used,\n\t\t\t\t\t\terror: result.error ?? `Exited with code ${code}`,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tthis.resolveWaiter(task.task_id, result);\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tthis.slots.delete(task.task_id);\n\t\t\t\tbudget.flush();\n\t\t\t\tconst duration = Date.now() - slot.spawned_at;\n\t\t\t\tconst tokens_used = budget.getUsed();\n\t\t\t\tif (!isRetry) {\n\t\t\t\t\tthis.startTask(task, true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst error = err instanceof Error ? err.message : String(err);\n\t\t\t\tconst result: SubagentResult = {\n\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\tok: false,\n\t\t\t\t\tstdout,\n\t\t\t\t\tstderr,\n\t\t\t\t\texit_code: null,\n\t\t\t\t\terror,\n\t\t\t\t\tstatus: \"failed\",\n\t\t\t\t};\n\t\t\t\tthis.writeOutputJson(task.task_id, result);\n\t\t\t\tthis.emit(\"task_failed\", {\n\t\t\t\t\ttask_id: task.task_id,\n\t\t\t\t\tagent_type: task.agent_type,\n\t\t\t\t\tduration,\n\t\t\t\t\ttokens_used,\n\t\t\t\t\terror,\n\t\t\t\t});\n\t\t\t\tthis.resolveWaiter(task.task_id, result);\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tthis.budgets.delete(task.task_id);\n\t\t\t\tthis.pull();\n\t\t\t});\n\t}\n\n\tprivate tryReadResultJson(task_id: string, cwd: string): Record<string, unknown> | undefined {\n\t\tconst path = join(cwd, CONFIG_DIR_NAME, \"agents\", task_id, \"result.json\");\n\t\tif (!existsSync(path)) return undefined;\n\t\ttry {\n\t\t\tconst raw = readFileSync(path, \"utf-8\");\n\t\t\treturn JSON.parse(raw) as Record<string, unknown>;\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\tprivate resolveWaiter(task_id: string, result: SubagentResult): void {\n\t\t// Persist terminal status for get_status() even after wait_for consumes the result\n\t\tif (result.status === \"stalled\") this.taskStatus.set(task_id, \"stalled\");\n\t\telse if (result.status === \"timeout\") this.taskStatus.set(task_id, \"timeout\");\n\t\telse if (result.ok) this.taskStatus.set(task_id, \"done\");\n\t\telse this.taskStatus.set(task_id, \"failed\");\n\n\t\tconst waiter = this.waiters.get(task_id);\n\t\tif (waiter) {\n\t\t\twaiter.resolve(result);\n\t\t\tthis.waiters.delete(task_id);\n\t\t\treturn;\n\t\t}\n\t\tthis.completed.set(task_id, result);\n\t}\n}\n"]}
|