@kolisachint/hoocode-agent 0.4.16 → 0.4.17
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 +13 -0
- package/dist/core/dispatch-evaluator.d.ts +9 -36
- package/dist/core/dispatch-evaluator.d.ts.map +1 -1
- package/dist/core/dispatch-evaluator.js +11 -302
- package/dist/core/dispatch-evaluator.js.map +1 -1
- package/dist/core/subagent-pool.d.ts +2 -13
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +6 -31
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/token-budget.d.ts +4 -0
- package/dist/core/token-budget.d.ts.map +1 -1
- package/dist/core/token-budget.js +4 -2
- package/dist/core/token-budget.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +0 -3
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +0 -6
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/init-templates.generated.d.ts +0 -1
- package/dist/init-templates.generated.d.ts.map +1 -1
- package/dist/init-templates.generated.js +5 -13
- package/dist/init-templates.generated.js.map +1 -1
- package/docs/routing.md +48 -36
- 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 +1 -0
- package/templates/agents/edit.md +1 -0
- package/templates/agents/explore.md +1 -0
- package/templates/agents/review.md +1 -0
- package/templates/agents/test.md +1 -0
- package/dist/core/subagent.d.ts +0 -14
- package/dist/core/subagent.d.ts.map +0 -1
- package/dist/core/subagent.js +0 -27
- package/dist/core/subagent.js.map +0 -1
- package/templates/subagent/doc.md +0 -16
- package/templates/subagent/edit.md +0 -22
- package/templates/subagent/explore.md +0 -21
- package/templates/subagent/fix.md +0 -22
- package/templates/subagent/review.md +0 -21
- package/templates/subagent/test.md +0 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.17] - 2026-06-01
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Consolidated subagent prompts to a single source of truth. The duplicate `templates/subagent/**` prompt set (and the generated `EMBEDDED_SUBAGENT_PROMPTS` map) is removed; `templates/agents/**` (the frontmatter registry) is now the only prompt source. The built-in agent set is the canonical five: `explore`, `edit`, `test`, `review`, `doc`. The unreachable `fix` mode (never exposed by the Task tool, `/subagent`, or routing) was dropped.
|
|
8
|
+
- Trimmed `DispatchEvaluator` to its only live responsibilities: the nested-delegation depth guard and a complexity estimate for the dispatch log. Delegation is fully description-driven (the parent agent chooses the agent), so the dead keyword-routing/auto-split surface was removed. Updated `docs/routing.md` to match.
|
|
9
|
+
- Moved built-in subagent tool allowlists into agent frontmatter (`tools:` in `templates/agents/*.md`), making the agent registry the single source of truth for each agent's prompt, tools, and model. The hardcoded `SubagentMode` enum and `MODE_TOOLS` map are gone; `SubagentPool` reads the allowlist solely from the resolved definition.
|
|
10
|
+
|
|
11
|
+
### Removed
|
|
12
|
+
|
|
13
|
+
- Removed unused exports: `isSubagentRecommended` (tools/subagent), `SubagentPool.dispatchBatch`, and the `DispatchEvaluator` routing helpers (`classifyWithConfidence`, `shouldSplit`, `canHandleInline`, `getReason`).
|
|
14
|
+
- Removed the `core/subagent.ts` module (`SubagentMode`, `SUBAGENT_MODES`, `MODE_TOOLS`); its role is now served by the frontmatter agent registry.
|
|
15
|
+
|
|
3
16
|
## [0.4.16] - 2026-06-01
|
|
4
17
|
|
|
5
18
|
### Fixed
|
|
@@ -1,48 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Subagent dispatch guard + lightweight task telemetry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The parent agent selects which subagent to delegate to (via the Task tool),
|
|
5
|
+
* so this module performs NO keyword routing. It survives for two narrow
|
|
6
|
+
* responsibilities, both cheap and LLM-free:
|
|
7
|
+
* 1. Depth guard — a subagent (HOOCODE_SUBAGENT_DEPTH>=1) must not spawn
|
|
8
|
+
* further subagents.
|
|
9
|
+
* 2. Complexity estimate — a heuristic recorded in the dispatch log for
|
|
10
|
+
* diagnostics only.
|
|
7
11
|
*/
|
|
8
|
-
export type AgentType = "explore" | "edit" | "test" | "review" | "doc";
|
|
9
12
|
export interface TaskAnalysis {
|
|
13
|
+
/** False only when the depth guard blocks delegation. */
|
|
10
14
|
should_delegate: boolean;
|
|
11
|
-
agent_type: AgentType | null;
|
|
12
15
|
reason: string;
|
|
13
16
|
estimated_complexity: "low" | "medium" | "high";
|
|
14
|
-
parallelizable: boolean;
|
|
15
|
-
context_needed: string[];
|
|
16
|
-
/**
|
|
17
|
-
* Normalized routing confidence in [0, 1]: the share of matched keyword
|
|
18
|
-
* signal that pointed at the chosen agent type. 0 when no keywords matched.
|
|
19
|
-
*/
|
|
20
|
-
confidence: number;
|
|
21
17
|
}
|
|
22
|
-
export interface Subtask {
|
|
23
|
-
agent_type: AgentType;
|
|
24
|
-
prompt: string;
|
|
25
|
-
estimated_files: number;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Classify a task to an agent type with a normalized confidence in [0, 1].
|
|
29
|
-
*
|
|
30
|
-
* confidence = winning score / total matched score across all agent types, i.e.
|
|
31
|
-
* the share of keyword signal that agrees on the winner. 1.0 means every matched
|
|
32
|
-
* keyword pointed at one type; lower values mean the signal was split. Returns a
|
|
33
|
-
* null type with confidence 0 when nothing matched.
|
|
34
|
-
*/
|
|
35
|
-
export declare function classifyWithConfidence(task: string): {
|
|
36
|
-
agent_type: AgentType | null;
|
|
37
|
-
confidence: number;
|
|
38
|
-
};
|
|
39
18
|
export declare class DispatchEvaluator {
|
|
40
19
|
evaluate(task: string): TaskAnalysis;
|
|
41
|
-
shouldSplit(task: string): {
|
|
42
|
-
split: boolean;
|
|
43
|
-
subtasks: Subtask[];
|
|
44
|
-
};
|
|
45
|
-
canHandleInline(task: string): boolean;
|
|
46
|
-
getReason(analysis: TaskAnalysis): string;
|
|
47
20
|
}
|
|
48
21
|
//# sourceMappingURL=dispatch-evaluator.d.ts.map
|
|
@@ -1 +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;IACzB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,OAAO;IACvB,UAAU,EAAE,SAAS,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;CACxB;AA4ID;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,SAAS,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAiBzG;AAwGD,qBAAa,iBAAiB;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CA8CnC;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\t/**\n\t * Normalized routing confidence in [0, 1]: the share of matched keyword\n\t * signal that pointed at the chosen agent type. 0 when no keywords matched.\n\t */\n\tconfidence: number;\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\n/**\n * Explicit, deterministic tie-break order. When two agent types match the same\n * number of keywords, the earliest type in this list wins. This mirrors the\n * previous (accidental) object-iteration order so routing stays stable, but the\n * order is now intentional and documented rather than incidental.\n */\nconst AGENT_PRIORITY: readonly AgentType[] = [\"explore\", \"edit\", \"test\", \"review\", \"doc\"];\n\nfunction scoreAgents(task: string): Record<AgentType, number> {\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\treturn scores;\n}\n\n/**\n * Classify a task to an agent type with a normalized confidence in [0, 1].\n *\n * confidence = winning score / total matched score across all agent types, i.e.\n * the share of keyword signal that agrees on the winner. 1.0 means every matched\n * keyword pointed at one type; lower values mean the signal was split. Returns a\n * null type with confidence 0 when nothing matched.\n */\nexport function classifyWithConfidence(task: string): { agent_type: AgentType | null; confidence: number } {\n\tconst scores = scoreAgents(task);\n\n\tlet total = 0;\n\tfor (const type of AGENT_PRIORITY) total += scores[type];\n\tif (total === 0) return { agent_type: null, confidence: 0 };\n\n\tlet best: AgentType = AGENT_PRIORITY[0];\n\tlet bestScore = -1;\n\tfor (const type of AGENT_PRIORITY) {\n\t\tif (scores[type] > bestScore) {\n\t\t\tbestScore = scores[type];\n\t\t\tbest = type;\n\t\t}\n\t}\n\n\treturn { agent_type: bestScore > 0 ? best : null, confidence: bestScore / total };\n}\n\nfunction detectAgentType(task: string): AgentType | null {\n\treturn classifyWithConfidence(task).agent_type;\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\tconfidence: 0,\n\t\t\t};\n\t\t}\n\n\t\tconst { agent_type: agentType, confidence } = classifyWithConfidence(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\tconfidence,\n\t\t\t};\n\t\t}\n\n\t\t// When delegating, a missing keyword match defaults to explore: a fresh\n\t\t// read-only investigation is the safest start for an ambiguous task, and the\n\t\t// parent can re-delegate with a specific mode afterwards.\n\t\tconst delegateType: AgentType = agentType ?? \"explore\";\n\n\t\treturn {\n\t\t\tshould_delegate: true,\n\t\t\tagent_type: delegateType,\n\t\t\treason: `${delegateType} 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\tconfidence,\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
|
+
{"version":3,"file":"dispatch-evaluator.d.ts","sourceRoot":"","sources":["../../src/core/dispatch-evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,YAAY;IAC5B,yDAAyD;IACzD,eAAe,EAAE,OAAO,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;CAChD;AAoBD,qBAAa,iBAAiB;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAenC;CACD","sourcesContent":["/**\n * Subagent dispatch guard + lightweight task telemetry.\n *\n * The parent agent selects which subagent to delegate to (via the Task tool),\n * so this module performs NO keyword routing. It survives for two narrow\n * responsibilities, both cheap and LLM-free:\n * 1. Depth guard — a subagent (HOOCODE_SUBAGENT_DEPTH>=1) must not spawn\n * further subagents.\n * 2. Complexity estimate — a heuristic recorded in the dispatch log for\n * diagnostics only.\n */\n\nexport interface TaskAnalysis {\n\t/** False only when the depth guard blocks delegation. */\n\tshould_delegate: boolean;\n\treason: string;\n\testimated_complexity: \"low\" | \"medium\" | \"high\";\n}\n\n/** Heuristic complexity estimate from file/line/scope mentions in the task. */\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\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\treason: \"Subagents cannot spawn subagents\",\n\t\t\t\testimated_complexity: \"low\",\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tshould_delegate: true,\n\t\t\treason: \"delegated to subagent\",\n\t\t\testimated_complexity: estimateComplexity(task),\n\t\t};\n\t}\n}\n"]}
|
|
@@ -1,161 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Subagent dispatch guard + lightweight task telemetry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The parent agent selects which subagent to delegate to (via the Task tool),
|
|
5
|
+
* so this module performs NO keyword routing. It survives for two narrow
|
|
6
|
+
* responsibilities, both cheap and LLM-free:
|
|
7
|
+
* 1. Depth guard — a subagent (HOOCODE_SUBAGENT_DEPTH>=1) must not spawn
|
|
8
|
+
* further subagents.
|
|
9
|
+
* 2. Complexity estimate — a heuristic recorded in the dispatch log for
|
|
10
|
+
* diagnostics only.
|
|
7
11
|
*/
|
|
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
|
-
/**
|
|
102
|
-
* Explicit, deterministic tie-break order. When two agent types match the same
|
|
103
|
-
* number of keywords, the earliest type in this list wins. This mirrors the
|
|
104
|
-
* previous (accidental) object-iteration order so routing stays stable, but the
|
|
105
|
-
* order is now intentional and documented rather than incidental.
|
|
106
|
-
*/
|
|
107
|
-
const AGENT_PRIORITY = ["explore", "edit", "test", "review", "doc"];
|
|
108
|
-
function scoreAgents(task) {
|
|
109
|
-
const lower = task.toLowerCase();
|
|
110
|
-
const scores = {
|
|
111
|
-
explore: countMatches(task, EXPLORE_KEYWORDS),
|
|
112
|
-
edit: countMatches(task, EDIT_KEYWORDS),
|
|
113
|
-
test: countMatches(task, TEST_KEYWORDS),
|
|
114
|
-
review: countMatches(task, REVIEW_KEYWORDS),
|
|
115
|
-
doc: countMatches(task, DOC_KEYWORDS),
|
|
116
|
-
};
|
|
117
|
-
// Boost doc when the task is clearly about documentation
|
|
118
|
-
if (scores.doc > 0 && (lower.includes("readme") || lower.includes("documentation") || lower.includes("document "))) {
|
|
119
|
-
scores.doc += 2;
|
|
120
|
-
}
|
|
121
|
-
// Boost test when the task is clearly about testing
|
|
122
|
-
if (scores.test > 0 && (lower.includes("test") || lower.includes("tests"))) {
|
|
123
|
-
scores.test += 2;
|
|
124
|
-
}
|
|
125
|
-
// Boost review for security-related tasks
|
|
126
|
-
if (scores.review > 0 && lower.includes("security")) {
|
|
127
|
-
scores.review += 2;
|
|
128
|
-
}
|
|
129
|
-
return scores;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Classify a task to an agent type with a normalized confidence in [0, 1].
|
|
133
|
-
*
|
|
134
|
-
* confidence = winning score / total matched score across all agent types, i.e.
|
|
135
|
-
* the share of keyword signal that agrees on the winner. 1.0 means every matched
|
|
136
|
-
* keyword pointed at one type; lower values mean the signal was split. Returns a
|
|
137
|
-
* null type with confidence 0 when nothing matched.
|
|
138
|
-
*/
|
|
139
|
-
export function classifyWithConfidence(task) {
|
|
140
|
-
const scores = scoreAgents(task);
|
|
141
|
-
let total = 0;
|
|
142
|
-
for (const type of AGENT_PRIORITY)
|
|
143
|
-
total += scores[type];
|
|
144
|
-
if (total === 0)
|
|
145
|
-
return { agent_type: null, confidence: 0 };
|
|
146
|
-
let best = AGENT_PRIORITY[0];
|
|
147
|
-
let bestScore = -1;
|
|
148
|
-
for (const type of AGENT_PRIORITY) {
|
|
149
|
-
if (scores[type] > bestScore) {
|
|
150
|
-
bestScore = scores[type];
|
|
151
|
-
best = type;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return { agent_type: bestScore > 0 ? best : null, confidence: bestScore / total };
|
|
155
|
-
}
|
|
156
|
-
function detectAgentType(task) {
|
|
157
|
-
return classifyWithConfidence(task).agent_type;
|
|
158
|
-
}
|
|
12
|
+
/** Heuristic complexity estimate from file/line/scope mentions in the task. */
|
|
159
13
|
function estimateComplexity(task) {
|
|
160
14
|
const fileMatches = task.match(/\b[\w/-]+\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\b/g);
|
|
161
15
|
const fileCount = fileMatches ? fileMatches.length : 0;
|
|
@@ -169,166 +23,21 @@ function estimateComplexity(task) {
|
|
|
169
23
|
return "medium";
|
|
170
24
|
return "low";
|
|
171
25
|
}
|
|
172
|
-
function canHandleInline(task) {
|
|
173
|
-
if (estimateComplexity(task) !== "low")
|
|
174
|
-
return false;
|
|
175
|
-
const fileMatches = task.match(/\b[\w/-]+\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\b/g);
|
|
176
|
-
if (fileMatches && fileMatches.length > 1)
|
|
177
|
-
return false;
|
|
178
|
-
const hasCrossDomain = CROSS_DOMAIN_MARKERS.some((m) => task.toLowerCase().includes(m));
|
|
179
|
-
if (hasCrossDomain)
|
|
180
|
-
return false;
|
|
181
|
-
// Exploration: broad tasks delegate; simple lookups can be inline
|
|
182
|
-
const isExplore = detectAgentType(task) === "explore";
|
|
183
|
-
if (isExplore) {
|
|
184
|
-
const broadExplore = /\b(understand|investigate|trace|how does|how is|scout|map out|get familiar)\b/i.test(task);
|
|
185
|
-
return !broadExplore;
|
|
186
|
-
}
|
|
187
|
-
// Documentation tasks always delegate to the doc subagent
|
|
188
|
-
if (detectAgentType(task) === "doc") {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
const isReadOnly = countMatches(task, EXPLORE_KEYWORDS) > 0 && countMatches(task, EDIT_KEYWORDS) === 0;
|
|
192
|
-
const isTrivialEdit = countMatches(task, EDIT_KEYWORDS) > 0 && !/\b(create|implement|build|refactor|migrate|restructure)\b/i.test(task);
|
|
193
|
-
return isReadOnly || isTrivialEdit;
|
|
194
|
-
}
|
|
195
|
-
function extractSubtasks(task) {
|
|
196
|
-
// Split on sentence boundaries and conjunctions, then classify each segment.
|
|
197
|
-
const segments = task
|
|
198
|
-
.split(/(?:[,;]|\.(?:\s+|$))\s*/)
|
|
199
|
-
.map((s) => s.trim())
|
|
200
|
-
.filter((s) => s.length > 10);
|
|
201
|
-
if (segments.length < 2) {
|
|
202
|
-
// No obvious sentence split — try cross-domain markers
|
|
203
|
-
const parts = [];
|
|
204
|
-
let remaining = task;
|
|
205
|
-
for (const marker of CROSS_DOMAIN_MARKERS) {
|
|
206
|
-
const idx = remaining.toLowerCase().indexOf(marker);
|
|
207
|
-
if (idx !== -1) {
|
|
208
|
-
parts.push(remaining.slice(0, idx).trim());
|
|
209
|
-
remaining = remaining.slice(idx + marker.length).trim();
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (parts.length > 0) {
|
|
213
|
-
parts.push(remaining);
|
|
214
|
-
return parts
|
|
215
|
-
.map((p) => {
|
|
216
|
-
const type = detectAgentType(p);
|
|
217
|
-
if (!type)
|
|
218
|
-
return null;
|
|
219
|
-
const est = estimateComplexity(p);
|
|
220
|
-
return {
|
|
221
|
-
agent_type: type,
|
|
222
|
-
prompt: p,
|
|
223
|
-
estimated_files: est === "high" ? 4 : est === "medium" ? 2 : 1,
|
|
224
|
-
};
|
|
225
|
-
})
|
|
226
|
-
.filter((s) => s !== null);
|
|
227
|
-
}
|
|
228
|
-
return [];
|
|
229
|
-
}
|
|
230
|
-
return segments
|
|
231
|
-
.map((segment) => {
|
|
232
|
-
const type = detectAgentType(segment);
|
|
233
|
-
if (!type)
|
|
234
|
-
return null;
|
|
235
|
-
const est = estimateComplexity(segment);
|
|
236
|
-
return {
|
|
237
|
-
agent_type: type,
|
|
238
|
-
prompt: segment,
|
|
239
|
-
estimated_files: est === "high" ? 4 : est === "medium" ? 2 : 1,
|
|
240
|
-
};
|
|
241
|
-
})
|
|
242
|
-
.filter((s) => s !== null);
|
|
243
|
-
}
|
|
244
|
-
/* ------------------------------------------------------------------ */
|
|
245
|
-
// Evaluator
|
|
246
26
|
export class DispatchEvaluator {
|
|
247
27
|
evaluate(task) {
|
|
248
28
|
const depth = Number.parseInt(process.env.HOOCODE_SUBAGENT_DEPTH ?? "0", 10);
|
|
249
29
|
if (depth >= 1) {
|
|
250
30
|
return {
|
|
251
31
|
should_delegate: false,
|
|
252
|
-
agent_type: null,
|
|
253
32
|
reason: "Subagents cannot spawn subagents",
|
|
254
33
|
estimated_complexity: "low",
|
|
255
|
-
parallelizable: false,
|
|
256
|
-
context_needed: [],
|
|
257
|
-
confidence: 0,
|
|
258
34
|
};
|
|
259
35
|
}
|
|
260
|
-
const { agent_type: agentType, confidence } = classifyWithConfidence(task);
|
|
261
|
-
const complexity = estimateComplexity(task);
|
|
262
|
-
const inline = canHandleInline(task);
|
|
263
|
-
const subtasks = extractSubtasks(task);
|
|
264
|
-
const parallelizable = subtasks.length > 1 || (complexity === "high" && subtasks.length > 0);
|
|
265
|
-
if (inline) {
|
|
266
|
-
return {
|
|
267
|
-
should_delegate: false,
|
|
268
|
-
agent_type: null,
|
|
269
|
-
reason: `Simple ${agentType ?? "task"} suitable for inline handling (<50 lines, 1 file, no cross-domain)`,
|
|
270
|
-
estimated_complexity: complexity,
|
|
271
|
-
parallelizable: false,
|
|
272
|
-
context_needed: [],
|
|
273
|
-
confidence,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
// When delegating, a missing keyword match defaults to explore: a fresh
|
|
277
|
-
// read-only investigation is the safest start for an ambiguous task, and the
|
|
278
|
-
// parent can re-delegate with a specific mode afterwards.
|
|
279
|
-
const delegateType = agentType ?? "explore";
|
|
280
36
|
return {
|
|
281
37
|
should_delegate: true,
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
estimated_complexity: complexity,
|
|
285
|
-
parallelizable,
|
|
286
|
-
context_needed: parallelizable ? subtasks.map((st) => st.prompt) : [task],
|
|
287
|
-
confidence,
|
|
38
|
+
reason: "delegated to subagent",
|
|
39
|
+
estimated_complexity: estimateComplexity(task),
|
|
288
40
|
};
|
|
289
41
|
}
|
|
290
|
-
shouldSplit(task) {
|
|
291
|
-
const subtasks = extractSubtasks(task);
|
|
292
|
-
if (subtasks.length >= 2) {
|
|
293
|
-
return { split: true, subtasks };
|
|
294
|
-
}
|
|
295
|
-
// Check for explicit multi-domain keywords even when sentence splitting failed
|
|
296
|
-
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);
|
|
297
|
-
if (!multiDomain)
|
|
298
|
-
return { split: false, subtasks: [] };
|
|
299
|
-
// Force split using cross-domain markers
|
|
300
|
-
const parts = [];
|
|
301
|
-
let remaining = task;
|
|
302
|
-
for (const marker of CROSS_DOMAIN_MARKERS) {
|
|
303
|
-
const idx = remaining.toLowerCase().indexOf(marker);
|
|
304
|
-
if (idx !== -1) {
|
|
305
|
-
parts.push(remaining.slice(0, idx).trim());
|
|
306
|
-
remaining = remaining.slice(idx + marker.length).trim();
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
if (parts.length === 0)
|
|
310
|
-
return { split: false, subtasks: [] };
|
|
311
|
-
parts.push(remaining);
|
|
312
|
-
const forcedSubtasks = parts
|
|
313
|
-
.map((p) => {
|
|
314
|
-
const type = detectAgentType(p);
|
|
315
|
-
if (!type)
|
|
316
|
-
return null;
|
|
317
|
-
const est = estimateComplexity(p);
|
|
318
|
-
return {
|
|
319
|
-
agent_type: type,
|
|
320
|
-
prompt: p,
|
|
321
|
-
estimated_files: est === "high" ? 4 : est === "medium" ? 2 : 1,
|
|
322
|
-
};
|
|
323
|
-
})
|
|
324
|
-
.filter((s) => s !== null);
|
|
325
|
-
return forcedSubtasks.length >= 2 ? { split: true, subtasks: forcedSubtasks } : { split: false, subtasks: [] };
|
|
326
|
-
}
|
|
327
|
-
canHandleInline(task) {
|
|
328
|
-
return canHandleInline(task);
|
|
329
|
-
}
|
|
330
|
-
getReason(analysis) {
|
|
331
|
-
return analysis.reason;
|
|
332
|
-
}
|
|
333
42
|
}
|
|
334
43
|
//# sourceMappingURL=dispatch-evaluator.js.map
|
|
@@ -1 +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\t/**\n\t * Normalized routing confidence in [0, 1]: the share of matched keyword\n\t * signal that pointed at the chosen agent type. 0 when no keywords matched.\n\t */\n\tconfidence: number;\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\n/**\n * Explicit, deterministic tie-break order. When two agent types match the same\n * number of keywords, the earliest type in this list wins. This mirrors the\n * previous (accidental) object-iteration order so routing stays stable, but the\n * order is now intentional and documented rather than incidental.\n */\nconst AGENT_PRIORITY: readonly AgentType[] = [\"explore\", \"edit\", \"test\", \"review\", \"doc\"];\n\nfunction scoreAgents(task: string): Record<AgentType, number> {\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\treturn scores;\n}\n\n/**\n * Classify a task to an agent type with a normalized confidence in [0, 1].\n *\n * confidence = winning score / total matched score across all agent types, i.e.\n * the share of keyword signal that agrees on the winner. 1.0 means every matched\n * keyword pointed at one type; lower values mean the signal was split. Returns a\n * null type with confidence 0 when nothing matched.\n */\nexport function classifyWithConfidence(task: string): { agent_type: AgentType | null; confidence: number } {\n\tconst scores = scoreAgents(task);\n\n\tlet total = 0;\n\tfor (const type of AGENT_PRIORITY) total += scores[type];\n\tif (total === 0) return { agent_type: null, confidence: 0 };\n\n\tlet best: AgentType = AGENT_PRIORITY[0];\n\tlet bestScore = -1;\n\tfor (const type of AGENT_PRIORITY) {\n\t\tif (scores[type] > bestScore) {\n\t\t\tbestScore = scores[type];\n\t\t\tbest = type;\n\t\t}\n\t}\n\n\treturn { agent_type: bestScore > 0 ? best : null, confidence: bestScore / total };\n}\n\nfunction detectAgentType(task: string): AgentType | null {\n\treturn classifyWithConfidence(task).agent_type;\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\tconfidence: 0,\n\t\t\t};\n\t\t}\n\n\t\tconst { agent_type: agentType, confidence } = classifyWithConfidence(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\tconfidence,\n\t\t\t};\n\t\t}\n\n\t\t// When delegating, a missing keyword match defaults to explore: a fresh\n\t\t// read-only investigation is the safest start for an ambiguous task, and the\n\t\t// parent can re-delegate with a specific mode afterwards.\n\t\tconst delegateType: AgentType = agentType ?? \"explore\";\n\n\t\treturn {\n\t\t\tshould_delegate: true,\n\t\t\tagent_type: delegateType,\n\t\t\treason: `${delegateType} 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\tconfidence,\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
|
+
{"version":3,"file":"dispatch-evaluator.js","sourceRoot":"","sources":["../../src/core/dispatch-evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AASH,+EAA+E;AAC/E,SAAS,kBAAkB,CAAC,IAAY,EAA6B;IACpE,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,2EAA2E,CAAC,CAAC;IAC5G,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAEvD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;IACxD,MAAM,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEpE,MAAM,SAAS,GAAG,wFAAwF,CAAC,IAAI,CAC9G,IAAI,CACJ,CAAC;IACF,MAAM,WAAW,GAAG,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEhG,IAAI,SAAS,GAAG,GAAG,IAAI,SAAS,IAAI,CAAC,IAAI,SAAS;QAAE,OAAO,MAAM,CAAC;IAClE,IAAI,SAAS,GAAG,EAAE,IAAI,SAAS,IAAI,CAAC,IAAI,WAAW;QAAE,OAAO,QAAQ,CAAC;IACrE,OAAO,KAAK,CAAC;AAAA,CACb;AAED,MAAM,OAAO,iBAAiB;IAC7B,QAAQ,CAAC,IAAY,EAAgB;QACpC,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;QAC7E,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YAChB,OAAO;gBACN,eAAe,EAAE,KAAK;gBACtB,MAAM,EAAE,kCAAkC;gBAC1C,oBAAoB,EAAE,KAAK;aAC3B,CAAC;QACH,CAAC;QAED,OAAO;YACN,eAAe,EAAE,IAAI;YACrB,MAAM,EAAE,uBAAuB;YAC/B,oBAAoB,EAAE,kBAAkB,CAAC,IAAI,CAAC;SAC9C,CAAC;IAAA,CACF;CACD","sourcesContent":["/**\n * Subagent dispatch guard + lightweight task telemetry.\n *\n * The parent agent selects which subagent to delegate to (via the Task tool),\n * so this module performs NO keyword routing. It survives for two narrow\n * responsibilities, both cheap and LLM-free:\n * 1. Depth guard — a subagent (HOOCODE_SUBAGENT_DEPTH>=1) must not spawn\n * further subagents.\n * 2. Complexity estimate — a heuristic recorded in the dispatch log for\n * diagnostics only.\n */\n\nexport interface TaskAnalysis {\n\t/** False only when the depth guard blocks delegation. */\n\tshould_delegate: boolean;\n\treason: string;\n\testimated_complexity: \"low\" | \"medium\" | \"high\";\n}\n\n/** Heuristic complexity estimate from file/line/scope mentions in the task. */\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\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\treason: \"Subagents cannot spawn subagents\",\n\t\t\t\testimated_complexity: \"low\",\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tshould_delegate: true,\n\t\t\treason: \"delegated to subagent\",\n\t\t\testimated_complexity: estimateComplexity(task),\n\t\t};\n\t}\n}\n"]}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import { type SubagentMode } from "./subagent.js";
|
|
4
3
|
export interface SubagentPoolTask {
|
|
5
4
|
task_id: string;
|
|
6
|
-
agent_type:
|
|
5
|
+
agent_type: string;
|
|
7
6
|
task: string;
|
|
8
7
|
context?: string;
|
|
9
8
|
token_budget?: number;
|
|
@@ -54,7 +53,7 @@ export interface TaskResult {
|
|
|
54
53
|
export interface DispatchOptions {
|
|
55
54
|
/** Skip evaluation and force this agent type (user/explicit override).
|
|
56
55
|
* Accepts any registry-defined agent name, not just the built-in modes. */
|
|
57
|
-
forceAgent?:
|
|
56
|
+
forceAgent?: string;
|
|
58
57
|
/** Context distilled from the calling agent, passed to the subagent. */
|
|
59
58
|
context?: string;
|
|
60
59
|
/** Model id for the subagent (defaults to the child's configured default). */
|
|
@@ -173,16 +172,6 @@ export declare class SubagentPool extends EventEmitter {
|
|
|
173
172
|
resume(task_id: string, prompt: string, options?: Omit<DispatchOptions, "forceAgent" | "sessionFile">): Promise<TaskResult>;
|
|
174
173
|
/** Recover the agent type a task was dispatched with, from its dispatch log. */
|
|
175
174
|
private readDispatchAgentType;
|
|
176
|
-
/**
|
|
177
|
-
* Dispatch a batch of subtasks concurrently.
|
|
178
|
-
*
|
|
179
|
-
* Spawns up to `maxConcurrency` at once; overflow is queued with FIFO.
|
|
180
|
-
* Returns aggregated results in the same order as the input.
|
|
181
|
-
*/
|
|
182
|
-
dispatchBatch(tasks: Array<{
|
|
183
|
-
agent_type: SubagentMode;
|
|
184
|
-
prompt: string;
|
|
185
|
-
}>, shared?: Omit<DispatchOptions, "forceAgent">): Promise<TaskResult[]>;
|
|
186
175
|
private writeDispatchLog;
|
|
187
176
|
private writeOutputJson;
|
|
188
177
|
/** Kill all running processes, clear the queue, and reject pending waiters. */
|