cli-claw-kit 0.0.1
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/LICENSE +21 -0
- package/README.md +245 -0
- package/config/default-groups.json +1 -0
- package/config/global-agents-md.template.md +37 -0
- package/config/mount-allowlist.json +11 -0
- package/container/Dockerfile +160 -0
- package/container/agent-runner/dist/.tsbuildinfo +1 -0
- package/container/agent-runner/dist/agent-definitions.js +22 -0
- package/container/agent-runner/dist/channel-prefixes.js +16 -0
- package/container/agent-runner/dist/codex-config.js +29 -0
- package/container/agent-runner/dist/image-detector.js +96 -0
- package/container/agent-runner/dist/index.js +2587 -0
- package/container/agent-runner/dist/mcp-tools.js +1076 -0
- package/container/agent-runner/dist/stream-event.types.js +5 -0
- package/container/agent-runner/dist/stream-processor.js +867 -0
- package/container/agent-runner/dist/types.js +6 -0
- package/container/agent-runner/dist/utils.js +115 -0
- package/container/agent-runner/package.json +36 -0
- package/container/agent-runner/prompts/security-rules.md +31 -0
- package/container/agent-runner/src/agent-definitions.ts +27 -0
- package/container/agent-runner/src/channel-prefixes.ts +16 -0
- package/container/agent-runner/src/codex-config.ts +40 -0
- package/container/agent-runner/src/image-detector.ts +116 -0
- package/container/agent-runner/src/index.ts +3107 -0
- package/container/agent-runner/src/mcp-tools.ts +1295 -0
- package/container/agent-runner/src/stream-event.types.ts +10 -0
- package/container/agent-runner/src/stream-processor.ts +932 -0
- package/container/agent-runner/src/types.ts +75 -0
- package/container/agent-runner/src/utils.ts +114 -0
- package/container/agent-runner/tsconfig.json +17 -0
- package/container/build.sh +28 -0
- package/container/entrypoint.sh +64 -0
- package/container/skills/agent-browser/SKILL.md +159 -0
- package/container/skills/install-skill/SKILL.md +64 -0
- package/container/skills/post-test-cleanup/SKILL.md +121 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/agent-output-parser.js +459 -0
- package/dist/app-root.js +52 -0
- package/dist/assistant-meta-footer.js +1 -0
- package/dist/auth.js +91 -0
- package/dist/billing.js +694 -0
- package/dist/channel-prefixes.js +16 -0
- package/dist/cli.js +86 -0
- package/dist/commands.js +79 -0
- package/dist/config.js +120 -0
- package/dist/container-runner.js +981 -0
- package/dist/daily-summary.js +210 -0
- package/dist/db.js +3683 -0
- package/dist/dingtalk.js +1347 -0
- package/dist/feishu-markdown-style.js +97 -0
- package/dist/feishu-streaming-card.js +1875 -0
- package/dist/feishu.js +1628 -0
- package/dist/file-manager.js +270 -0
- package/dist/group-queue.js +1070 -0
- package/dist/group-runtime.js +35 -0
- package/dist/host-workspace-cwd.js +85 -0
- package/dist/im-channel.js +384 -0
- package/dist/im-command-utils.js +142 -0
- package/dist/im-downloader.js +45 -0
- package/dist/im-manager.js +527 -0
- package/dist/im-utils.js +53 -0
- package/dist/image-detector.js +96 -0
- package/dist/index.js +5828 -0
- package/dist/logger.js +22 -0
- package/dist/mcp-utils.js +66 -0
- package/dist/message-attachments.js +69 -0
- package/dist/message-notifier.js +36 -0
- package/dist/middleware/auth.js +85 -0
- package/dist/mount-security.js +315 -0
- package/dist/permissions.js +67 -0
- package/dist/project-memory.js +6 -0
- package/dist/provider-pool.js +189 -0
- package/dist/qq.js +826 -0
- package/dist/reset-admin.js +42 -0
- package/dist/routes/admin.js +543 -0
- package/dist/routes/agent-definitions.js +241 -0
- package/dist/routes/agents.js +533 -0
- package/dist/routes/auth.js +675 -0
- package/dist/routes/billing.js +490 -0
- package/dist/routes/browse.js +210 -0
- package/dist/routes/bug-report.js +387 -0
- package/dist/routes/config.js +1868 -0
- package/dist/routes/files.js +671 -0
- package/dist/routes/groups.js +1367 -0
- package/dist/routes/mcp-servers.js +320 -0
- package/dist/routes/memory.js +523 -0
- package/dist/routes/monitor.js +307 -0
- package/dist/routes/skills.js +777 -0
- package/dist/routes/tasks.js +509 -0
- package/dist/routes/usage.js +64 -0
- package/dist/routes/workspace-config.js +458 -0
- package/dist/runtime-build.js +112 -0
- package/dist/runtime-command-handler.js +189 -0
- package/dist/runtime-command-registry.js +1 -0
- package/dist/runtime-config.js +1777 -0
- package/dist/runtime-identity.js +52 -0
- package/dist/schemas.js +590 -0
- package/dist/script-runner.js +64 -0
- package/dist/sdk-query.js +82 -0
- package/dist/skill-utils.js +145 -0
- package/dist/sqlite-compat.js +19 -0
- package/dist/stream-event.types.js +5 -0
- package/dist/streaming-runtime-meta.js +29 -0
- package/dist/task-scheduler.js +695 -0
- package/dist/task-utils.js +13 -0
- package/dist/telegram-pairing.js +59 -0
- package/dist/telegram.js +897 -0
- package/dist/terminal-manager.js +307 -0
- package/dist/tool-step-display.js +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +85 -0
- package/dist/web-context.js +161 -0
- package/dist/web.js +1377 -0
- package/dist/wechat-crypto.js +182 -0
- package/dist/wechat.js +589 -0
- package/dist/workspace-runtime-reset.js +35 -0
- package/package.json +107 -0
- package/shared/assistant-meta-footer.ts +127 -0
- package/shared/channel-prefixes.ts +16 -0
- package/shared/dist/assistant-meta-footer.d.ts +29 -0
- package/shared/dist/assistant-meta-footer.js +85 -0
- package/shared/dist/channel-prefixes.d.ts +4 -0
- package/shared/dist/channel-prefixes.js +16 -0
- package/shared/dist/image-detector.d.ts +20 -0
- package/shared/dist/image-detector.js +96 -0
- package/shared/dist/runtime-command-registry.d.ts +38 -0
- package/shared/dist/runtime-command-registry.js +185 -0
- package/shared/dist/stream-event.d.ts +65 -0
- package/shared/dist/stream-event.js +8 -0
- package/shared/dist/tool-step-display.d.ts +4 -0
- package/shared/dist/tool-step-display.js +11 -0
- package/shared/image-detector.ts +116 -0
- package/shared/runtime-command-registry.ts +252 -0
- package/shared/stream-event.ts +67 -0
- package/shared/tool-step-display.ts +21 -0
- package/shared/tsconfig.json +24 -0
- package/web/dist/assets/BillingPage-B1wBR_o-.js +52 -0
- package/web/dist/assets/ChatPage-6GBZ9nXN.css +32 -0
- package/web/dist/assets/ChatPage-BOJcXtaj.js +161 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/SettingsPage-DoY7FoZ_.js +153 -0
- package/web/dist/assets/ShareImageDialog-C1ga8b7l.js +22 -0
- package/web/dist/assets/TasksPage-CRivnNsx.js +14 -0
- package/web/dist/assets/_basePickBy-Bf-bSoS9.js +1 -0
- package/web/dist/assets/_baseUniq-zAOaCuKw.js +1 -0
- package/web/dist/assets/arc-Dm9mVQ9U.js +1 -0
- package/web/dist/assets/architectureDiagram-2XIMDMQ5-BLmzX1wr.js +36 -0
- package/web/dist/assets/band-CquvqAHh.js +1 -0
- package/web/dist/assets/blockDiagram-WCTKOSBZ-B9pcqm3j.js +132 -0
- package/web/dist/assets/c4Diagram-IC4MRINW-Cytx1q3b.js +10 -0
- package/web/dist/assets/channel-BOVj73LR.js +1 -0
- package/web/dist/assets/channel-meta-CQD0Pei-.js +41 -0
- package/web/dist/assets/chunk-4BX2VUAB-0ToDr6RE.js +1 -0
- package/web/dist/assets/chunk-55IACEB6-DQDjnXfS.js +1 -0
- package/web/dist/assets/chunk-FMBD7UC4-Di8ABm6c.js +15 -0
- package/web/dist/assets/chunk-JSJVCQXG-BZQN6rnX.js +1 -0
- package/web/dist/assets/chunk-KX2RTZJC-zBbcpaN_.js +1 -0
- package/web/dist/assets/chunk-NQ4KR5QH-BCrLoU88.js +220 -0
- package/web/dist/assets/chunk-QZHKN3VN-Bqk8juan.js +1 -0
- package/web/dist/assets/chunk-WL4C6EOR-D2YX-MHY.js +189 -0
- package/web/dist/assets/classDiagram-VBA2DB6C-DUUoMyaK.js +1 -0
- package/web/dist/assets/classDiagram-v2-RAHNMMFH-DUUoMyaK.js +1 -0
- package/web/dist/assets/clone-BmaCesfa.js +1 -0
- package/web/dist/assets/cose-bilkent-S5V4N54A-CTsv6qQA.js +1 -0
- package/web/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/web/dist/assets/dagre-KLK3FWXG-Ci4Jh9nu.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-E7M64L7V-BFRnfTI2.js +24 -0
- package/web/dist/assets/diagram-IFDJBPK2-B7Zhnp0b.js +43 -0
- package/web/dist/assets/diagram-P4PSJMXO-BVyP7nwq.js +24 -0
- package/web/dist/assets/erDiagram-INFDFZHY-NorKdTOF.js +70 -0
- package/web/dist/assets/error-CGD5mp5f.js +1 -0
- package/web/dist/assets/flowDiagram-PKNHOUZH-Ch97nABF.js +162 -0
- package/web/dist/assets/ganttDiagram-A5KZAMGK-BQ2pLWsy.js +292 -0
- package/web/dist/assets/gitGraphDiagram-K3NZZRJ6-bcvnBsD2.js +65 -0
- package/web/dist/assets/graph-CeAEckur.js +1 -0
- package/web/dist/assets/index-CPnL1_qC.js +768 -0
- package/web/dist/assets/index-DVevCbcO.css +10 -0
- package/web/dist/assets/infoDiagram-LFFYTUFH-CcsrFdj-.js +2 -0
- package/web/dist/assets/init-Dmth1JHB.js +1 -0
- package/web/dist/assets/ishikawaDiagram-PHBUUO56-1upyMfHN.js +70 -0
- package/web/dist/assets/journeyDiagram-4ABVD52K-CKUi-V0c.js +139 -0
- package/web/dist/assets/kanban-definition-K7BYSVSG-DOnQwXfL.js +89 -0
- package/web/dist/assets/layout-BmMMqTnJ.js +1 -0
- package/web/dist/assets/linear-DiaJloY5.js +1 -0
- package/web/dist/assets/mermaid.core-BWLV1B2v.js +254 -0
- package/web/dist/assets/mindmap-definition-YRQLILUH-BeAKHVWP.js +68 -0
- package/web/dist/assets/ordinal-DILIJJjt.js +1 -0
- package/web/dist/assets/pieDiagram-SKSYHLDU-DfiMSfWo.js +30 -0
- package/web/dist/assets/quadrantDiagram-337W2JSQ-wZxZOJxd.js +7 -0
- package/web/dist/assets/requirementDiagram-Z7DCOOCP-BK4HHm17.js +73 -0
- package/web/dist/assets/sankeyDiagram-WA2Y5GQK-BX6t2avX.js +10 -0
- package/web/dist/assets/sequenceDiagram-2WXFIKYE-BPQlkbAa.js +145 -0
- package/web/dist/assets/sheet-rI0FfB1g.js +6 -0
- package/web/dist/assets/sliders-horizontal-CuijWFNK.js +6 -0
- package/web/dist/assets/sparkles-BsMYXJoT.js +11 -0
- package/web/dist/assets/square-0CqMX1Q3.js +11 -0
- package/web/dist/assets/stateDiagram-RAJIS63D-DxkV0Vwd.js +1 -0
- package/web/dist/assets/stateDiagram-v2-FVOUBMTO-qLYoiOPe.js +1 -0
- package/web/dist/assets/step-D51IIHGA.js +1 -0
- package/web/dist/assets/tasks-D8JjBTwx.js +1 -0
- package/web/dist/assets/time-O8zIGux3.js +1 -0
- package/web/dist/assets/timeline-definition-YZTLITO2-kNp1DyFc.js +61 -0
- package/web/dist/assets/treemap-KZPCXAKY-CkrClVhk.js +162 -0
- package/web/dist/assets/utils-KGAn0XTg.js +11 -0
- package/web/dist/assets/vennDiagram-LZ73GAT5-CgdzEZz4.js +34 -0
- package/web/dist/assets/xychartDiagram-JWTSCODW-DfYGPfNB.js +7 -0
- package/web/dist/assets/zap-_hKJYy7J.js +6 -0
- package/web/dist/favicon.svg +332 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-55-Regular.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-65-Medium.woff2 +0 -0
- package/web/dist/fonts/AlibabaPuHuiTi-3-75-SemiBold.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin-ext.woff2 +0 -0
- package/web/dist/fonts/DMSans-latin.woff2 +0 -0
- package/web/dist/icons/README.md +20 -0
- package/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/web/dist/icons/icon-128.png +0 -0
- package/web/dist/icons/icon-144.png +0 -0
- package/web/dist/icons/icon-152.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-192.svg +332 -0
- package/web/dist/icons/icon-384.png +0 -0
- package/web/dist/icons/icon-48.png +0 -0
- package/web/dist/icons/icon-512-maskable.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon-512.svg +332 -0
- package/web/dist/icons/icon-72.png +0 -0
- package/web/dist/icons/icon-96.png +0 -0
- package/web/dist/icons/loading-logo.svg +332 -0
- package/web/dist/icons/logo-1024.png +0 -0
- package/web/dist/icons/logo-icon.svg +332 -0
- package/web/dist/icons/logo-text.svg +332 -0
- package/web/dist/index.html +30 -0
- package/web/dist/manifest.webmanifest +1 -0
- package/web/dist/registerSW.js +1 -0
- package/web/dist/sw.js +1 -0
- package/web/dist/workbox-08d6266a.js +1 -0
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamEventProcessor — encapsulates all streaming event processing logic
|
|
3
|
+
* extracted from runQuery() in index.ts.
|
|
4
|
+
*
|
|
5
|
+
* Manages:
|
|
6
|
+
* - Text/thinking buffering and flushing
|
|
7
|
+
* - Tool use start/end tracking (top-level, nested, Skill, Task)
|
|
8
|
+
* - Sub-agent message conversion to StreamEvents
|
|
9
|
+
* - Cleanup of residual tool states
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ContainerOutput, StreamEvent } from './types.js';
|
|
13
|
+
import { extractSkillName, summarizeToolInput } from './utils.js';
|
|
14
|
+
|
|
15
|
+
/** Tools with specialized input_json_delta handling — generic accumulation is skipped for these. */
|
|
16
|
+
const SPECIAL_TOOLS = ['Skill', 'Task', 'Agent', 'AskUserQuestion', 'TodoWrite'];
|
|
17
|
+
|
|
18
|
+
type EmitFn = (output: ContainerOutput) => void;
|
|
19
|
+
type LogFn = (message: string) => void;
|
|
20
|
+
export class StreamEventProcessor {
|
|
21
|
+
private readonly emit: EmitFn;
|
|
22
|
+
private readonly log: LogFn;
|
|
23
|
+
|
|
24
|
+
// Text aggregation buffers — keyed by parentToolUseId (BUF_MAIN for top-level)
|
|
25
|
+
private readonly BUF_MAIN = '__main__';
|
|
26
|
+
private readonly streamBufs = new Map<string, { text: string; think: string }>();
|
|
27
|
+
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
28
|
+
private seenTextualResult = false;
|
|
29
|
+
private readonly FLUSH_MS = 100;
|
|
30
|
+
private readonly FLUSH_CHARS = 200;
|
|
31
|
+
|
|
32
|
+
// Full text accumulator — SDK's result.result only contains the last text block;
|
|
33
|
+
// this accumulates all text_delta to produce the complete response.
|
|
34
|
+
private fullTextAccumulator = '';
|
|
35
|
+
|
|
36
|
+
// Top-level tool use tracking
|
|
37
|
+
private activeTopLevelToolUseId: string | null = null;
|
|
38
|
+
// Active Skill tool ID: tools called inside Skill may lack parent_tool_use_id
|
|
39
|
+
private activeSkillToolUseId: string | null = null;
|
|
40
|
+
|
|
41
|
+
// Accumulate Skill tool input_json_delta to extract skillName
|
|
42
|
+
// Keyed by content block index (event.index) to match deltas correctly
|
|
43
|
+
private readonly pendingSkillInput = new Map<number, {
|
|
44
|
+
toolUseId: string; inputJson: string; resolved: boolean;
|
|
45
|
+
parentToolUseId: string | null; isNested: boolean;
|
|
46
|
+
}>();
|
|
47
|
+
|
|
48
|
+
// Accumulate Task tool input_json_delta to extract description and team_name
|
|
49
|
+
private readonly pendingTaskInput = new Map<number, {
|
|
50
|
+
toolUseId: string; inputJson: string; resolved: boolean; isTeammate?: boolean;
|
|
51
|
+
}>();
|
|
52
|
+
|
|
53
|
+
// Accumulate AskUserQuestion tool input_json_delta to extract questions/options
|
|
54
|
+
private readonly pendingAskUserInput = new Map<number, {
|
|
55
|
+
toolUseId: string; inputJson: string; resolved: boolean;
|
|
56
|
+
parentToolUseId: string | null; isNested: boolean;
|
|
57
|
+
}>();
|
|
58
|
+
|
|
59
|
+
// Accumulate TodoWrite tool input_json_delta to extract todos
|
|
60
|
+
private readonly pendingTodoInput = new Map<number, {
|
|
61
|
+
toolUseId: string; inputJson: string; resolved: boolean;
|
|
62
|
+
parentToolUseId: string | null; isNested: boolean;
|
|
63
|
+
}>();
|
|
64
|
+
// Accumulate generic tool input_json_delta to extract toolInputSummary
|
|
65
|
+
private readonly pendingGenericInput = new Map<number, {
|
|
66
|
+
toolUseId: string; inputJson: string; resolved: boolean;
|
|
67
|
+
parentToolUseId: string | null; isNested: boolean;
|
|
68
|
+
toolName: string;
|
|
69
|
+
}>();
|
|
70
|
+
|
|
71
|
+
// Confirmed teammate Tasks (detected via team_name)
|
|
72
|
+
private readonly teammateTaskToolUseIds = new Set<string>();
|
|
73
|
+
|
|
74
|
+
// Task tool_use_ids — tool_use_end is only emitted via tool_use_summary,
|
|
75
|
+
// not prematurely when the next content block starts
|
|
76
|
+
private readonly taskToolUseIds = new Set<string>();
|
|
77
|
+
|
|
78
|
+
// Track active nested tool per parent context (for synthetic tool_use_end)
|
|
79
|
+
private readonly activeNestedToolByParent = new Map<string, { toolUseId: string; toolName: string }>();
|
|
80
|
+
|
|
81
|
+
// Background Task tool_use_ids (run_in_background: true)
|
|
82
|
+
private readonly backgroundTaskToolUseIds = new Set<string>();
|
|
83
|
+
|
|
84
|
+
// SDK internal task_id → API tool_use_id mapping.
|
|
85
|
+
// Built from task_started/task_progress system messages so that
|
|
86
|
+
// task_notification (which carries SDK task_id) can be translated
|
|
87
|
+
// back to the tool_use_id used at creation time.
|
|
88
|
+
private readonly sdkTaskIdToToolUseId = new Map<string, string>();
|
|
89
|
+
|
|
90
|
+
// Sub-agent active tools per parent task ID
|
|
91
|
+
private readonly activeSubAgentToolsByTask = new Map<string, Set<string>>();
|
|
92
|
+
|
|
93
|
+
constructor(emit: EmitFn, log: LogFn) {
|
|
94
|
+
this.emit = emit;
|
|
95
|
+
this.log = log;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Get or create a buffer for a given key. */
|
|
99
|
+
private getBuf(key: string): { text: string; think: string } {
|
|
100
|
+
let b = this.streamBufs.get(key);
|
|
101
|
+
if (!b) { b = { text: '', think: '' }; this.streamBufs.set(key, b); }
|
|
102
|
+
return b;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Flush all pending text/thinking buffers. */
|
|
106
|
+
private flushBuffers(): void {
|
|
107
|
+
for (const [key, buf] of this.streamBufs) {
|
|
108
|
+
const pid = key === this.BUF_MAIN ? undefined : key;
|
|
109
|
+
if (buf.text) {
|
|
110
|
+
this.emit({ status: 'stream', result: null, streamEvent: { eventType: 'text_delta', text: buf.text, parentToolUseId: pid } });
|
|
111
|
+
buf.text = '';
|
|
112
|
+
}
|
|
113
|
+
if (buf.think) {
|
|
114
|
+
this.emit({ status: 'stream', result: null, streamEvent: { eventType: 'thinking_delta', text: buf.think, parentToolUseId: pid } });
|
|
115
|
+
buf.think = '';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.flushTimer = null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Schedule a flush, either immediately (if buffer is large enough) or after FLUSH_MS. */
|
|
122
|
+
private scheduleFlush(): void {
|
|
123
|
+
let maxLen = 0;
|
|
124
|
+
for (const buf of this.streamBufs.values()) {
|
|
125
|
+
maxLen = Math.max(maxLen, buf.text.length, buf.think.length);
|
|
126
|
+
}
|
|
127
|
+
if (maxLen >= this.FLUSH_CHARS) {
|
|
128
|
+
if (this.flushTimer) { clearTimeout(this.flushTimer); this.flushTimer = null; }
|
|
129
|
+
this.flushBuffers();
|
|
130
|
+
} else if (!this.flushTimer) {
|
|
131
|
+
this.flushTimer = setTimeout(() => this.flushBuffers(), this.FLUSH_MS);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Clean up tools associated with a Task. */
|
|
136
|
+
private cleanupTaskTools(taskId: string): void {
|
|
137
|
+
const nested = this.activeNestedToolByParent.get(taskId);
|
|
138
|
+
if (nested) {
|
|
139
|
+
this.emit({ status: 'stream', result: null,
|
|
140
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: nested.toolUseId, parentToolUseId: taskId },
|
|
141
|
+
});
|
|
142
|
+
this.activeNestedToolByParent.delete(taskId);
|
|
143
|
+
}
|
|
144
|
+
const subTools = this.activeSubAgentToolsByTask.get(taskId);
|
|
145
|
+
if (subTools) {
|
|
146
|
+
for (const toolId of subTools) {
|
|
147
|
+
this.emit({ status: 'stream', result: null,
|
|
148
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: toolId, parentToolUseId: taskId },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
this.activeSubAgentToolsByTask.delete(taskId);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Process a stream_event message from the SDK.
|
|
157
|
+
* Returns true if the message was handled (caller should continue to next message).
|
|
158
|
+
*/
|
|
159
|
+
processStreamEvent(message: { type: string; parent_tool_use_id?: string | null; event: any; }): boolean {
|
|
160
|
+
const parentToolUseId =
|
|
161
|
+
message.parent_tool_use_id === undefined ? null : message.parent_tool_use_id;
|
|
162
|
+
const isNested = parentToolUseId !== null;
|
|
163
|
+
|
|
164
|
+
const event = message.event;
|
|
165
|
+
// Diagnostic log: print non-delta nested events
|
|
166
|
+
if (isNested && event.type !== 'content_block_delta') {
|
|
167
|
+
const evtType = event.type === 'content_block_start'
|
|
168
|
+
? `block_start/${event.content_block?.type}${event.content_block?.name ? `:${event.content_block.name}` : ''}`
|
|
169
|
+
: event.type;
|
|
170
|
+
this.log(`[stream-nested] parent=${parentToolUseId} evt=${evtType} tasks=[${[...this.taskToolUseIds].map(id => id.slice(0, 12)).join(',')}]`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (event.type === 'content_block_start') {
|
|
174
|
+
const _b = event.content_block;
|
|
175
|
+
this.log(`[stream] parent=${parentToolUseId ?? 'null'} block=${_b?.type}${_b?.name ? ` name=${_b.name}` : ''}${_b?.id ? ` id=${_b.id.slice(0, 12)}` : ''}`);
|
|
176
|
+
const block = event.content_block;
|
|
177
|
+
|
|
178
|
+
if (block?.type === 'tool_use') {
|
|
179
|
+
this.handleToolUseStart(block, parentToolUseId, isNested, event.index);
|
|
180
|
+
} else if (block?.type === 'text') {
|
|
181
|
+
this.handleTextBlockStart(parentToolUseId, isNested);
|
|
182
|
+
}
|
|
183
|
+
} else if (event.type === 'content_block_delta') {
|
|
184
|
+
this.handleContentBlockDelta(event, parentToolUseId);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Handle tool_use content_block_start. */
|
|
191
|
+
private handleToolUseStart(
|
|
192
|
+
block: { type: string; name: string; id?: string; input?: unknown },
|
|
193
|
+
parentToolUseId: string | null,
|
|
194
|
+
isNested: boolean,
|
|
195
|
+
blockIndex?: number,
|
|
196
|
+
): void {
|
|
197
|
+
// Determine if this is inside a Skill: SDK may not set parent_tool_use_id
|
|
198
|
+
const isInsideSkill = !isNested && this.activeSkillToolUseId && block.name !== 'Skill';
|
|
199
|
+
const effectiveIsNested = isNested || !!isInsideSkill;
|
|
200
|
+
const effectiveParentToolUseId = isInsideSkill ? this.activeSkillToolUseId : parentToolUseId;
|
|
201
|
+
|
|
202
|
+
if (!effectiveIsNested && this.activeTopLevelToolUseId && this.activeTopLevelToolUseId !== block.id) {
|
|
203
|
+
// Task tool_use_end only via tool_use_summary (not premature)
|
|
204
|
+
if (!this.taskToolUseIds.has(this.activeTopLevelToolUseId)) {
|
|
205
|
+
this.emit({
|
|
206
|
+
status: 'stream', result: null,
|
|
207
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: this.activeTopLevelToolUseId },
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (this.activeTopLevelToolUseId === this.activeSkillToolUseId) {
|
|
211
|
+
this.activeSkillToolUseId = null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!effectiveIsNested) this.activeTopLevelToolUseId = block.id || null;
|
|
215
|
+
|
|
216
|
+
// Track nested tools: end previous active tool under same parent
|
|
217
|
+
if (effectiveIsNested && effectiveParentToolUseId) {
|
|
218
|
+
const prevNested = this.activeNestedToolByParent.get(effectiveParentToolUseId);
|
|
219
|
+
if (prevNested && prevNested.toolUseId !== block.id) {
|
|
220
|
+
this.emit({
|
|
221
|
+
status: 'stream', result: null,
|
|
222
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: prevNested.toolUseId, parentToolUseId: effectiveParentToolUseId },
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
this.activeNestedToolByParent.set(effectiveParentToolUseId, { toolUseId: block.id || '', toolName: block.name });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.emit({
|
|
229
|
+
status: 'stream', result: null,
|
|
230
|
+
streamEvent: {
|
|
231
|
+
eventType: 'tool_use_start',
|
|
232
|
+
toolName: block.name,
|
|
233
|
+
toolUseId: block.id,
|
|
234
|
+
parentToolUseId: effectiveParentToolUseId,
|
|
235
|
+
isNested: effectiveIsNested,
|
|
236
|
+
skillName: extractSkillName(block.name, block.input),
|
|
237
|
+
toolInputSummary: summarizeToolInput(block.input),
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Track Skill tool_use block
|
|
242
|
+
if (block.name === 'Skill' && block.id) {
|
|
243
|
+
this.activeSkillToolUseId = block.id;
|
|
244
|
+
if (typeof blockIndex === 'number') {
|
|
245
|
+
this.pendingSkillInput.set(blockIndex, {
|
|
246
|
+
toolUseId: block.id, inputJson: '', resolved: false,
|
|
247
|
+
parentToolUseId, isNested,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Track AskUserQuestion tool
|
|
253
|
+
if (block.name === 'AskUserQuestion' && block.id) {
|
|
254
|
+
if (typeof blockIndex === 'number') {
|
|
255
|
+
this.pendingAskUserInput.set(blockIndex, {
|
|
256
|
+
toolUseId: block.id, inputJson: '', resolved: false,
|
|
257
|
+
parentToolUseId, isNested,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Track TodoWrite tool
|
|
263
|
+
if (block.name === 'TodoWrite' && block.id) {
|
|
264
|
+
if (typeof blockIndex === 'number') {
|
|
265
|
+
this.pendingTodoInput.set(blockIndex, {
|
|
266
|
+
toolUseId: block.id, inputJson: '', resolved: false,
|
|
267
|
+
parentToolUseId, isNested,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Track generic tools for input_json_delta → toolInputSummary
|
|
273
|
+
if (block.name && !SPECIAL_TOOLS.includes(block.name) && typeof blockIndex === 'number') {
|
|
274
|
+
this.pendingGenericInput.set(blockIndex, {
|
|
275
|
+
toolUseId: block.id || '', inputJson: '', resolved: false,
|
|
276
|
+
parentToolUseId: effectiveParentToolUseId, isNested: effectiveIsNested,
|
|
277
|
+
toolName: block.name,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Track Task / Agent tool (both spawn sub-agents whose messages need forwarding)
|
|
282
|
+
if ((block.name === 'Task' || block.name === 'Agent') && block.id) {
|
|
283
|
+
this.taskToolUseIds.add(block.id);
|
|
284
|
+
this.emit({
|
|
285
|
+
status: 'stream', result: null,
|
|
286
|
+
streamEvent: { eventType: 'task_start', toolUseId: block.id, toolName: block.name },
|
|
287
|
+
});
|
|
288
|
+
if (typeof blockIndex === 'number') {
|
|
289
|
+
this.pendingTaskInput.set(blockIndex, {
|
|
290
|
+
toolUseId: block.id, inputJson: '', resolved: false,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Handle text content_block_start. */
|
|
297
|
+
private handleTextBlockStart(parentToolUseId: string | null, isNested: boolean): void {
|
|
298
|
+
// New text block means top-level tool has finished executing (main agent only)
|
|
299
|
+
if (!isNested && this.activeTopLevelToolUseId) {
|
|
300
|
+
if (!this.taskToolUseIds.has(this.activeTopLevelToolUseId)) {
|
|
301
|
+
this.emit({
|
|
302
|
+
status: 'stream', result: null,
|
|
303
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: this.activeTopLevelToolUseId },
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
this.activeTopLevelToolUseId = null;
|
|
307
|
+
this.activeSkillToolUseId = null;
|
|
308
|
+
}
|
|
309
|
+
// Nested text block: end active nested tool under that parent
|
|
310
|
+
if (isNested && parentToolUseId) {
|
|
311
|
+
const prevNested = this.activeNestedToolByParent.get(parentToolUseId);
|
|
312
|
+
if (prevNested) {
|
|
313
|
+
this.emit({
|
|
314
|
+
status: 'stream', result: null,
|
|
315
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: prevNested.toolUseId, parentToolUseId },
|
|
316
|
+
});
|
|
317
|
+
this.activeNestedToolByParent.delete(parentToolUseId);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Handle content_block_delta events (text, thinking, input_json). */
|
|
323
|
+
private handleContentBlockDelta(event: any, parentToolUseId: string | null): void {
|
|
324
|
+
const delta = event.delta;
|
|
325
|
+
if (delta?.type === 'text_delta' && delta.text) {
|
|
326
|
+
const bufKey = parentToolUseId || this.BUF_MAIN;
|
|
327
|
+
this.getBuf(bufKey).text += delta.text;
|
|
328
|
+
if (bufKey === this.BUF_MAIN) this.fullTextAccumulator += delta.text;
|
|
329
|
+
this.scheduleFlush();
|
|
330
|
+
} else if (delta?.type === 'thinking_delta' && delta.thinking) {
|
|
331
|
+
const bufKey = parentToolUseId || this.BUF_MAIN;
|
|
332
|
+
this.getBuf(bufKey).think += delta.thinking;
|
|
333
|
+
this.scheduleFlush();
|
|
334
|
+
} else if (delta?.type === 'input_json_delta' && delta.partial_json) {
|
|
335
|
+
const blockIndex = event.index;
|
|
336
|
+
if (typeof blockIndex === 'number') {
|
|
337
|
+
this.handleInputJsonDelta(blockIndex, delta.partial_json);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Handle input_json_delta for Skill and Task tools. */
|
|
343
|
+
private handleInputJsonDelta(blockIndex: number, partialJson: string): void {
|
|
344
|
+
// Accumulate Skill input JSON
|
|
345
|
+
const pending = this.pendingSkillInput.get(blockIndex);
|
|
346
|
+
if (pending && !pending.resolved) {
|
|
347
|
+
pending.inputJson += partialJson;
|
|
348
|
+
const skillMatch = pending.inputJson.match(/"skill"\s*:\s*"([^"]+)"/);
|
|
349
|
+
if (skillMatch) {
|
|
350
|
+
pending.resolved = true;
|
|
351
|
+
this.pendingSkillInput.delete(blockIndex);
|
|
352
|
+
this.emit({
|
|
353
|
+
status: 'stream', result: null,
|
|
354
|
+
streamEvent: {
|
|
355
|
+
eventType: 'tool_progress',
|
|
356
|
+
toolName: 'Skill',
|
|
357
|
+
toolUseId: pending.toolUseId,
|
|
358
|
+
parentToolUseId: pending.parentToolUseId,
|
|
359
|
+
isNested: pending.isNested,
|
|
360
|
+
skillName: skillMatch[1],
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Accumulate AskUserQuestion input JSON
|
|
367
|
+
const pendingAsk = this.pendingAskUserInput.get(blockIndex);
|
|
368
|
+
if (pendingAsk && !pendingAsk.resolved) {
|
|
369
|
+
pendingAsk.inputJson += partialJson;
|
|
370
|
+
// Try to parse once we see "questions" field
|
|
371
|
+
if (pendingAsk.inputJson.includes('"question')) {
|
|
372
|
+
try {
|
|
373
|
+
const parsed = JSON.parse(pendingAsk.inputJson);
|
|
374
|
+
if (parsed.question || parsed.questions) {
|
|
375
|
+
pendingAsk.resolved = true;
|
|
376
|
+
this.pendingAskUserInput.delete(blockIndex);
|
|
377
|
+
this.emit({
|
|
378
|
+
status: 'stream', result: null,
|
|
379
|
+
streamEvent: {
|
|
380
|
+
eventType: 'tool_progress',
|
|
381
|
+
toolName: 'AskUserQuestion',
|
|
382
|
+
toolUseId: pendingAsk.toolUseId,
|
|
383
|
+
parentToolUseId: pendingAsk.parentToolUseId,
|
|
384
|
+
isNested: pendingAsk.isNested,
|
|
385
|
+
toolInput: parsed,
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
// JSON not complete yet, continue accumulating
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Accumulate TodoWrite input JSON
|
|
396
|
+
const pendingTodo = this.pendingTodoInput.get(blockIndex);
|
|
397
|
+
if (pendingTodo && !pendingTodo.resolved) {
|
|
398
|
+
pendingTodo.inputJson += partialJson;
|
|
399
|
+
if (pendingTodo.inputJson.includes('"todos"')) {
|
|
400
|
+
try {
|
|
401
|
+
const parsed = JSON.parse(pendingTodo.inputJson);
|
|
402
|
+
if (Array.isArray(parsed.todos)) {
|
|
403
|
+
pendingTodo.resolved = true;
|
|
404
|
+
this.pendingTodoInput.delete(blockIndex);
|
|
405
|
+
this.emit({
|
|
406
|
+
status: 'stream', result: null,
|
|
407
|
+
streamEvent: {
|
|
408
|
+
eventType: 'todo_update',
|
|
409
|
+
todos: parsed.todos,
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
} catch {
|
|
414
|
+
// JSON not complete yet, continue accumulating
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Accumulate Task input JSON
|
|
420
|
+
const pendingTask = this.pendingTaskInput.get(blockIndex);
|
|
421
|
+
if (pendingTask && !pendingTask.resolved) {
|
|
422
|
+
pendingTask.inputJson += partialJson;
|
|
423
|
+
// Detect team_name
|
|
424
|
+
if (!pendingTask.isTeammate) {
|
|
425
|
+
const teamMatch = pendingTask.inputJson.match(/"team_name"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
426
|
+
if (teamMatch) {
|
|
427
|
+
pendingTask.isTeammate = true;
|
|
428
|
+
this.teammateTaskToolUseIds.add(pendingTask.toolUseId);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const descMatch = pendingTask.inputJson.match(/"description"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
432
|
+
if (descMatch) {
|
|
433
|
+
pendingTask.resolved = true;
|
|
434
|
+
this.pendingTaskInput.delete(blockIndex);
|
|
435
|
+
const isTeammate = pendingTask.isTeammate || false;
|
|
436
|
+
if (isTeammate) this.teammateTaskToolUseIds.add(pendingTask.toolUseId);
|
|
437
|
+
this.emit({
|
|
438
|
+
status: 'stream', result: null,
|
|
439
|
+
streamEvent: {
|
|
440
|
+
eventType: 'task_start',
|
|
441
|
+
toolUseId: pendingTask.toolUseId,
|
|
442
|
+
toolName: 'Task',
|
|
443
|
+
taskDescription: descMatch[1].replace(/\\"/g, '"').slice(0, 200),
|
|
444
|
+
...(isTeammate ? { isTeammate: true } : {}),
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Accumulate generic tool input JSON for toolInputSummary.
|
|
451
|
+
// Only attempt JSON.parse when the accumulated string looks complete (ends with '}')
|
|
452
|
+
// to avoid O(n^2) repeated parse failures on large tool inputs.
|
|
453
|
+
// Cap at 10KB to avoid unbounded memory growth on tools with large inputs (Write, Edit).
|
|
454
|
+
const GENERIC_INPUT_MAX = 10_240;
|
|
455
|
+
const pendingGeneric = this.pendingGenericInput.get(blockIndex);
|
|
456
|
+
if (pendingGeneric && !pendingGeneric.resolved) {
|
|
457
|
+
if (pendingGeneric.inputJson.length >= GENERIC_INPUT_MAX) {
|
|
458
|
+
pendingGeneric.resolved = true;
|
|
459
|
+
this.pendingGenericInput.delete(blockIndex);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
pendingGeneric.inputJson += partialJson;
|
|
463
|
+
const trimmed = pendingGeneric.inputJson.trimEnd();
|
|
464
|
+
const summary = trimmed.endsWith('}') ? summarizeToolInput((() => {
|
|
465
|
+
try { return JSON.parse(pendingGeneric.inputJson); } catch { return null; }
|
|
466
|
+
})()) : undefined;
|
|
467
|
+
if (summary) {
|
|
468
|
+
pendingGeneric.resolved = true;
|
|
469
|
+
this.pendingGenericInput.delete(blockIndex);
|
|
470
|
+
this.emit({
|
|
471
|
+
status: 'stream', result: null,
|
|
472
|
+
streamEvent: {
|
|
473
|
+
eventType: 'tool_progress',
|
|
474
|
+
toolName: pendingGeneric.toolName,
|
|
475
|
+
toolUseId: pendingGeneric.toolUseId,
|
|
476
|
+
parentToolUseId: pendingGeneric.parentToolUseId,
|
|
477
|
+
isNested: pendingGeneric.isNested,
|
|
478
|
+
toolInputSummary: summary,
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Process a tool_progress message.
|
|
487
|
+
*/
|
|
488
|
+
processToolProgress(message: any): void {
|
|
489
|
+
const parentToolUseId =
|
|
490
|
+
message.parent_tool_use_id === undefined ? null : message.parent_tool_use_id;
|
|
491
|
+
this.emit({
|
|
492
|
+
status: 'stream', result: null,
|
|
493
|
+
streamEvent: {
|
|
494
|
+
eventType: 'tool_progress',
|
|
495
|
+
toolName: message.tool_name,
|
|
496
|
+
toolUseId: message.tool_use_id,
|
|
497
|
+
parentToolUseId,
|
|
498
|
+
isNested: parentToolUseId !== null,
|
|
499
|
+
elapsedSeconds: message.elapsed_time_seconds,
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Process a tool_use_summary message.
|
|
506
|
+
*/
|
|
507
|
+
processToolUseSummary(message: any): void {
|
|
508
|
+
const ids = Array.isArray(message.preceding_tool_use_ids)
|
|
509
|
+
? message.preceding_tool_use_ids.filter((id: unknown): id is string => typeof id === 'string')
|
|
510
|
+
: [];
|
|
511
|
+
this.log(`[tool_use_summary] ids=[${ids.map((id: string) => id.slice(0, 12)).join(',')}] taskToolUseIds=[${[...this.taskToolUseIds].map(id => id.slice(0, 12)).join(',')}] bgTasks=[${[...this.backgroundTaskToolUseIds].map(id => id.slice(0, 12)).join(',')}]`);
|
|
512
|
+
for (const id of ids) {
|
|
513
|
+
// Foreground Task completion: synthesize task_notification
|
|
514
|
+
if (this.taskToolUseIds.has(id) && !this.backgroundTaskToolUseIds.has(id)) {
|
|
515
|
+
this.log(`Synthesizing task_notification for foreground Task ${id.slice(0, 12)}`);
|
|
516
|
+
this.cleanupTaskTools(id);
|
|
517
|
+
this.emit({
|
|
518
|
+
status: 'stream', result: null,
|
|
519
|
+
streamEvent: {
|
|
520
|
+
eventType: 'task_notification',
|
|
521
|
+
taskId: id,
|
|
522
|
+
taskStatus: 'completed',
|
|
523
|
+
taskSummary: '',
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
this.taskToolUseIds.delete(id);
|
|
528
|
+
this.backgroundTaskToolUseIds.delete(id);
|
|
529
|
+
this.emit({
|
|
530
|
+
status: 'stream', result: null,
|
|
531
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: id },
|
|
532
|
+
});
|
|
533
|
+
if (this.activeTopLevelToolUseId === id) {
|
|
534
|
+
this.activeTopLevelToolUseId = null;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Process system messages (status, hook_started, hook_progress, hook_response).
|
|
541
|
+
* Returns true if the message was handled.
|
|
542
|
+
*/
|
|
543
|
+
processSystemMessage(message: any): boolean {
|
|
544
|
+
if (message.subtype === 'status') {
|
|
545
|
+
const statusText = message.status?.type || null;
|
|
546
|
+
this.emit({ status: 'stream', result: null, streamEvent: { eventType: 'status', statusText } });
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
if (message.subtype === 'hook_started') {
|
|
550
|
+
this.emit({
|
|
551
|
+
status: 'stream', result: null,
|
|
552
|
+
streamEvent: { eventType: 'hook_started', hookName: message.hook_name, hookEvent: message.hook_event },
|
|
553
|
+
});
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
if (message.subtype === 'hook_progress') {
|
|
557
|
+
this.emit({
|
|
558
|
+
status: 'stream', result: null,
|
|
559
|
+
streamEvent: { eventType: 'hook_progress', hookName: message.hook_name, hookEvent: message.hook_event },
|
|
560
|
+
});
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
if (message.subtype === 'hook_response') {
|
|
564
|
+
this.emit({
|
|
565
|
+
status: 'stream', result: null,
|
|
566
|
+
streamEvent: { eventType: 'hook_response', hookName: message.hook_name, hookEvent: message.hook_event, hookOutcome: message.outcome },
|
|
567
|
+
});
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
// API retry — emit status so user sees retry progress and activity stays alive
|
|
571
|
+
if (message.subtype === 'api_retry') {
|
|
572
|
+
const attempt = message.attempt ?? '?';
|
|
573
|
+
const max = message.max_retries ?? '?';
|
|
574
|
+
const delayMs = message.retry_delay_ms ?? 0;
|
|
575
|
+
const delaySec = Math.round(delayMs / 1000);
|
|
576
|
+
this.emit({
|
|
577
|
+
status: 'stream', result: null,
|
|
578
|
+
streamEvent: { eventType: 'status', statusText: `API 重试中 (${attempt}/${max}),${delaySec}s 后重试` },
|
|
579
|
+
});
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
// task_started / task_progress — emit a status event to keep stdout activity alive.
|
|
583
|
+
// Without this, long-running tasks produce no stdout output, and the host's
|
|
584
|
+
// stuck-runner detector may kill the process after 6 minutes of silence.
|
|
585
|
+
// Also build sdkTaskId → toolUseId mapping for task_notification translation.
|
|
586
|
+
if (message.subtype === 'task_started' || message.subtype === 'task_progress') {
|
|
587
|
+
if (message.task_id && message.tool_use_id) {
|
|
588
|
+
this.sdkTaskIdToToolUseId.set(message.task_id, message.tool_use_id);
|
|
589
|
+
}
|
|
590
|
+
const desc = message.description || message.summary || '';
|
|
591
|
+
const toolName = message.last_tool_name || '';
|
|
592
|
+
const statusText = message.subtype === 'task_started'
|
|
593
|
+
? `Task 启动: ${desc.slice(0, 80)}`
|
|
594
|
+
: `Task 进度${toolName ? ` [${toolName}]` : ''}: ${desc.slice(0, 80)}`;
|
|
595
|
+
this.emit({
|
|
596
|
+
status: 'stream', result: null,
|
|
597
|
+
streamEvent: { eventType: 'status', statusText },
|
|
598
|
+
});
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Convenience: emit a status StreamEvent.
|
|
606
|
+
*/
|
|
607
|
+
emitStatus(statusText: string): void {
|
|
608
|
+
this.emit({ status: 'stream', result: null, streamEvent: { eventType: 'status', statusText } });
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Process sub-agent messages (assistant/user with parent_tool_use_id that matches a Task).
|
|
613
|
+
* Returns true if the message was handled as a sub-agent message.
|
|
614
|
+
*/
|
|
615
|
+
processSubAgentMessage(message: any): boolean {
|
|
616
|
+
const msgParentToolUseId = message.parent_tool_use_id ?? null;
|
|
617
|
+
if (!msgParentToolUseId || !this.taskToolUseIds.has(msgParentToolUseId)) {
|
|
618
|
+
if (msgParentToolUseId && (message.type === 'assistant' || message.type === 'user')) {
|
|
619
|
+
this.log(`[WARN] Sub-agent message dropped: parent=${msgParentToolUseId.slice(0, 12)} not in taskToolUseIds=[${[...this.taskToolUseIds].map(id => id.slice(0, 12)).join(',')}]`);
|
|
620
|
+
}
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (message.type === 'assistant') {
|
|
625
|
+
const subContent = message.message?.content as Array<{
|
|
626
|
+
type: string; text?: string; thinking?: string;
|
|
627
|
+
name?: string; id?: string; input?: Record<string, unknown>;
|
|
628
|
+
}> | undefined;
|
|
629
|
+
if (Array.isArray(subContent)) {
|
|
630
|
+
// End previous sub-agent active tools
|
|
631
|
+
const prevTools = this.activeSubAgentToolsByTask.get(msgParentToolUseId);
|
|
632
|
+
if (prevTools && prevTools.size > 0) {
|
|
633
|
+
for (const toolId of prevTools) {
|
|
634
|
+
this.emit({ status: 'stream', result: null,
|
|
635
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: toolId, parentToolUseId: msgParentToolUseId },
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
prevTools.clear();
|
|
639
|
+
}
|
|
640
|
+
for (const block of subContent) {
|
|
641
|
+
if (block.type === 'thinking' && block.thinking) {
|
|
642
|
+
this.emit({ status: 'stream', result: null,
|
|
643
|
+
streamEvent: { eventType: 'thinking_delta', text: block.thinking, parentToolUseId: msgParentToolUseId },
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
if (block.type === 'text' && block.text) {
|
|
647
|
+
this.emit({ status: 'stream', result: null,
|
|
648
|
+
streamEvent: { eventType: 'text_delta', text: block.text, parentToolUseId: msgParentToolUseId },
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
if (block.type === 'tool_use' && block.id) {
|
|
652
|
+
this.emit({ status: 'stream', result: null,
|
|
653
|
+
streamEvent: {
|
|
654
|
+
eventType: 'tool_use_start',
|
|
655
|
+
toolName: block.name || 'unknown',
|
|
656
|
+
toolUseId: block.id,
|
|
657
|
+
parentToolUseId: msgParentToolUseId,
|
|
658
|
+
isNested: true,
|
|
659
|
+
toolInputSummary: summarizeToolInput(block.input),
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
if (!this.activeSubAgentToolsByTask.has(msgParentToolUseId)) {
|
|
663
|
+
this.activeSubAgentToolsByTask.set(msgParentToolUseId, new Set());
|
|
664
|
+
}
|
|
665
|
+
this.activeSubAgentToolsByTask.get(msgParentToolUseId)!.add(block.id);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
this.log(`[sub-agent] parent=${msgParentToolUseId.slice(0, 12)} blocks=${subContent.length} types=[${subContent.map(b => b.type).join(',')}]`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (message.type === 'user') {
|
|
673
|
+
const rawContent = message.message?.content;
|
|
674
|
+
if (typeof rawContent === 'string' && rawContent) {
|
|
675
|
+
this.emit({ status: 'stream', result: null,
|
|
676
|
+
streamEvent: { eventType: 'text_delta', text: rawContent, parentToolUseId: msgParentToolUseId },
|
|
677
|
+
});
|
|
678
|
+
} else if (Array.isArray(rawContent)) {
|
|
679
|
+
const activeSub = this.activeSubAgentToolsByTask.get(msgParentToolUseId);
|
|
680
|
+
for (const block of rawContent as Array<{ type: string; text?: string; thinking?: string; tool_use_id?: string }>) {
|
|
681
|
+
if (block.type === 'text' && block.text) {
|
|
682
|
+
this.emit({ status: 'stream', result: null,
|
|
683
|
+
streamEvent: { eventType: 'text_delta', text: block.text, parentToolUseId: msgParentToolUseId },
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
if (block.type === 'thinking' && block.thinking) {
|
|
687
|
+
this.emit({ status: 'stream', result: null,
|
|
688
|
+
streamEvent: { eventType: 'thinking_delta', text: block.thinking, parentToolUseId: msgParentToolUseId },
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
692
|
+
this.emit({ status: 'stream', result: null,
|
|
693
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: block.tool_use_id, parentToolUseId: msgParentToolUseId },
|
|
694
|
+
});
|
|
695
|
+
activeSub?.delete(block.tool_use_id);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Check if a tool_use was already resolved by the streaming accumulator. */
|
|
705
|
+
private isPendingResolved(
|
|
706
|
+
pendingMap: Map<number, { toolUseId: string; resolved: boolean }>,
|
|
707
|
+
toolUseId: string,
|
|
708
|
+
): boolean {
|
|
709
|
+
for (const pending of pendingMap.values()) {
|
|
710
|
+
if (pending.toolUseId === toolUseId && pending.resolved) return true;
|
|
711
|
+
}
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Process an assistant message for Skill/Task fallback extraction and pending tracker cleanup.
|
|
717
|
+
*/
|
|
718
|
+
processAssistantMessage(message: any): void {
|
|
719
|
+
const content = message.message?.content;
|
|
720
|
+
if (!Array.isArray(content)) return;
|
|
721
|
+
|
|
722
|
+
// Fallback: extract skill name from complete assistant message
|
|
723
|
+
for (const block of content) {
|
|
724
|
+
if (block.type === 'tool_use' && block.name === 'Skill' && block.id && block.input) {
|
|
725
|
+
const skillName = extractSkillName(block.name, block.input);
|
|
726
|
+
if (skillName && !this.isPendingResolved(this.pendingSkillInput, block.id)) {
|
|
727
|
+
this.emit({
|
|
728
|
+
status: 'stream', result: null,
|
|
729
|
+
streamEvent: { eventType: 'tool_progress', toolName: 'Skill', toolUseId: block.id, skillName },
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Fallback: identify background Tasks and Teammate Tasks from complete input
|
|
736
|
+
for (const block of content) {
|
|
737
|
+
if (block.type === 'tool_use' && (block.name === 'Task' || block.name === 'Agent') && block.id && block.input) {
|
|
738
|
+
const taskInput = block.input as Record<string, unknown>;
|
|
739
|
+
if (taskInput.run_in_background === true) {
|
|
740
|
+
this.backgroundTaskToolUseIds.add(block.id);
|
|
741
|
+
this.log(`Task ${block.id.slice(0, 12)} marked as background`);
|
|
742
|
+
}
|
|
743
|
+
if (taskInput.team_name && !this.teammateTaskToolUseIds.has(block.id)) {
|
|
744
|
+
this.teammateTaskToolUseIds.add(block.id);
|
|
745
|
+
this.log(`Task ${block.id.slice(0, 12)} marked as teammate (team=${taskInput.team_name})`);
|
|
746
|
+
this.emit({
|
|
747
|
+
status: 'stream', result: null,
|
|
748
|
+
streamEvent: { eventType: 'task_start', toolUseId: block.id, toolName: 'Task', isTeammate: true },
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Fallback: extract AskUserQuestion input from complete assistant message
|
|
755
|
+
for (const block of content) {
|
|
756
|
+
if (block.type === 'tool_use' && block.name === 'AskUserQuestion' && block.id && block.input) {
|
|
757
|
+
if (!this.isPendingResolved(this.pendingAskUserInput, block.id)) {
|
|
758
|
+
this.emit({
|
|
759
|
+
status: 'stream', result: null,
|
|
760
|
+
streamEvent: {
|
|
761
|
+
eventType: 'tool_progress',
|
|
762
|
+
toolName: 'AskUserQuestion',
|
|
763
|
+
toolUseId: block.id,
|
|
764
|
+
toolInput: block.input as Record<string, unknown>,
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Fallback: extract TodoWrite todos from complete assistant message
|
|
772
|
+
for (const block of content) {
|
|
773
|
+
if (block.type === 'tool_use' && block.name === 'TodoWrite' && block.id && block.input) {
|
|
774
|
+
if (!this.isPendingResolved(this.pendingTodoInput, block.id)) {
|
|
775
|
+
const todoInput = block.input as Record<string, unknown>;
|
|
776
|
+
if (Array.isArray(todoInput.todos)) {
|
|
777
|
+
this.emit({
|
|
778
|
+
status: 'stream', result: null,
|
|
779
|
+
streamEvent: {
|
|
780
|
+
eventType: 'todo_update',
|
|
781
|
+
todos: todoInput.todos as Array<{ id: string; content: string; status: 'pending' | 'in_progress' | 'completed' }>,
|
|
782
|
+
},
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Clear pending trackers to avoid memory leaks
|
|
790
|
+
this.pendingSkillInput.clear();
|
|
791
|
+
this.pendingTaskInput.clear();
|
|
792
|
+
this.pendingAskUserInput.clear();
|
|
793
|
+
this.pendingTodoInput.clear();
|
|
794
|
+
this.pendingGenericInput.clear();
|
|
795
|
+
this.sdkTaskIdToToolUseId.clear();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Process a task_notification system message.
|
|
800
|
+
* The SDK's task_id differs from the API's tool_use_id used at task creation.
|
|
801
|
+
* We resolve the effective toolUseId via: message.tool_use_id → sdkTaskId map → raw task_id.
|
|
802
|
+
*/
|
|
803
|
+
processTaskNotification(message: { task_id: string; tool_use_id?: string; status: string; summary: string }): void {
|
|
804
|
+
const effectiveToolUseId = message.tool_use_id
|
|
805
|
+
|| this.sdkTaskIdToToolUseId.get(message.task_id)
|
|
806
|
+
|| message.task_id;
|
|
807
|
+
if (effectiveToolUseId !== message.task_id) {
|
|
808
|
+
this.log(`Task notification: sdkTaskId=${message.task_id} → toolUseId=${effectiveToolUseId} status=${message.status}`);
|
|
809
|
+
} else {
|
|
810
|
+
this.log(`Task notification: task=${message.task_id} status=${message.status} summary=${message.summary}`);
|
|
811
|
+
}
|
|
812
|
+
this.emit({
|
|
813
|
+
status: 'stream', result: null,
|
|
814
|
+
streamEvent: {
|
|
815
|
+
eventType: 'task_notification',
|
|
816
|
+
taskId: effectiveToolUseId,
|
|
817
|
+
taskStatus: message.status,
|
|
818
|
+
taskSummary: message.summary,
|
|
819
|
+
isBackground: true,
|
|
820
|
+
},
|
|
821
|
+
});
|
|
822
|
+
this.cleanupTaskTools(effectiveToolUseId);
|
|
823
|
+
this.backgroundTaskToolUseIds.delete(effectiveToolUseId);
|
|
824
|
+
if (this.taskToolUseIds.has(effectiveToolUseId)) {
|
|
825
|
+
this.taskToolUseIds.delete(effectiveToolUseId);
|
|
826
|
+
this.emit({
|
|
827
|
+
status: 'stream', result: null,
|
|
828
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: effectiveToolUseId },
|
|
829
|
+
});
|
|
830
|
+
if (this.activeTopLevelToolUseId === effectiveToolUseId) {
|
|
831
|
+
this.activeTopLevelToolUseId = null;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
// Clean up the mapping entry
|
|
835
|
+
this.sdkTaskIdToToolUseId.delete(message.task_id);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Process a result message. Handles flushing and returns the effective result text.
|
|
840
|
+
* Returns null if there's no textual result.
|
|
841
|
+
*/
|
|
842
|
+
processResult(textResult: string | null | undefined): { effectiveResult: string | null; seenTextual: boolean } {
|
|
843
|
+
if (textResult) {
|
|
844
|
+
if (this.flushTimer) { clearTimeout(this.flushTimer); this.flushTimer = null; }
|
|
845
|
+
this.flushBuffers();
|
|
846
|
+
this.seenTextualResult = true;
|
|
847
|
+
}
|
|
848
|
+
// Use fullTextAccumulator if it's more complete than SDK's result
|
|
849
|
+
const effectiveResult = this.fullTextAccumulator.length > (textResult?.length || 0)
|
|
850
|
+
? this.fullTextAccumulator
|
|
851
|
+
: (textResult || null);
|
|
852
|
+
// Reset accumulator for next query loop
|
|
853
|
+
this.fullTextAccumulator = '';
|
|
854
|
+
return { effectiveResult, seenTextual: !!textResult };
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/** Reset the full text accumulator (e.g., on context overflow). */
|
|
858
|
+
resetFullTextAccumulator(): void {
|
|
859
|
+
this.fullTextAccumulator = '';
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Cleanup all residual state after the query loop ends.
|
|
864
|
+
* Must be called after the for-await loop completes or on error.
|
|
865
|
+
*/
|
|
866
|
+
cleanup(): void {
|
|
867
|
+
// Cancel pending timer, then flush or clear remaining buffers
|
|
868
|
+
if (this.flushTimer) { clearTimeout(this.flushTimer); this.flushTimer = null; }
|
|
869
|
+
if (this.seenTextualResult) {
|
|
870
|
+
// Textual result already emitted. Drop buffered tail to avoid stale residue.
|
|
871
|
+
this.streamBufs.clear();
|
|
872
|
+
} else {
|
|
873
|
+
this.flushBuffers();
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Emit tool_use_end for active top-level tool (except Task tools)
|
|
877
|
+
if (this.activeTopLevelToolUseId) {
|
|
878
|
+
if (!this.taskToolUseIds.has(this.activeTopLevelToolUseId)) {
|
|
879
|
+
this.emit({
|
|
880
|
+
status: 'stream', result: null,
|
|
881
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: this.activeTopLevelToolUseId },
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
this.activeTopLevelToolUseId = null;
|
|
885
|
+
this.activeSkillToolUseId = null;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Safety net: emit completion signals for pending Task tools
|
|
889
|
+
if (this.taskToolUseIds.size > 0) {
|
|
890
|
+
this.log(`[safety-net] ${this.taskToolUseIds.size} Task tools still pending: [${[...this.taskToolUseIds].map(id => id.slice(0, 12)).join(',')}]`);
|
|
891
|
+
}
|
|
892
|
+
for (const id of this.taskToolUseIds) {
|
|
893
|
+
if (!this.backgroundTaskToolUseIds.has(id)) {
|
|
894
|
+
this.log(`[safety-net] Synthesizing task_notification for Task ${id.slice(0, 12)}`);
|
|
895
|
+
this.cleanupTaskTools(id);
|
|
896
|
+
this.emit({
|
|
897
|
+
status: 'stream', result: null,
|
|
898
|
+
streamEvent: { eventType: 'task_notification', taskId: id, taskStatus: 'completed', taskSummary: '' },
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
this.emit({
|
|
902
|
+
status: 'stream', result: null,
|
|
903
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: id },
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
this.taskToolUseIds.clear();
|
|
907
|
+
|
|
908
|
+
// Clean up residual nested tool tracking
|
|
909
|
+
for (const [parentId, nested] of this.activeNestedToolByParent) {
|
|
910
|
+
this.emit({
|
|
911
|
+
status: 'stream', result: null,
|
|
912
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: nested.toolUseId, parentToolUseId: parentId },
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
this.activeNestedToolByParent.clear();
|
|
916
|
+
|
|
917
|
+
// Clean up residual sub-agent active tools
|
|
918
|
+
for (const [taskId, subTools] of this.activeSubAgentToolsByTask) {
|
|
919
|
+
for (const toolId of subTools) {
|
|
920
|
+
this.emit({ status: 'stream', result: null,
|
|
921
|
+
streamEvent: { eventType: 'tool_use_end', toolUseId: toolId, parentToolUseId: taskId },
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
this.activeSubAgentToolsByTask.clear();
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** Get the accumulated full text (for result comparison). */
|
|
929
|
+
getFullText(): string {
|
|
930
|
+
return this.fullTextAccumulator;
|
|
931
|
+
}
|
|
932
|
+
}
|