@kolisachint/hoocode-agent 0.4.13 → 0.4.14
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 +9 -0
- package/dist/core/dispatch-evaluator.d.ts +17 -0
- package/dist/core/dispatch-evaluator.d.ts.map +1 -1
- package/dist/core/dispatch-evaluator.js +44 -10
- package/dist/core/dispatch-evaluator.js.map +1 -1
- package/dist/core/task-store.d.ts +12 -7
- package/dist/core/task-store.d.ts.map +1 -1
- package/dist/core/task-store.js +23 -15
- package/dist/core/task-store.js.map +1 -1
- package/dist/core/token-budget.d.ts.map +1 -1
- package/dist/core/token-budget.js +15 -12
- package/dist/core/token-budget.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +1 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +8 -6
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +31 -12
- 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 +4 -2
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +7 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.14] - 2026-05-31
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Task panel restyled to the hoocode design system: the `#id` recedes (dim), completed task titles fade to muted while active/failed stay full foreground, each finished row's token count sits one step brighter than its elapsed time, and the header turn delta uses muted framing with bright numbers. The reviewed/deterministic stamp is now quiet (dim) rather than green; only the active "watching" state keeps a tint. Pure visual change, no logic changes.
|
|
8
|
+
- Subagent routing is now deterministic on ties: the dispatch evaluator uses an explicit agent-priority order instead of incidental object-iteration order, exposes a normalized `confidence` on each analysis, and defaults an ambiguous delegated task to `explore`. Tightened the parent's subagent guidance to prefer inline handling for small/quick tasks.
|
|
9
|
+
- Raised default subagent token budgets to stop hard-stopping agents mid-task: explore 35k, edit 60k, test 45k, fix 45k, review 35k, doc 30k (fallback 35k). Real per-event usage tracking is unchanged.
|
|
10
|
+
- Tool execution blocks (bash commands, diffs, file reads, etc.) are tighter: dropped the box vertical padding so consecutive tools are separated by a single blank line instead of three, saving vertical space in the TUI.
|
|
11
|
+
|
|
3
12
|
## [0.4.13] - 2026-05-30
|
|
4
13
|
|
|
5
14
|
## [0.4.12] - 2026-05-30
|
|
@@ -13,12 +13,29 @@ export interface TaskAnalysis {
|
|
|
13
13
|
estimated_complexity: "low" | "medium" | "high";
|
|
14
14
|
parallelizable: boolean;
|
|
15
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;
|
|
16
21
|
}
|
|
17
22
|
export interface Subtask {
|
|
18
23
|
agent_type: AgentType;
|
|
19
24
|
prompt: string;
|
|
20
25
|
estimated_files: number;
|
|
21
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
|
+
};
|
|
22
39
|
export declare class DispatchEvaluator {
|
|
23
40
|
evaluate(task: string): TaskAnalysis;
|
|
24
41
|
shouldSplit(task: string): {
|
|
@@ -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;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"]}
|
|
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"]}
|
|
@@ -98,7 +98,14 @@ function countMatches(text, keywords) {
|
|
|
98
98
|
const lower = text.toLowerCase();
|
|
99
99
|
return keywords.reduce((count, kw) => count + (lower.includes(kw) ? 1 : 0), 0);
|
|
100
100
|
}
|
|
101
|
-
|
|
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) {
|
|
102
109
|
const lower = task.toLowerCase();
|
|
103
110
|
const scores = {
|
|
104
111
|
explore: countMatches(task, EXPLORE_KEYWORDS),
|
|
@@ -119,15 +126,35 @@ function detectAgentType(task) {
|
|
|
119
126
|
if (scores.review > 0 && lower.includes("security")) {
|
|
120
127
|
scores.review += 2;
|
|
121
128
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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];
|
|
127
151
|
best = type;
|
|
128
152
|
}
|
|
129
153
|
}
|
|
130
|
-
return bestScore > 0 ? best : null;
|
|
154
|
+
return { agent_type: bestScore > 0 ? best : null, confidence: bestScore / total };
|
|
155
|
+
}
|
|
156
|
+
function detectAgentType(task) {
|
|
157
|
+
return classifyWithConfidence(task).agent_type;
|
|
131
158
|
}
|
|
132
159
|
function estimateComplexity(task) {
|
|
133
160
|
const fileMatches = task.match(/\b[\w/-]+\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\b/g);
|
|
@@ -227,9 +254,10 @@ export class DispatchEvaluator {
|
|
|
227
254
|
estimated_complexity: "low",
|
|
228
255
|
parallelizable: false,
|
|
229
256
|
context_needed: [],
|
|
257
|
+
confidence: 0,
|
|
230
258
|
};
|
|
231
259
|
}
|
|
232
|
-
const agentType =
|
|
260
|
+
const { agent_type: agentType, confidence } = classifyWithConfidence(task);
|
|
233
261
|
const complexity = estimateComplexity(task);
|
|
234
262
|
const inline = canHandleInline(task);
|
|
235
263
|
const subtasks = extractSubtasks(task);
|
|
@@ -242,15 +270,21 @@ export class DispatchEvaluator {
|
|
|
242
270
|
estimated_complexity: complexity,
|
|
243
271
|
parallelizable: false,
|
|
244
272
|
context_needed: [],
|
|
273
|
+
confidence,
|
|
245
274
|
};
|
|
246
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";
|
|
247
280
|
return {
|
|
248
281
|
should_delegate: true,
|
|
249
|
-
agent_type:
|
|
250
|
-
reason: `${
|
|
282
|
+
agent_type: delegateType,
|
|
283
|
+
reason: `${delegateType} task with ${complexity} complexity requires isolated subagent`,
|
|
251
284
|
estimated_complexity: complexity,
|
|
252
285
|
parallelizable,
|
|
253
286
|
context_needed: parallelizable ? subtasks.map((st) => st.prompt) : [task],
|
|
287
|
+
confidence,
|
|
254
288
|
};
|
|
255
289
|
}
|
|
256
290
|
shouldSplit(task) {
|
|
@@ -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}\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
|
+
{"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"]}
|
|
@@ -37,17 +37,22 @@ declare class TaskStore {
|
|
|
37
37
|
update(id: number, patch: TaskPatch): void;
|
|
38
38
|
remove(id: number): void;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* Drop finished tasks and restart numbering from #1 once the pane is empty.
|
|
41
41
|
*
|
|
42
|
-
* Called when a new user message arrives: finished
|
|
43
|
-
* (with their final status) for the whole turn and
|
|
44
|
-
* starts the next turn, so
|
|
42
|
+
* Called when a new user message arrives: finished tasks from the previous turn
|
|
43
|
+
* stay visible (with their final status) for the whole turn and are wiped only
|
|
44
|
+
* when the user starts the next turn, so the next turn opens with an empty pane
|
|
45
|
+
* and its first task is #1 again. Active (pending/in_progress) tasks are kept —
|
|
46
|
+
* a follow-up/steer message can arrive while a subagent is still running, and
|
|
47
|
+
* dropping its task here would orphan the live work (its later status update
|
|
48
|
+
* would target a removed id and silently vanish). Numbering only restarts once
|
|
49
|
+
* no active task survives, so ids never collide with a kept task.
|
|
45
50
|
*/
|
|
46
|
-
|
|
51
|
+
reset(): void;
|
|
47
52
|
list(): readonly Task[];
|
|
48
|
-
|
|
49
|
-
/** Clear all tasks. Intended for test isolation only. */
|
|
53
|
+
/** Wipe all tasks and restart numbering. Intended for test isolation only. */
|
|
50
54
|
clear(): void;
|
|
55
|
+
subscribe(listener: Listener): () => void;
|
|
51
56
|
private emit;
|
|
52
57
|
}
|
|
53
58
|
/** Shared, process-wide task store. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvE,MAAM,WAAW,IAAI;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;IACnB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,KAAK,CAAC,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;KACb,CAAC;CACF;AAED,MAAM,WAAW,iBAAiB;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;AAE3F,KAAK,QAAQ,GAAG,MAAM,IAAI,CAAC;AAE3B,cAAM,SAAS;IACd,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuB;IAEjD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,IAAI,CAa3D;IAED,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CASzC;IAED,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAKvB;IAED
|
|
1
|
+
{"version":3,"file":"task-store.d.ts","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvE,MAAM,WAAW,IAAI;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;IACnB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,KAAK,CAAC,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;KACb,CAAC;CACF;AAED,MAAM,WAAW,iBAAiB;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;AAE3F,KAAK,QAAQ,GAAG,MAAM,IAAI,CAAC;AAE3B,cAAM,SAAS;IACd,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuB;IAEjD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,IAAI,CAa3D;IAED,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CASzC;IAED,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAKvB;IAED;;;;;;;;;;;OAWG;IACH,KAAK,IAAI,IAAI,CAMZ;IAED,IAAI,IAAI,SAAS,IAAI,EAAE,CAEtB;IAED,8EAA8E;IAC9E,KAAK,IAAI,IAAI,CAIZ;IAED,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,IAAI,CAKxC;IAED,OAAO,CAAC,IAAI;CAKZ;AAED,uCAAuC;AACvC,eAAO,MAAM,SAAS,WAAkB,CAAC","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Subagent mode when this task is owned by a subagent (e.g. \"explore\"). */\n\tsubagentMode?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsubagentMode?: string;\n}\n\nexport type TaskPatch = Partial<Pick<Task, \"title\" | \"status\" | \"subagentMode\" | \"usage\">>;\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) return;\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\tif (active.length === 0) this.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
package/dist/core/task-store.js
CHANGED
|
@@ -47,33 +47,41 @@ class TaskStore {
|
|
|
47
47
|
this.emit();
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* Drop finished tasks and restart numbering from #1 once the pane is empty.
|
|
51
51
|
*
|
|
52
|
-
* Called when a new user message arrives: finished
|
|
53
|
-
* (with their final status) for the whole turn and
|
|
54
|
-
* starts the next turn, so
|
|
52
|
+
* Called when a new user message arrives: finished tasks from the previous turn
|
|
53
|
+
* stay visible (with their final status) for the whole turn and are wiped only
|
|
54
|
+
* when the user starts the next turn, so the next turn opens with an empty pane
|
|
55
|
+
* and its first task is #1 again. Active (pending/in_progress) tasks are kept —
|
|
56
|
+
* a follow-up/steer message can arrive while a subagent is still running, and
|
|
57
|
+
* dropping its task here would orphan the live work (its later status update
|
|
58
|
+
* would target a removed id and silently vanish). Numbering only restarts once
|
|
59
|
+
* no active task survives, so ids never collide with a kept task.
|
|
55
60
|
*/
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
reset() {
|
|
62
|
+
const active = this.tasks.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
63
|
+
if (active.length === this.tasks.length && this.nextId === 1)
|
|
64
|
+
return;
|
|
65
|
+
this.tasks = active;
|
|
66
|
+
if (active.length === 0)
|
|
67
|
+
this.nextId = 1;
|
|
68
|
+
this.emit();
|
|
61
69
|
}
|
|
62
70
|
list() {
|
|
63
71
|
return this.tasks;
|
|
64
72
|
}
|
|
73
|
+
/** Wipe all tasks and restart numbering. Intended for test isolation only. */
|
|
74
|
+
clear() {
|
|
75
|
+
this.tasks = [];
|
|
76
|
+
this.nextId = 1;
|
|
77
|
+
this.emit();
|
|
78
|
+
}
|
|
65
79
|
subscribe(listener) {
|
|
66
80
|
this.listeners.add(listener);
|
|
67
81
|
return () => {
|
|
68
82
|
this.listeners.delete(listener);
|
|
69
83
|
};
|
|
70
84
|
}
|
|
71
|
-
/** Clear all tasks. Intended for test isolation only. */
|
|
72
|
-
clear() {
|
|
73
|
-
this.tasks = [];
|
|
74
|
-
this.nextId = 1;
|
|
75
|
-
this.emit();
|
|
76
|
-
}
|
|
77
85
|
emit() {
|
|
78
86
|
for (const listener of this.listeners) {
|
|
79
87
|
listener();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-store.js","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA8BH,MAAM,SAAS;IACN,KAAK,GAAW,EAAE,CAAC;IACnB,MAAM,GAAG,CAAC,CAAC;IACF,SAAS,GAAG,IAAI,GAAG,EAAY,CAAC;IAEjD,MAAM,CAAC,KAAa,EAAE,OAAO,GAAsB,EAAE,EAAQ;QAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,IAAI,GAAS;YAClB,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE;YACjB,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,iBAAiB;YACxC,MAAM,EAAE,SAAS;YACjB,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;SACd,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IAAA,CACZ;IAED,MAAM,CAAC,EAAU,EAAE,KAAgB,EAAQ;QAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACxD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;YAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC3D,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS;YAAE,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC;QAC7E,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACxD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,MAAM,CAAC,EAAU,EAAQ;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACrD,IAAI,GAAG,KAAK,CAAC,CAAC;YAAE,OAAO;QACvB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED
|
|
1
|
+
{"version":3,"file":"task-store.js","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Subagent mode when this task is owned by a subagent (e.g. \"explore\"). */\n\tsubagentMode?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsubagentMode?: string;\n}\n\nexport type TaskPatch = Partial<Pick<Task, \"title\" | \"status\" | \"subagentMode\" | \"usage\">>;\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) return;\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Drop finished tasks and restart numbering from #1 once the pane is empty.\n\t *\n\t * Called when a new user message arrives: finished tasks from the previous turn\n\t * stay visible (with their final status) for the whole turn and are wiped only\n\t * when the user starts the next turn, so the next turn opens with an empty pane\n\t * and its first task is #1 again. Active (pending/in_progress) tasks are kept —\n\t * a follow-up/steer message can arrive while a subagent is still running, and\n\t * dropping its task here would orphan the live work (its later status update\n\t * would target a removed id and silently vanish). Numbering only restarts once\n\t * no active task survives, so ids never collide with a kept task.\n\t */\n\treset(): void {\n\t\tconst active = this.tasks.filter((t) => t.status === \"pending\" || t.status === \"in_progress\");\n\t\tif (active.length === this.tasks.length && this.nextId === 1) return;\n\t\tthis.tasks = active;\n\t\tif (active.length === 0) this.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\t/** Wipe all tasks and restart numbering. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tprivate emit(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-budget.d.ts","sourceRoot":"","sources":["../../src/core/token-budget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAK3C,MAAM,WAAW,iBAAiB;IACjC,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACrB;
|
|
1
|
+
{"version":3,"file":"token-budget.d.ts","sourceRoot":"","sources":["../../src/core/token-budget.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAK3C,MAAM,WAAW,iBAAiB;IACjC,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACrB;AAqBD,gDAAgD;AAChD,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;;;GAOG;AACH,qBAAa,WAAY,SAAQ,YAAY;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAE7B,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,YAAY,CAAM;IAE1B,+BAA+B;IAC/B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,kCAAkC;IAClC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAE3C,YAAY,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAO,EAQ9F;IAED,wDAAwD;IACxD,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAYjC;IAED,sEAAsE;IACtE,KAAK,IAAI,IAAI,CAKZ;IAED,sCAAsC;IACtC,OAAO,IAAI,MAAM,CAEhB;IAED,qCAAqC;IACrC,QAAQ,IAAI,MAAM,CAEjB;IAED,qDAAqD;IACrD,QAAQ,IAAI,OAAO,CAElB;IAED,4CAA4C;IAC5C,UAAU,IAAI,OAAO,CAEpB;IAED,4CAA4C;IAC5C,OAAO,IAAI,IAAI,CAkBd;IAED,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,SAAS;CAgDjB","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { CONFIG_DIR_NAME } from \"../config.js\";\n\nexport interface TokenBudgetConfig {\n\t/** Budget limit in tokens. */\n\tlimit: number;\n}\n\nexport interface TokenBudgetState {\n\ttask_id: string;\n\tagent_type: string;\n\tbudget: number;\n\tused: number;\n\twarned: boolean;\n\texceeded: boolean;\n\tlast_updated: number;\n}\n\n/**\n * Default token budgets per agent type (in tokens). Raised from the previous\n * 8k–16k range, which was too low and hard-stopped subagents mid-task.\n * - explore: 35 000 (read-only scanning, tracing deps)\n * - edit: 60 000 (reads, plans, writes, iterates — heaviest)\n * - test: 45 000 (writes tests, runs, debugs failures)\n * - fix: 45 000 (reads, edits, re-runs to verify the fix)\n * - review: 35 000 (read-only audit, security analysis)\n * - doc: 30 000 (reads code, writes docs/comments)\n */\nconst DEFAULT_BUDGETS: Record<string, number> = {\n\texplore: 35000,\n\tedit: 60000,\n\ttest: 45000,\n\tfix: 45000,\n\treview: 35000,\n\tdoc: 30000,\n};\n\n/** Get the default budget for an agent type. */\nexport function getDefaultBudget(agent_type: string): number {\n\treturn DEFAULT_BUDGETS[agent_type] ?? 35000;\n}\n\n/**\n * Tracks cumulative token usage for a single subagent task by parsing\n * newline-delimited JSON events from the subagent's stdout stream.\n *\n * Emits:\n * - \"budget_warning\" when 80% of the budget is consumed\n * - \"budget_exceeded\" when 100% of the budget is consumed\n */\nexport class TokenBudget extends EventEmitter {\n\tprivate readonly task_id: string;\n\tprivate readonly agent_type: string;\n\tprivate readonly limit: number;\n\tprivate readonly cwd: string;\n\n\tprivate used = 0;\n\tprivate warned = false;\n\tprivate exceeded = false;\n\tprivate stdoutBuffer = \"\";\n\n\t/** Warning threshold (80%). */\n\tprivate readonly warningThreshold: number;\n\t/** Hard-stop threshold (100%). */\n\tprivate readonly exceededThreshold: number;\n\n\tconstructor(task_id: string, agent_type: string, options: { limit?: number; cwd?: string } = {}) {\n\t\tsuper();\n\t\tthis.task_id = task_id;\n\t\tthis.agent_type = agent_type;\n\t\tthis.limit = options.limit ?? getDefaultBudget(agent_type);\n\t\tthis.cwd = options.cwd ?? process.cwd();\n\t\tthis.warningThreshold = Math.floor(this.limit * 0.8);\n\t\tthis.exceededThreshold = this.limit;\n\t}\n\n\t/** Process a chunk of stdout data from the subagent. */\n\tprocessStdout(chunk: string): void {\n\t\tthis.stdoutBuffer += chunk;\n\n\t\twhile (true) {\n\t\t\tconst lineEnd = this.stdoutBuffer.indexOf(\"\\n\");\n\t\t\tif (lineEnd === -1) break;\n\t\t\tconst line = this.stdoutBuffer.slice(0, lineEnd).trimEnd();\n\t\t\tthis.stdoutBuffer = this.stdoutBuffer.slice(lineEnd + 1);\n\t\t\tif (line) {\n\t\t\t\tthis.parseLine(line);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Flush any remaining buffered stdout. Call when the stream ends. */\n\tflush(): void {\n\t\tif (this.stdoutBuffer.trim()) {\n\t\t\tthis.parseLine(this.stdoutBuffer.trim());\n\t\t\tthis.stdoutBuffer = \"\";\n\t\t}\n\t}\n\n\t/** Current cumulative token usage. */\n\tgetUsed(): number {\n\t\treturn this.used;\n\t}\n\n\t/** Configured token budget limit. */\n\tgetLimit(): number {\n\t\treturn this.limit;\n\t}\n\n\t/** Whether the budget warning has been triggered. */\n\tisWarned(): boolean {\n\t\treturn this.warned;\n\t}\n\n\t/** Whether the budget has been exceeded. */\n\tisExceeded(): boolean {\n\t\treturn this.exceeded;\n\t}\n\n\t/** Persist current budget state to disk. */\n\tpersist(): void {\n\t\tconst state: TokenBudgetState = {\n\t\t\ttask_id: this.task_id,\n\t\t\tagent_type: this.agent_type,\n\t\t\tbudget: this.limit,\n\t\t\tused: this.used,\n\t\t\twarned: this.warned,\n\t\t\texceeded: this.exceeded,\n\t\t\tlast_updated: Date.now(),\n\t\t};\n\n\t\tconst path = this.budgetPath();\n\t\ttry {\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, JSON.stringify(state, null, 2));\n\t\t} catch {\n\t\t\t// Persistence is best-effort; silently ignore write failures\n\t\t}\n\t}\n\n\tprivate budgetPath(): string {\n\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"agents\", this.task_id, \"budget.json\");\n\t}\n\n\tprivate parseLine(line: string): void {\n\t\tlet event: unknown;\n\t\ttry {\n\t\t\tevent = JSON.parse(line);\n\t\t} catch {\n\t\t\treturn; // Not valid JSON, ignore\n\t\t}\n\n\t\tif (!event || typeof event !== \"object\") return;\n\n\t\t// Look for message_end events with assistant messages that have usage\n\t\tconst typedEvent = event as Record<string, unknown>;\n\t\tif (typedEvent.type !== \"message_end\") return;\n\n\t\tconst message = typedEvent.message as Record<string, unknown> | undefined;\n\t\tif (!message) return;\n\t\tif (message.role !== \"assistant\") return;\n\n\t\tconst usage = message.usage as Record<string, unknown> | undefined;\n\t\tif (!usage) return;\n\n\t\tconst totalTokens = typeof usage.totalTokens === \"number\" ? usage.totalTokens : undefined;\n\t\tif (totalTokens === undefined || totalTokens <= 0) return;\n\n\t\tthis.used += totalTokens;\n\n\t\t// Check thresholds\n\t\tif (!this.warned && this.used >= this.warningThreshold) {\n\t\t\tthis.warned = true;\n\t\t\tthis.emit(\"budget_warning\", {\n\t\t\t\ttask_id: this.task_id,\n\t\t\t\tmessage: \"You are near token limit. Summarize and write result.json now.\",\n\t\t\t\tused: this.used,\n\t\t\t\tlimit: this.limit,\n\t\t\t});\n\t\t}\n\n\t\tif (!this.exceeded && this.used >= this.exceededThreshold) {\n\t\t\tthis.exceeded = true;\n\t\t\tthis.emit(\"budget_exceeded\", {\n\t\t\t\ttask_id: this.task_id,\n\t\t\t\tused: this.used,\n\t\t\t\tlimit: this.limit,\n\t\t\t});\n\t\t}\n\n\t\tthis.persist();\n\t}\n}\n"]}
|
|
@@ -3,23 +3,26 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { CONFIG_DIR_NAME } from "../config.js";
|
|
5
5
|
/**
|
|
6
|
-
* Default token budgets per agent type (in tokens).
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
6
|
+
* Default token budgets per agent type (in tokens). Raised from the previous
|
|
7
|
+
* 8k–16k range, which was too low and hard-stopped subagents mid-task.
|
|
8
|
+
* - explore: 35 000 (read-only scanning, tracing deps)
|
|
9
|
+
* - edit: 60 000 (reads, plans, writes, iterates — heaviest)
|
|
10
|
+
* - test: 45 000 (writes tests, runs, debugs failures)
|
|
11
|
+
* - fix: 45 000 (reads, edits, re-runs to verify the fix)
|
|
12
|
+
* - review: 35 000 (read-only audit, security analysis)
|
|
13
|
+
* - doc: 30 000 (reads code, writes docs/comments)
|
|
12
14
|
*/
|
|
13
15
|
const DEFAULT_BUDGETS = {
|
|
14
|
-
explore:
|
|
15
|
-
edit:
|
|
16
|
-
test:
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
explore: 35000,
|
|
17
|
+
edit: 60000,
|
|
18
|
+
test: 45000,
|
|
19
|
+
fix: 45000,
|
|
20
|
+
review: 35000,
|
|
21
|
+
doc: 30000,
|
|
19
22
|
};
|
|
20
23
|
/** Get the default budget for an agent type. */
|
|
21
24
|
export function getDefaultBudget(agent_type) {
|
|
22
|
-
return DEFAULT_BUDGETS[agent_type] ??
|
|
25
|
+
return DEFAULT_BUDGETS[agent_type] ?? 35000;
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
28
|
* Tracks cumulative token usage for a single subagent task by parsing
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-budget.js","sourceRoot":"","sources":["../../src/core/token-budget.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { CONFIG_DIR_NAME } from \"../config.js\";\n\nexport interface TokenBudgetConfig {\n\t/** Budget limit in tokens. */\n\tlimit: number;\n}\n\nexport interface TokenBudgetState {\n\ttask_id: string;\n\tagent_type: string;\n\tbudget: number;\n\tused: number;\n\twarned: boolean;\n\texceeded: boolean;\n\tlast_updated: number;\n}\n\n/**\n * Default token budgets per agent type (in tokens).\n * - explore: 8 000\n * - edit: 16 000\n * - test: 16 000\n * - review: 12 000\n * - doc: 10 000\n */\nconst DEFAULT_BUDGETS: Record<string, number> = {\n\texplore: 8000,\n\tedit: 16000,\n\ttest: 16000,\n\treview: 12000,\n\tdoc: 10000,\n};\n\n/** Get the default budget for an agent type. */\nexport function getDefaultBudget(agent_type: string): number {\n\treturn DEFAULT_BUDGETS[agent_type] ?? 16000;\n}\n\n/**\n * Tracks cumulative token usage for a single subagent task by parsing\n * newline-delimited JSON events from the subagent's stdout stream.\n *\n * Emits:\n * - \"budget_warning\" when 80% of the budget is consumed\n * - \"budget_exceeded\" when 100% of the budget is consumed\n */\nexport class TokenBudget extends EventEmitter {\n\tprivate readonly task_id: string;\n\tprivate readonly agent_type: string;\n\tprivate readonly limit: number;\n\tprivate readonly cwd: string;\n\n\tprivate used = 0;\n\tprivate warned = false;\n\tprivate exceeded = false;\n\tprivate stdoutBuffer = \"\";\n\n\t/** Warning threshold (80%). */\n\tprivate readonly warningThreshold: number;\n\t/** Hard-stop threshold (100%). */\n\tprivate readonly exceededThreshold: number;\n\n\tconstructor(task_id: string, agent_type: string, options: { limit?: number; cwd?: string } = {}) {\n\t\tsuper();\n\t\tthis.task_id = task_id;\n\t\tthis.agent_type = agent_type;\n\t\tthis.limit = options.limit ?? getDefaultBudget(agent_type);\n\t\tthis.cwd = options.cwd ?? process.cwd();\n\t\tthis.warningThreshold = Math.floor(this.limit * 0.8);\n\t\tthis.exceededThreshold = this.limit;\n\t}\n\n\t/** Process a chunk of stdout data from the subagent. */\n\tprocessStdout(chunk: string): void {\n\t\tthis.stdoutBuffer += chunk;\n\n\t\twhile (true) {\n\t\t\tconst lineEnd = this.stdoutBuffer.indexOf(\"\\n\");\n\t\t\tif (lineEnd === -1) break;\n\t\t\tconst line = this.stdoutBuffer.slice(0, lineEnd).trimEnd();\n\t\t\tthis.stdoutBuffer = this.stdoutBuffer.slice(lineEnd + 1);\n\t\t\tif (line) {\n\t\t\t\tthis.parseLine(line);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Flush any remaining buffered stdout. Call when the stream ends. */\n\tflush(): void {\n\t\tif (this.stdoutBuffer.trim()) {\n\t\t\tthis.parseLine(this.stdoutBuffer.trim());\n\t\t\tthis.stdoutBuffer = \"\";\n\t\t}\n\t}\n\n\t/** Current cumulative token usage. */\n\tgetUsed(): number {\n\t\treturn this.used;\n\t}\n\n\t/** Configured token budget limit. */\n\tgetLimit(): number {\n\t\treturn this.limit;\n\t}\n\n\t/** Whether the budget warning has been triggered. */\n\tisWarned(): boolean {\n\t\treturn this.warned;\n\t}\n\n\t/** Whether the budget has been exceeded. */\n\tisExceeded(): boolean {\n\t\treturn this.exceeded;\n\t}\n\n\t/** Persist current budget state to disk. */\n\tpersist(): void {\n\t\tconst state: TokenBudgetState = {\n\t\t\ttask_id: this.task_id,\n\t\t\tagent_type: this.agent_type,\n\t\t\tbudget: this.limit,\n\t\t\tused: this.used,\n\t\t\twarned: this.warned,\n\t\t\texceeded: this.exceeded,\n\t\t\tlast_updated: Date.now(),\n\t\t};\n\n\t\tconst path = this.budgetPath();\n\t\ttry {\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, JSON.stringify(state, null, 2));\n\t\t} catch {\n\t\t\t// Persistence is best-effort; silently ignore write failures\n\t\t}\n\t}\n\n\tprivate budgetPath(): string {\n\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"agents\", this.task_id, \"budget.json\");\n\t}\n\n\tprivate parseLine(line: string): void {\n\t\tlet event: unknown;\n\t\ttry {\n\t\t\tevent = JSON.parse(line);\n\t\t} catch {\n\t\t\treturn; // Not valid JSON, ignore\n\t\t}\n\n\t\tif (!event || typeof event !== \"object\") return;\n\n\t\t// Look for message_end events with assistant messages that have usage\n\t\tconst typedEvent = event as Record<string, unknown>;\n\t\tif (typedEvent.type !== \"message_end\") return;\n\n\t\tconst message = typedEvent.message as Record<string, unknown> | undefined;\n\t\tif (!message) return;\n\t\tif (message.role !== \"assistant\") return;\n\n\t\tconst usage = message.usage as Record<string, unknown> | undefined;\n\t\tif (!usage) return;\n\n\t\tconst totalTokens = typeof usage.totalTokens === \"number\" ? usage.totalTokens : undefined;\n\t\tif (totalTokens === undefined || totalTokens <= 0) return;\n\n\t\tthis.used += totalTokens;\n\n\t\t// Check thresholds\n\t\tif (!this.warned && this.used >= this.warningThreshold) {\n\t\t\tthis.warned = true;\n\t\t\tthis.emit(\"budget_warning\", {\n\t\t\t\ttask_id: this.task_id,\n\t\t\t\tmessage: \"You are near token limit. Summarize and write result.json now.\",\n\t\t\t\tused: this.used,\n\t\t\t\tlimit: this.limit,\n\t\t\t});\n\t\t}\n\n\t\tif (!this.exceeded && this.used >= this.exceededThreshold) {\n\t\t\tthis.exceeded = true;\n\t\t\tthis.emit(\"budget_exceeded\", {\n\t\t\t\ttask_id: this.task_id,\n\t\t\t\tused: this.used,\n\t\t\t\tlimit: this.limit,\n\t\t\t});\n\t\t}\n\n\t\tthis.persist();\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"token-budget.js","sourceRoot":"","sources":["../../src/core/token-budget.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { CONFIG_DIR_NAME } from \"../config.js\";\n\nexport interface TokenBudgetConfig {\n\t/** Budget limit in tokens. */\n\tlimit: number;\n}\n\nexport interface TokenBudgetState {\n\ttask_id: string;\n\tagent_type: string;\n\tbudget: number;\n\tused: number;\n\twarned: boolean;\n\texceeded: boolean;\n\tlast_updated: number;\n}\n\n/**\n * Default token budgets per agent type (in tokens). Raised from the previous\n * 8k–16k range, which was too low and hard-stopped subagents mid-task.\n * - explore: 35 000 (read-only scanning, tracing deps)\n * - edit: 60 000 (reads, plans, writes, iterates — heaviest)\n * - test: 45 000 (writes tests, runs, debugs failures)\n * - fix: 45 000 (reads, edits, re-runs to verify the fix)\n * - review: 35 000 (read-only audit, security analysis)\n * - doc: 30 000 (reads code, writes docs/comments)\n */\nconst DEFAULT_BUDGETS: Record<string, number> = {\n\texplore: 35000,\n\tedit: 60000,\n\ttest: 45000,\n\tfix: 45000,\n\treview: 35000,\n\tdoc: 30000,\n};\n\n/** Get the default budget for an agent type. */\nexport function getDefaultBudget(agent_type: string): number {\n\treturn DEFAULT_BUDGETS[agent_type] ?? 35000;\n}\n\n/**\n * Tracks cumulative token usage for a single subagent task by parsing\n * newline-delimited JSON events from the subagent's stdout stream.\n *\n * Emits:\n * - \"budget_warning\" when 80% of the budget is consumed\n * - \"budget_exceeded\" when 100% of the budget is consumed\n */\nexport class TokenBudget extends EventEmitter {\n\tprivate readonly task_id: string;\n\tprivate readonly agent_type: string;\n\tprivate readonly limit: number;\n\tprivate readonly cwd: string;\n\n\tprivate used = 0;\n\tprivate warned = false;\n\tprivate exceeded = false;\n\tprivate stdoutBuffer = \"\";\n\n\t/** Warning threshold (80%). */\n\tprivate readonly warningThreshold: number;\n\t/** Hard-stop threshold (100%). */\n\tprivate readonly exceededThreshold: number;\n\n\tconstructor(task_id: string, agent_type: string, options: { limit?: number; cwd?: string } = {}) {\n\t\tsuper();\n\t\tthis.task_id = task_id;\n\t\tthis.agent_type = agent_type;\n\t\tthis.limit = options.limit ?? getDefaultBudget(agent_type);\n\t\tthis.cwd = options.cwd ?? process.cwd();\n\t\tthis.warningThreshold = Math.floor(this.limit * 0.8);\n\t\tthis.exceededThreshold = this.limit;\n\t}\n\n\t/** Process a chunk of stdout data from the subagent. */\n\tprocessStdout(chunk: string): void {\n\t\tthis.stdoutBuffer += chunk;\n\n\t\twhile (true) {\n\t\t\tconst lineEnd = this.stdoutBuffer.indexOf(\"\\n\");\n\t\t\tif (lineEnd === -1) break;\n\t\t\tconst line = this.stdoutBuffer.slice(0, lineEnd).trimEnd();\n\t\t\tthis.stdoutBuffer = this.stdoutBuffer.slice(lineEnd + 1);\n\t\t\tif (line) {\n\t\t\t\tthis.parseLine(line);\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Flush any remaining buffered stdout. Call when the stream ends. */\n\tflush(): void {\n\t\tif (this.stdoutBuffer.trim()) {\n\t\t\tthis.parseLine(this.stdoutBuffer.trim());\n\t\t\tthis.stdoutBuffer = \"\";\n\t\t}\n\t}\n\n\t/** Current cumulative token usage. */\n\tgetUsed(): number {\n\t\treturn this.used;\n\t}\n\n\t/** Configured token budget limit. */\n\tgetLimit(): number {\n\t\treturn this.limit;\n\t}\n\n\t/** Whether the budget warning has been triggered. */\n\tisWarned(): boolean {\n\t\treturn this.warned;\n\t}\n\n\t/** Whether the budget has been exceeded. */\n\tisExceeded(): boolean {\n\t\treturn this.exceeded;\n\t}\n\n\t/** Persist current budget state to disk. */\n\tpersist(): void {\n\t\tconst state: TokenBudgetState = {\n\t\t\ttask_id: this.task_id,\n\t\t\tagent_type: this.agent_type,\n\t\t\tbudget: this.limit,\n\t\t\tused: this.used,\n\t\t\twarned: this.warned,\n\t\t\texceeded: this.exceeded,\n\t\t\tlast_updated: Date.now(),\n\t\t};\n\n\t\tconst path = this.budgetPath();\n\t\ttry {\n\t\t\tmkdirSync(dirname(path), { recursive: true });\n\t\t\twriteFileSync(path, JSON.stringify(state, null, 2));\n\t\t} catch {\n\t\t\t// Persistence is best-effort; silently ignore write failures\n\t\t}\n\t}\n\n\tprivate budgetPath(): string {\n\t\treturn join(this.cwd, CONFIG_DIR_NAME, \"agents\", this.task_id, \"budget.json\");\n\t}\n\n\tprivate parseLine(line: string): void {\n\t\tlet event: unknown;\n\t\ttry {\n\t\t\tevent = JSON.parse(line);\n\t\t} catch {\n\t\t\treturn; // Not valid JSON, ignore\n\t\t}\n\n\t\tif (!event || typeof event !== \"object\") return;\n\n\t\t// Look for message_end events with assistant messages that have usage\n\t\tconst typedEvent = event as Record<string, unknown>;\n\t\tif (typedEvent.type !== \"message_end\") return;\n\n\t\tconst message = typedEvent.message as Record<string, unknown> | undefined;\n\t\tif (!message) return;\n\t\tif (message.role !== \"assistant\") return;\n\n\t\tconst usage = message.usage as Record<string, unknown> | undefined;\n\t\tif (!usage) return;\n\n\t\tconst totalTokens = typeof usage.totalTokens === \"number\" ? usage.totalTokens : undefined;\n\t\tif (totalTokens === undefined || totalTokens <= 0) return;\n\n\t\tthis.used += totalTokens;\n\n\t\t// Check thresholds\n\t\tif (!this.warned && this.used >= this.warningThreshold) {\n\t\t\tthis.warned = true;\n\t\t\tthis.emit(\"budget_warning\", {\n\t\t\t\ttask_id: this.task_id,\n\t\t\t\tmessage: \"You are near token limit. Summarize and write result.json now.\",\n\t\t\t\tused: this.used,\n\t\t\t\tlimit: this.limit,\n\t\t\t});\n\t\t}\n\n\t\tif (!this.exceeded && this.used >= this.exceededThreshold) {\n\t\t\tthis.exceeded = true;\n\t\t\tthis.emit(\"budget_exceeded\", {\n\t\t\t\ttask_id: this.task_id,\n\t\t\t\tused: this.used,\n\t\t\t\tlimit: this.limit,\n\t\t\t});\n\t\t}\n\n\t\tthis.persist();\n\t}\n}\n"]}
|