claude-code-advisor
Advanced tools
+383
-764
@@ -1,822 +0,441 @@ | ||
| /// Source Analysis /// 2026-04-04 /// | ||
| # 00. Feature Discovery | ||
| # Claude Code | ||
| 隐藏功能全解析 | ||
| > Source analysis based on 1,902 `src/` files, 512,685 lines of source code. | ||
| 基于当前 snapshot 的 1,902 个 `src/` 文件、512,685 行源码做的功能发现与证据归档 | ||
| ## Stats | ||
| 248 | ||
| | Category | Count | | ||
| |---|---| | ||
| | Feature Flags (build-time) | 87 | | ||
| | GrowthBook Runtime Flags | 59 | | ||
| | Agent Tools | 57 | | ||
| | Slash Commands | 103+ | | ||
| | Hidden Systems | 11 | | ||
| Feature Flags | ||
| ## Key Evidence Files | ||
| 57 | ||
| | System | Source Files | | ||
| |---|---| | ||
| | Buddy | `src/buddy/companion.ts`, `types.ts`, `sprites.ts` | | ||
| | Kairos / Proactive | `src/commands.ts`, `src/utils/systemPrompt.ts`, `src/hooks/useReplBridge.tsx` | | ||
| | Teleport / UltraPlan | `src/utils/teleport.tsx`, `src/utils/ultraplan/ccrSession.ts` | | ||
| | UltraReview | `src/commands/review.ts`, `src/commands/review/reviewRemote.ts` | | ||
| | Bridge / Remote | `src/bridge/`, `src/remote/`, `src/server/` | | ||
| Agent Tools | ||
| --- | ||
| 103+ | ||
| ## Major Hidden Systems | ||
| Slash Commands | ||
| ### Buddy — AI Pet System | ||
| **Gate:** `feature('BUDDY')` | ||
| 11 | ||
| Deterministic pet generation from `userId` hash (Mulberry32 PRNG). **18 species**: duck, goose, blob, cat, dragon, octopus, owl, penguin, turtle, snail, ghost, axolotl, capybara, cactus, robot, rabbit, mushroom, chonk. **8 hats**: crown, tophat, propeller, halo, wizard, beanie, tinyduck, none. **6 eye types**. **5 stats**: DEBUGGING / PATIENCE / CHAOS / WISDOM / SNARK. | ||
| Hidden Systems | ||
| Terminal corner ASCII sprite animation (3 frames, 5 rows × 12 chars). Bubble comments on your actions. Rarity: Common(60%) → Legendary(1%), plus 1% shiny variant. Teaser window: 2026-04-01 ~ 04-07. | ||
| 59 | ||
| --- | ||
| GrowthBook Gates | ||
| ### Kairos — Proactive Assistant Mode | ||
| **Gate:** `feature('KAIROS')` | ||
| Hidden Systems More Features Build Flags GrowthBook Env Vars All Tools All Commands [← Back to Learning Index](index.html) | ||
| Claude as a 24/7 proactive assistant. Calendar integration (`SleepTool` scheduled wake), channel notifications (`KAIROS_CHANNELS` + MCP), GitHub webhook integration (`KAIROS_GITHUB_WEBHOOKS` + `SubscribePRTool`), push notifications (`PushNotificationTool`), file delivery (`SendUserFileTool`), `/brief` report, and **Auto-Dream** memory consolidation. 9 sub-flags. Largest feature cluster in Claude Code. | ||
| Method & Confidence | ||
| --- | ||
| 🧭这页是什么 | ||
| ### Auto-Dream — Background Memory Consolidation | ||
| **Gate:** `tengu_onyx_plover` | ||
| 这页是 **feature discovery ledger** ,目标是把隐藏系统、flag、工具和命令入口先扫出来,再跳转到后续章节深读。它不是每个系统的最终证明文档;像 Memory、MCP、Agent、Skill、Bridge、LSP 这些,都在后面的分章节里有更细的源码链路。 | ||
| Background "dreaming" process that auto-consolidates stored memories to prevent entropy. Two phases: `starting → updating`. Tracks file changes and history. Trigger conditions: ≥24 hours since last consolidation **and** ≥5 sessions touched. File-level distributed lock prevents concurrency. 4-phase pipeline: orient → gather → consolidate → prune. Max 30 turns. Shows "dreaming" status pill. | ||
| 📌关键证据锚点 | ||
| --- | ||
| **Buddy** :`src/buddy/companion.ts` \+ `types.ts` \+ `CompanionSprite.tsx` | ||
| **Kairos / Proactive** :`src/commands.ts` \+ `src/utils/systemPrompt.ts` \+ `src/hooks/useReplBridge.tsx` | ||
| **Teleport / UltraPlan** :`src/utils/teleport.tsx` \+ `src/utils/ultraplan/ccrSession.ts` | ||
| **UltraReview** :`src/commands/review.ts` \+ `src/commands/review/reviewRemote.ts` | ||
| **Bridge / Remote** :`src/bridge/` \+ `src/remote/` \+ `src/server/` | ||
| ### Teleport — Cross-Machine Session Transfer | ||
| **Gate:** `teleportToRemote()` | ||
| Major Hidden Systems | ||
| Transfers an entire work session from one machine to another. All context, Git branches, and records packed via **Git Bundle** seed filesystem. Flow: `validating → fetching_logs → fetching_branch → checking_out → done`. 4 retries + exponential backoff (2s/4s/8s/16s). Haiku auto-generates branch names and session titles. OAuth token forwarding. | ||
| 🐣Buddy,你的 AI 宠物 | ||
| --- | ||
| feature('BUDDY') | ||
| ### UltraPlan — 30-Minute Cloud Planning | ||
| **Gate:** `feature('ULTRAPLAN')` | ||
| 完整虚拟宠物系统。基于 **userId 哈希** 确定性生成(Mulberry32 PRNG),无法选择。**18 种物种** (duck, goose, blob, cat, dragon, octopus, owl, penguin, turtle, snail, ghost, axolotl, capybara, cactus, robot, rabbit, mushroom, chonk),**8 种帽子** (crown, tophat, propeller, halo, wizard, beanie, tinyduck, none),**6 种眼睛** ,**5 项属性** (DEBUGGING / PATIENCE / CHAOS / WISDOM / SNARK)。终端角落 **ASCII 精灵动画** (3 帧,5行×12字符),气泡评论你的操作。稀有度从 Common(60%) 到 **Legendary(1%)** ,还有 1% 闪光版。Teaser window: 2026-04-01 ~ 04-07。 | ||
| Triggered by `/ultraplan` (rainbow input highlight). Cloud CCR uses **Opus 4.6** for up to 30 minutes of deep planning. Result can be teleported back for local execution. State machine: `launching → sessionUrl → phase(needs_input/running) → pendingChoice`. Can execute directly in cloud (result lands as PR). Model overrideable via GrowthBook `tengu_ultraplan_model`. | ||
| 🌟Kairos,主动助手模式 | ||
| --- | ||
| feature('KAIROS') | ||
| ### UltraReview — Multi-Agent Code Review | ||
| **Gate:** `/ultrareview` | ||
| Claude 变成 **24 小时在线的主动助手** 。日程集成(`SleepTool` 定时唤醒),Channel 通知(`KAIROS_CHANNELS` \+ MCP),GitHub Webhook 联动(`KAIROS_GITHUB_WEBHOOKS` \+ `SubscribePRTool`),推送通知(`PushNotificationTool`),文件发送(`SendUserFileTool`),还有专属 `/brief` 简报和 **Auto-Dream 记忆整理** 。子 flags 9 个,是 Claude Code 最大的 feature cluster。 | ||
| Triggered by `/ultrareview`. Cloud-based **parallel multi-agent code review**, each agent examining code from a different angle. Requires GitHub repo + OAuth. Billing model: free quota then Extra Usage ($10 minimum), with confirmation dialog and overage disclaimer. Results returned as inline code comments. | ||
| 💤Auto-Dream,做梦整理记忆 | ||
| --- | ||
| tengu_onyx_plover | ||
| ### Agent Swarms — Persistent Agent Teams | ||
| **Gate:** `ENABLE_AGENT_SWARMS` | ||
| 你不用的时候它在后台启动 **"做梦"进程** ,自动整理你存下的所有记忆避免熵增。有 **starting → updating** 两阶段,跟踪文件变更和历史。触发条件:距上次整理 **≥24 小时** 且 **≥5 个 session** 被触碰。文件级分布式锁防并发。4 阶段流程:orient → gather → consolidate → prune。最多 30 轮,有 "dreaming" 状态 pill 显示。 | ||
| Creates **persistent agent teams** with inter-member communication. Team Lead in main terminal, Teammates in **tmux panes** (TmuxBackend), in-process (SpawnInProcess), or iTerm2 (ITermBackend). UDS mailbox communication. Color-coded member identification. Distributed permission sync (`permissionSync`), reconnect on disconnect, layout management. Tools: `TeamCreateTool` / `TeamDeleteTool` / `SendMessageTool`. | ||
| 🌀Teleport,跨机器传送门 | ||
| --- | ||
| teleportToRemote() | ||
| ### Away Summary — Background Activity Recap | ||
| **Gate:** `feature('AWAY_SUMMARY')` | ||
| 把整个工作会话 **从一台电脑传送到另一台** 。所有上下文、Git 分支、记录全部打包。用 **Git Bundle** 做 seed filesystem,公司干到一半,回家继续。流程:validating → fetching_logs → fetching_branch → checking_out → done。4 次重试 + 指数退避(2s/4s/8s/16s)。Haiku 自动生成分支名和 session 标题。OAuth token 自动转发。 | ||
| Auto-works while away, **generates "what I did" summary on return**. Uses Haiku for fast 1-3 sentence summary. Context: last 30 messages + session memory. Focuses on what's being built, not implementation details. Includes **concrete next step**. Skips status reports and commit recaps. Trigger: `IdleReturnDialog`. Non-blocking, silent error degradation. | ||
| 🧠UltraPlan,30 分钟云端规划 | ||
| --- | ||
| feature('ULTRAPLAN') | ||
| ## Additional Features | ||
| 输入 `/ultraplan` 触发(输入框彩虹高亮),云端 CCR 用 **Opus 4.6** 花最多 30 分钟深度规划。完成后可传送回本地执行。状态机:launching → sessionUrl → phase(needs_input/running) → pendingChoice。可选在云端直接执行(结果以 PR 落地)。Model 可通过 GrowthBook `tengu_ultraplan_model` 覆写。 | ||
| | Feature | Gate | Description | | ||
| |---|---|---| | ||
| | UDS Inbox | `UDS_INBOX` | Unix Domain Socket cross-session messaging. PID-scoped isolation, TeammateMailbox queue. | | ||
| | Coordinator Mode | `COORDINATOR_MODE` | Claude as project manager, directs Workers via task-notification XML. Shared scratchpad dir. | | ||
| | Advisor Model | — | `/advisor` — attaches stronger Opus 4.6 model as "supervisor" reviewing main model output. | | ||
| | Voice Mode | `VOICE_MODE` | Connects `voice_stream` for real-time STT. Requires OAuth. `/voice` command toggle. | | ||
| | Terminal Panel | `TERMINAL_PANEL` | Meta+J terminal panel, tmux-backed. `TerminalCaptureTool` captures panel output for Claude. | | ||
| | Chrome Integration | `tengu_chrome_auto_enable` | Post-extension install, Claude gets browser control via MCP. `WebBrowserTool` Playwright driver. | | ||
| | Self-Hosted Runner | `BYOC_ENVIRONMENT_RUNNER` | Run Claude Code on own server, BYOC mode. | | ||
| | Plugin Marketplace | — | GitHub-distributed plugin marketplace, skill+hook+MCP combos. `/plugin install`. ZIP cache support. | | ||
| | Cron Agent Triggers | `AGENT_TRIGGERS` | `CronCreate` / `CronDelete` / `CronList` — schedule prompt execution. `RemoteTriggerTool` for cloud cron. 7-day auto-expire. | | ||
| | Fork Subagent | `FORK_SUBAGENT` | Cache-safe fork — child agents share parent prompt prefix. `/fork` command, `/branch` management. | | ||
| | Context Collapse | `CONTEXT_COLLAPSE` | `CtxInspectTool` inspect state. REACTIVE_COMPACT, HISTORY_SNIP, CACHED_MICROCOMPACT. | | ||
| | Anti-Distillation | `ANTI_DISTILLATION_CC` | Prevent model output from being used for distillation training. API header level. | | ||
| 🔍UltraReview,Bug 舰队出动 | ||
| --- | ||
| /ultrareview | ||
| ## Build-Time Feature Flags (87) | ||
| 输入 `/ultrareview` 触发云端**多 Agent 并行代码审查** ,每个从不同角度检查你的代码。**暴力美学的代码审查** 。需要 GitHub repo + OAuth。有计费模型:free quota 用完后需 Extra Usage($10 起步),带确认对话框和 overage disclaimer。结果以 inline code comments 返回。 | ||
| | Flag | Description | Status | | ||
| |---|---|---| | ||
| | `PROACTIVE` | Proactive mode, SleepTool, background agent suggestions | Active | | ||
| | `KAIROS` | Proactive assistant mode, logs, session transcripts | Active | | ||
| | `KAIROS_BRIEF` | Brief-only view mode | Active | | ||
| | `KAIROS_CHANNELS` | MCP channel notifications | Active | | ||
| | `KAIROS_DREAM` | Auto-Dream memory consolidation (bundled skills) | Exp | | ||
| | `KAIROS_GITHUB_WEBHOOKS` | GitHub Webhook subscription | Active | | ||
| | `KAIROS_PUSH_NOTIFICATION` | Push notifications | Active | | ||
| | `AGENT_TRIGGERS` | Cron scheduled tasks (CronCreate/Delete/List) | Active | | ||
| | `AGENT_TRIGGERS_REMOTE` | Cloud cron jobs (RemoteTriggerTool) | Active | | ||
| | `MONITOR_TOOL` | Background task monitoring | Active | | ||
| | `FORK_SUBAGENT` | Sub-agent forking + cache sharing | Active | | ||
| | `UDS_INBOX` | Unix Domain Socket cross-process communication | Active | | ||
| | `WORKFLOW_SCRIPTS` | Workflow script execution | Active | | ||
| | `BUDDY` | AI pet system (18 species + rarity + animation) | Active | | ||
| | `VOICE_MODE` | Voice input/output | Active | | ||
| | `TERMINAL_PANEL` | Terminal panel/sidebar | Active | | ||
| | `COORDINATOR_MODE` | Multi-agent coordinator mode | Active | | ||
| | `BRIDGE_MODE` | IDE/remote bridge mode | Active | | ||
| | `CONTEXT_COLLAPSE` | Context collapse optimization | Active | | ||
| | `REACTIVE_COMPACT` | Reactive compaction strategy | Active | | ||
| | `CACHED_MICROCOMPACT` | Cached micro-compaction | Active | | ||
| | `HISTORY_SNIP` | History trimming | Active | | ||
| | `TOKEN_BUDGET` | Token budget tracking | Active | | ||
| | `EXTRACT_MEMORIES` | Auto memory extraction | Active | | ||
| | `TEAMMEM` | Team memory sharing | Active | | ||
| | `BG_SESSIONS` | Background session management | Active | | ||
| | `COMMIT_ATTRIBUTION` | Commit attribution tracking | Active | | ||
| | `CHICAGO_MCP` | MCP computer use integration | Active | | ||
| | `MCP_SKILLS` | MCP-based skill loading | Active | | ||
| | `TRANSCRIPT_CLASSIFIER` | Transcript classifier | Active | | ||
| | `BASH_CLASSIFIER` | Bash command classifier | Active | | ||
| | `REVIEW_ARTIFACT` | Review artifact skill | Active | | ||
| | `BUILDING_CLAUDE_APPS` | Claude API builder skill | Active | | ||
| | `UNATTENDED_RETRY` | Unattended retry | Active | | ||
| | `NATIVE_CLIENT_ATTESTATION` | Client attestation header | Active | | ||
| | `AWAY_SUMMARY` | Away summary generation | Exp | | ||
| | `MESSAGE_ACTIONS` | Message action menu | Exp | | ||
| | `HISTORY_PICKER` | History search picker UI | Exp | | ||
| | `QUICK_SEARCH` | Quick search shortcut | Exp | | ||
| | `AUTO_THEME` | Auto theme selection | Exp | | ||
| | `WEB_BROWSER_TOOL` | Web browser interaction (Playwright) | Exp | | ||
| | `DAEMON` | Background daemon mode | Exp | | ||
| | `ULTRAPLAN` | Cloud deep planning | Exp | | ||
| | `ULTRATHINK` | Ultra thinking mode | Exp | | ||
| | `TORCH` | Debug/monitoring commands | Exp | | ||
| | `LODESTONE` | Unknown (interactive helpers) | Exp | | ||
| | `TEMPLATES` | Job template system | Exp | | ||
| | `EXPERIMENTAL_SKILL_SEARCH` | Experimental skill search index | Exp | | ||
| | `MCP_RICH_OUTPUT` | MCP rich output formatting | Exp | | ||
| | `PROMPT_CACHE_BREAK_DETECTION` | Prompt cache invalidation detection | Active | | ||
| | `BREAK_CACHE_COMMAND` | Cache-busting command injection | Exp | | ||
| | `ANTI_DISTILLATION_CC` | Anti-distillation control | Exp | | ||
| | `CONNECTOR_TEXT` | Connector text block support | Exp | | ||
| | `DOWNLOAD_USER_SETTINGS` | Download user settings | Exp | | ||
| | `UPLOAD_USER_SETTINGS` | Upload user settings | Exp | | ||
| | `RUN_SKILL_GENERATOR` | Skill generator execution | Exp | | ||
| | `SKILL_IMPROVEMENT` | Skill improvement tracking | Exp | | ||
| | `CCR_REMOTE_SETUP` | CCR remote setup | Exp | | ||
| | `SELF_HOSTED_RUNNER` | Self-hosted runner | Exp | | ||
| | `BYOC_ENVIRONMENT_RUNNER` | BYOC environment execution | Exp | | ||
| | `SSH_REMOTE` | SSH remote execution | Exp | | ||
| | `DIRECT_CONNECT` | Direct connect mode | Exp | | ||
| | `VERIFICATION_AGENT` | Verification agent | Exp | | ||
| | `BUILTIN_EXPLORE_PLAN_AGENTS` | Built-in explore/plan agents | Exp | | ||
| | `AGENT_MEMORY_SNAPSHOT` | Agent memory snapshot | Exp | | ||
| | `HOOK_PROMPTS` | Hook prompt injection | Exp | | ||
| | `NEW_INIT` | New initialization flow | Exp | | ||
| | `TREE_SITTER_BASH` | Tree-sitter bash parsing | Exp | | ||
| | `TREE_SITTER_BASH_SHADOW` | Tree-sitter bash shadow mode | Exp | | ||
| | `STREAMLINED_OUTPUT` | Streamlined output formatting | Exp | | ||
| | `POWERSHELL_AUTO_MODE` | PowerShell auto-mode | Exp | | ||
| | `NATIVE_CLIPBOARD_IMAGE` | Native clipboard image support | Exp | | ||
| | `FILE_PERSISTENCE` | File persistence layer | Exp | | ||
| | `COMPACTION_REMINDERS` | Compaction reminder notifications | Exp | | ||
| | `PERFETTO_TRACING` | Perfetto tracing integration | Exp | | ||
| | `ENHANCED_TELEMETRY_BETA` | Enhanced telemetry beta | Exp | | ||
| | `SLOW_OPERATION_LOGGING` | Slow operation detailed logging | Exp | | ||
| | `MEMORY_SHAPE_TELEMETRY` | Memory shape telemetry | Exp | | ||
| | `COWORKER_TYPE_TELEMETRY` | Coworker type telemetry | Active | | ||
| | `IS_LIBC_MUSL` | MUSL libc detection | Infra | | ||
| | `IS_LIBC_GLIBC` | GLIBC libc detection | Infra | | ||
| | `OVERFLOW_TEST_TOOL` | Overflow handling test tool | Test | | ||
| | `ABLATION_BASELINE` | Ablation test baseline | Test | | ||
| | `SHOT_STATS` | Shot stats tracking | Exp | | ||
| | `ALLOW_TEST_VERSIONS` | Allow test versions | Test | | ||
| | `HARD_FAIL` | Hard fail mode | Test | | ||
| | `CCR_AUTO_CONNECT` | CCR auto-connect | Exp | | ||
| 🤖Agent Swarms,持久化 Agent 团队 | ||
| --- | ||
| ENABLE_AGENT_SWARMS | ||
| ## GrowthBook Runtime Flags (59 `tengu_*`) | ||
| 创建**持久化 Agent 团队** ,成员间互相通信协作。Team Lead 在主终端,Teammates 在 **tmux panes** (TmuxBackend)或进程内(SpawnInProcess)或 iTerm2(ITermBackend)。UDS 邮箱通信,颜色编码区分成员。分布式权限同步(permissionSync),断线重连,Layout 管理。`TeamCreateTool` / `TeamDeleteTool` / `SendMessageTool`。 | ||
| A/B testing + progressive rollout. Names are obfuscated whimsical codenames. Fetched at runtime via GrowthBook SDK, no rebuild required. | ||
| 😴Away Summary,你走了我还在 | ||
| | Flag | Default | Description | Category | | ||
| |---|---|---|---| | ||
| | `tengu_onyx_plover` | false | Auto-Dream consolidation trigger (minHours/minSessions config) | Memory | | ||
| | `tengu_passport_quail` | false | Memory passport — cross-project memory transfer | Memory | | ||
| | `tengu_slate_thimble` | false | Memory organization optimization | Memory | | ||
| | `tengu_coral_fern` | false | Memory management enhancement | Memory | | ||
| | `tengu_herring_clock` | false | Session memory persistence | Memory | | ||
| | `tengu_session_memory` | false | Explicit session memory toggle | Memory | | ||
| | `tengu_bramble_lintel` | null | Memory extraction throttle (every N eligible turns) | Memory | | ||
| | `tengu_cobalt_raccoon` | false | Reactive compaction strategy | Context | | ||
| | `tengu_slate_prism` | varies | UI/display optimization | UI | | ||
| | `tengu_hive_evidence` | false | Evidence collection system | Quality | | ||
| | `tengu_chomp_inflection` | false | Prompt suggestion popup | UX | | ||
| | `tengu_birch_trellis` | true | Bash permission defaults | Permission | | ||
| | `tengu_harbor` | false | Channel allowlist (Kairos channel whitelist) | Kairos | | ||
| | `tengu_harbor_permissions` | false | Channel permission control | Kairos | | ||
| | `tengu_cobalt_harbor` | false | Harbor variant | Kairos | | ||
| | `tengu_bridge_repl_v2` | false | Bridge REPL v2 protocol | Bridge | | ||
| | `tengu_bridge_system_init` | false | Bridge system init flow | Bridge | | ||
| | `tengu_remote_backend` | false | Remote TUI backend | Remote | | ||
| | `tengu_quartz_lantern` | false | File operation optimization | Perf | | ||
| | `tengu_cobalt_lantern` | false | Remote operation variant | Remote | | ||
| | `tengu_ccr_bridge` | false | CCR bridge connection | Remote | | ||
| | `tengu_ccr_mirror` | false | CCR mirror mode | Remote | | ||
| | `tengu_strap_foyer` | false | Settings sync — cloud sync settings | Sync | | ||
| | `tengu_auto_background_agents` | false | Auto background agent execution | Agent | | ||
| | `tengu_surreal_dali` | false | Remote agent scheduling (RemoteTriggerTool gate) | Agent | | ||
| | `tengu_cicada_nap_ms` | 0 | Background refresh throttle (ms) | Perf | | ||
| | `tengu_ultraplan_model` | Opus 4.6 | UltraPlan model selection | Model | | ||
| | `tengu_amber_stoat` | true | Built-in agent availability | Agent | | ||
| | `tengu_slim_subagent_claudemd` | true | Subagent ClaudeMd slim variant | Agent | | ||
| | `tengu_agent_list_attach` | false | Agent list attached to messages | Agent | | ||
| | `tengu_amber_flint` | true | Agent swarm enabled | Swarm | | ||
| | `tengu_terminal_panel` | false | Terminal panel (runtime gate) | UI | | ||
| | `tengu_terminal_sidebar` | false | Terminal sidebar display | UI | | ||
| | `tengu_willow_mode` | 'off' | Willow mode config | Mode | | ||
| | `tengu_destructive_command_warning` | false | Destructive command warnings | Safety | | ||
| | `tengu_glacier_2xr` | false | Tool search variant | Search | | ||
| | `tengu_lapis_finch` | false | Plugin hint recommendations | Plugin | | ||
| | `tengu_amber_quartz_disabled` | false | Voice mode kill switch | Voice | | ||
| | `tengu_collage_kaleidoscope` | true | Image paste handling | Media | | ||
| | `tengu_marble_fox` | false | File persistence variant | Storage | | ||
| | `tengu_marble_sandcastle` | false | Fast mode optimization | Perf | | ||
| | `tengu_pebble_leaf_prune` | false | Session storage pruning | Storage | | ||
| | `tengu_chrome_auto_enable` | false | Chrome MCP auto-enable | Chrome | | ||
| | `tengu_copper_bridge` | false | Chrome MCP bridge | Chrome | | ||
| | `tengu_lodestone_enabled` | false | Lodestone feature gate | Exp | | ||
| | `tengu_miraculo_the_bard` | false | Background service | Service | | ||
| | `tengu_amber_json_tools` | false | JSON tool variant | Tool | | ||
| | `tengu_copper_panda` | false | Skill improvement tracking | Skill | | ||
| | `tengu_immediate_model_command` | false | Immediate model switch | Model | | ||
| | `tengu_moth_copse` | false | Attachment handling | Media | | ||
| | `tengu_basalt_3kr` | false | MCP instructions delta | MCP | | ||
| | `tengu_trace_lantern` | false | Telemetry tracing | Telemetry | | ||
| | `tengu_fgts` | false | Fine-grained tool streaming | Streaming | | ||
| | `tengu_turtle_carbon` | true | Thinking mode enabled | Thinking | | ||
| | `tengu_otk_slot_v1` | false | OTK slot variant | Auth | | ||
| | `tengu_kairos_brief` | false | Brief mode (runtime gate) | Kairos | | ||
| | `tengu_jade_anvil_4` | false | Rate limit options | API | | ||
| | `tengu_attribution_header` | true | System attribution headers | System | | ||
| | `tengu_1p_event_batch_config` | — | First-party event batch config | Telemetry | | ||
| feature('AWAY_SUMMARY') | ||
| --- | ||
| 你走了自动干活,**回来生成"我干了啥"摘要** 。用 Haiku 快速生成 1-3 句话总结,截取最近 30 条消息 + session memory 作为上下文。聚焦你在 build 什么而非实现细节,附带 **concrete next step** 。跳过 status report 和 commit recap。触发点:IdleReturnDialog。非阻塞,错误静默降级。 | ||
| ## Environment Variables (102 `CLAUDE_CODE_*`) | ||
| 还有这些 | ||
| | Variable | Description | Category | | ||
| |---|---|---| | ||
| | `USE_BEDROCK` | Use AWS Bedrock instead of Claude API | Provider | | ||
| | `USE_VERTEX` | Use Google Vertex AI | Provider | | ||
| | `USE_FOUNDRY` | Use Google Foundry | Provider | | ||
| | `SKIP_BEDROCK_AUTH` | Skip Bedrock auth | Provider | | ||
| | `SKIP_VERTEX_AUTH` | Skip Vertex auth | Provider | | ||
| | `REMOTE` | Remote execution mode (CCR) | Remote | | ||
| | `ACTION` | Action mode execution | Mode | | ||
| | `BRIEF` | Brief mode output | Mode | | ||
| | `PROACTIVE` | Proactive mode override | Mode | | ||
| | `SIMPLE` | Simple/lightweight mode | Mode | | ||
| | `COORDINATOR_MODE` | Coordinator agent mode override | Mode | | ||
| | `DISABLE_THINKING` | Disable extended thinking | Thinking | | ||
| | `DISABLE_ADAPTIVE_THINKING` | Disable adaptive thinking | Thinking | | ||
| | `DISABLE_FAST_MODE` | Disable fast mode | Model | | ||
| | `DISABLE_AUTO_MEMORY` | Disable auto memory extraction | Memory | | ||
| | `DISABLE_CLAUDE_MDS` | Disable CLAUDE.md loading | Context | | ||
| | `DISABLE_1M_CONTEXT` | Disable 1M context window | Context | | ||
| | `DISABLE_GIT_INSTRUCTIONS` | Disable git instructions | Context | | ||
| | `DISABLE_BACKGROUND_TASKS` | Disable background task execution | Agent | | ||
| | `DISABLE_CRON` | Disable cron tool | Agent | | ||
| | `DISABLE_COMMAND_INJECTION_CHECK` | Disable injection detection | Security | | ||
| | `DISABLE_FILE_CHECKPOINTING` | Disable file checkpointing | Storage | | ||
| | `DISABLE_ATTACHMENTS` | Disable file attachments | Media | | ||
| | `DISABLE_TERMINAL_TITLE` | Disable terminal title updates | UI | | ||
| | `DISABLE_MOUSE` | Disable mouse support | UI | | ||
| | `ACCESSIBILITY` | Accessibility mode | UI | | ||
| | `NO_FLICKER` | Reduce visual flicker | UI | | ||
| | `STREAMLINED_OUTPUT` | Streamlined output format | UI | | ||
| | `FORCE_FULL_LOGO` | Always show full logo | UI | | ||
| | `BASH_SANDBOX_SHOW_INDICATOR` | Show sandbox indicator | UI | | ||
| | `BUBBLEWRAP` | Bubblewrap sandbox | Sandbox | | ||
| | `USE_POWERSHELL_TOOL` | Use PowerShell instead of Bash | Shell | | ||
| | `TWO_STAGE_CLASSIFIER` | Two-stage bash classification | Permission | | ||
| | `ENABLE_TELEMETRY` | Enable telemetry | Telemetry | | ||
| | `ENABLE_SDK_FILE_CHECKPOINTING` | SDK-based file checkpointing | Storage | | ||
| | `ENABLE_CFC` | Claude in Chrome support | Chrome | | ||
| | `ENABLE_FINE_GRAINED_TOOL_STREAMING` | Fine-grained tool streaming | Streaming | | ||
| | `ENABLE_TASKS` | Enable task list | Task | | ||
| | `ENABLE_XAA` | XAA/IDP login | Auth | | ||
| | `EXPERIMENTAL_AGENT_TEAMS` | Agent team swarms | Swarm | | ||
| | `PLAN_MODE_REQUIRED` | Force plan mode | Mode | | ||
| | `VERIFY_PLAN` | Verify plan execution | Mode | | ||
| | `UNATTENDED_RETRY` | Unattended retry override | Retry | | ||
| | `UNDERCOVER` | Undercover/stealth mode | Debug | | ||
| | `DEBUG_REPAINTS` | Debug Ink component repaints | Debug | | ||
| | `PROFILE_STARTUP` | Profile startup performance | Debug | | ||
| | `PROFILE_QUERY` | Profile query execution | Debug | | ||
| | `DUMP_AUTO_MODE` | Dump auto-mode decisions | Debug | | ||
| | `DUMP_SYSTEM_PROMPT` | Dump system prompt | Debug | | ||
| | `TERMINAL_RECORDING` | Terminal recording | Debug | | ||
| | `SAVE_HOOK_ADDITIONAL_CONTEXT` | Save hook extra context | Debug | | ||
| | `DISABLE_NONSTREAMING_FALLBACK` | Disable non-streaming fallback | API | | ||
| | `SKIP_FAST_MODE_NETWORK_ERRORS` | Fast mode ignore network errors | API | | ||
| | `DISABLE_EXPERIMENTAL_BETAS` | Disable experimental betas | Feature | | ||
| | `DISABLE_FEEDBACK_SURVEY` | Disable feedback survey | UI | | ||
| | `DISABLE_ADVISOR_TOOL` | Disable advisor tool | Tool | | ||
| | `DISABLE_POLICY_SKILLS` | Disable policy-based skills | Skill | | ||
| | `SYNC_PLUGIN_INSTALL` | Sync plugin installation | Plugin | | ||
| | `DISABLE_PRECOMPACT_SKIP` | Disable precompact skip optimization | Context | | ||
| | `DISABLE_NONESSENTIAL_TRAFFIC` | Reduce non-essential network traffic | Network | | ||
| | `SUBPROCESS_ENV_SCRUB` | Scrub subprocess environment variables | Security | | ||
| | `PROVIDER_MANAGED_BY_HOST` | Provider managed externally | Provider | | ||
| | `DISABLE_LEGACY_MODEL_REMAP` | Preserve legacy model naming | Model | | ||
| | `MCP_INSTR_DELTA` | MCP instructions delta | MCP | | ||
| | `USE_NATIVE_FILE_SEARCH` | Native file search | Search | | ||
| | `USE_CCR_V2` | CCR V2 transport protocol | Remote | | ||
| | `AUTO_CONNECT_IDE` | Auto-connect IDE | IDE | | ||
| | `IDE_SKIP_VALID_CHECK` | Skip IDE validation | IDE | | ||
| | `IDE_SKIP_AUTO_INSTALL` | Skip IDE auto-install | IDE | | ||
| | `GB_BASE_URL` | GrowthBook API base URL | Infra | | ||
| | `HOST_PLATFORM` | Override host platform | Infra | | ||
| | `ENTRYPOINT` | Entry type (local-agent, etc.) | Infra | | ||
| | `CONTAINER_ID` | Container ID tracking | Infra | | ||
| | `REMOTE_SESSION_ID` | Remote session ID | Infra | | ||
| | `TAGS` | Session tags | Infra | | ||
| | `COWORKER_TYPE` | Coworker type classification | Infra | | ||
| | `REMOTE_ENVIRONMENT_TYPE` | Remote environment classification | Infra | | ||
| | `PROXY_RESOLVES_HOSTS` | Proxy resolves hostnames | Network | | ||
| | `PLUGIN_USE_ZIP_CACHE` | Plugin zip cache | Plugin | | ||
| | `ADDITIONAL_DIRECTORIES_CLAUDE_MD` | Extra directory scanning | Context | | ||
| | `EXIT_AFTER_FIRST_RENDER` | Exit after first render (testing) | Test | | ||
| | `INCLUDE_PARTIAL_MESSAGES` | Telemetry includes partial messages | Telemetry | | ||
| | `EMIT_SESSION_STATE_EVENTS` | Emit session state events | Event | | ||
| | `SKIP_PROMPT_HISTORY` | Skip prompt history | History | | ||
| | `REMOTE_SEND_KEEPALIVES` | Remote keepalive messages | Remote | | ||
| | `AGENT_LIST_IN_MESSAGES` | List agents in messages | Agent | | ||
| | `ALWAYS_ENABLE_EFFORT` | Always enable effort mode | Mode | | ||
| | `EAGERLY_FLUSH` | Eager state flushing | Perf | | ||
| | `CCR_MIRROR` | CCR mirror override | Remote | | ||
| | `NEW_INIT` | New init flow override | Init | | ||
| | `BRIEF_UPLOAD` | Brief mode upload | Mode | | ||
| | `IS_COWORK` | Cowork environment marker | Infra | | ||
| | `USE_COWORK_PLUGINS` | Use cowork plugins | Plugin | | ||
| 📬 | ||
| --- | ||
| UDS Inbox,跨会话通信 | ||
| ## All Tools (57) | ||
| 通过 Unix Domain Socket 让不同 Claude Code 会话互相发消息。PID-scoped 隔离,TeammateMailbox 队列。三个窗口不再隔离,通过 uds 前缀寻址,真正的多进程协作。 | ||
| | Tool | Description | Tags | | ||
| |---|---|---| | ||
| | `Agent` | Spawn sub-agents, sync/async/fork/worktree isolation | deferred, concurrent | | ||
| | `Bash` | Shell command execution, streaming output, timeout + background | destructive | | ||
| | `Read` | Read files (text/image/PDF/Jupyter), offset+limit | read-only, concurrent | | ||
| | `Edit` | Exact string replacement, find-and-replace + unified diff | destructive | | ||
| | `Write` | Create/overwrite entire file | destructive | | ||
| | `Glob` | File pattern matching (max 100 results) | read-only, concurrent | | ||
| | `Grep` | ripgrep content search, regex + context lines | read-only, concurrent | | ||
| | `WebSearch` | Web search, allow/block domain filtering | read-only, concurrent | | ||
| | `WebFetch` | URL content fetch + prompt extraction | read-only, deferred | | ||
| | `NotebookEdit` | Jupyter cell editing (replace/insert/delete) | destructive | | ||
| | `Skill` | Execute bundled/user/MCP skills | deferred | | ||
| | `ToolSearch` | Search deferred tool schemas by keyword | deferred | | ||
| | `TaskCreate` | Create task (v2 task system) | deferred, concurrent | | ||
| | `TaskUpdate` | Update task status/deps/metadata | deferred, concurrent | | ||
| | `TaskStop` | Kill background task/shell | deferred | | ||
| | `SendMessage` | Send messages between swarm agents / broadcast | deferred, swarm | | ||
| | `TeamCreate` | Create multi-agent swarm team | deferred, swarm | | ||
| | `TeamDelete` | Disband swarm team + cleanup | deferred, swarm | | ||
| | `CronCreate` | Cron-scheduled prompt (cron expression) | deferred, AGENT_TRIGGERS | | ||
| | `RemoteTrigger` | Cloud cron agent (create/run/update) | deferred, AGENT_TRIGGERS_REMOTE | | ||
| | `Brief` | Kairos report message + attachments | KAIROS | | ||
| | `Sleep` | Proactive scheduled wake | PROACTIVE | | ||
| | `PushNotification` | Mobile push notification | KAIROS | | ||
| | `SubscribePR` | GitHub PR webhook subscription | KAIROS_GITHUB_WEBHOOKS | | ||
| | `EnterWorktree` | Create git worktree isolation env | deferred | | ||
| | `ExitWorktree` | Exit worktree (keep/remove) | deferred | | ||
| | `EnterPlanMode` | Enter plan mode for complex task design | read-only, concurrent | | ||
| | `ExitPlanMode` | Exit plan mode + execute | deferred | | ||
| | `WebBrowser` | Playwright browser automation | deferred, WEB_BROWSER_TOOL | | ||
| | `ListMcpResources` | List MCP server resources | read-only, deferred | | ||
| | `MCPTool` | Meta-tool wrapping all MCP server tools | MCP | | ||
| | `ListPeers` | List UDS connected peers | UDS_INBOX | | ||
| | `Workflow` | Execute bundled workflow scripts | WORKFLOW_SCRIPTS | | ||
| | `CtxInspect` | Inspect context collapse state | CONTEXT_COLLAPSE | | ||
| | `TerminalCapture` | Capture terminal panel output | TERMINAL_PANEL | | ||
| | `Monitor` | Background task monitoring | MONITOR_TOOL | | ||
| | `Snip` | History trim/extract | HISTORY_SNIP | | ||
| | `StructuredOutput` | SDK non-interactive JSON output | read-only | | ||
| | `REPL` | Ant-only REPL (wraps Bash/Read/Edit) | ant-only | | ||
| | `Config` | Get/set config (theme, model, permissions) | ant-only | | ||
| 🎯 | ||
| --- | ||
| Coordinator 协调者模式 | ||
| ## Slash Commands (103+) | ||
| Claude 变项目经理,只指挥 Worker 干活。带 Scratchpad 共享目录。Worker 通过 task-notification XML 汇报。所有 agent 强制 async,summarization 默认开启。 | ||
| ### Conversation | ||
| `/resume` `/clear` `/compact` `/branch` `/rename` `/rewind` `/export` `/session` `/plan` `/tasks` | ||
| 🧙 | ||
| ### Code & Git | ||
| `/commit` `/commit-push-pr` `/review` `/ultrareview`* `/security-review` `/diff` `/context` `/init` `/init-verifiers`† | ||
| Advisor 顾问模型 | ||
| ### Model & Mode | ||
| `/model` `/effort` `/fast` `/advisor` `/voice`* `/ultraplan`* | ||
| `/advisor` — 给主模型配一个更强的"监工"顾问。Opus 4.6 作为 advisor,实时审查主模型的输出质量。 | ||
| ### Kairos / Proactive | ||
| `/brief`* `/assistant`* `/proactive`* `/subscribe-pr`* `/buddy`* | ||
| 🎤 | ||
| ### UI & Display | ||
| `/theme` `/color` `/vim` `/stickers` `/status` `/cost` `/output-style` `/statusline` | ||
| Voice 语音模式 | ||
| ### Integrations | ||
| `/mcp` `/plugin` `/desktop` `/chrome` `/mobile` `/ide` `/install-github-app` `/install-slack-app` `/web-setup`* `/remote-control`* | ||
| 连接 voice_stream 实时语音转文字。需 OAuth 登录。`feature('VOICE_MODE')` gate。支持 /voice 命令切换。 | ||
| ### Memory & Config | ||
| `/memory` `/think-back` `/config` `/permissions` `/hooks` `/keybindings` `/privacy-settings` `/skills` | ||
| 📺 | ||
| ### Advanced / Hidden | ||
| `/fork`* `/peers`* `/workflows`* `/force-snip`* `/torch`* `/teleport`† `/break-cache`† `/mock-limits`† | ||
| Terminal Panel | ||
| Meta+J 开终端面板,基于 tmux,shell 不丢失。`TerminalCaptureTool` 捕获面板输出供 Claude 读取。 | ||
| 🌐 | ||
| Chrome 浏览器集成 | ||
| 扩展安装后 Claude 通过 MCP 获得浏览器控制力。`tengu_chrome_auto_enable` 自动启用。`WebBrowserTool` Playwright 驱动。 | ||
| 🏠 | ||
| Self-Hosted Runner | ||
| 在自己服务器跑 Claude Code,BYOC 自带云模式。`BYOC_ENVIRONMENT_RUNNER`。 | ||
| 🔌 | ||
| Plugin Marketplace | ||
| GitHub 仓库分发插件市场,技能+钩子+MCP 组合。`/plugin install` 安装,支持 zip cache。 | ||
| ⏰ | ||
| Cron Agent Triggers | ||
| `CronCreate` / `CronDelete` / `CronList` — 定时执行 prompt。`RemoteTriggerTool` 云端 cron。7 天自动过期。 | ||
| 🔀 | ||
| Fork Subagent | ||
| Cache-safe fork — 子 agent 共享父级 prompt prefix 省 token。`/fork` 命令,`/branch` 分支管理。 | ||
| 📐 | ||
| Context Collapse | ||
| `CtxInspectTool` 检查状态。REACTIVE_COMPACT 响应式压缩。HISTORY_SNIP 历史裁剪。CACHED_MICROCOMPACT 缓存微压缩。 | ||
| 🔐 | ||
| Anti-Distillation | ||
| `ANTI_DISTILLATION_CC` — 防止模型输出被用于蒸馏训练。API header 级别控制。 | ||
| Build-Time Feature Flags (87) | ||
| Flag| Description| Status | ||
| ---|---|--- | ||
| PROACTIVE| 自主 proactive 模式,SleepTool,后台 agent 建议| Active | ||
| KAIROS| 主动助手模式,日志,session transcripts| Active | ||
| KAIROS_BRIEF| Brief-only 视图模式| Active | ||
| KAIROS_CHANNELS| MCP Channel 通知| Active | ||
| KAIROS_DREAM| Auto-Dream 记忆整理(bundled skills)| Exp | ||
| KAIROS_GITHUB_WEBHOOKS| GitHub Webhook 订阅| Active | ||
| KAIROS_PUSH_NOTIFICATION| 推送通知| Active | ||
| AGENT_TRIGGERS| Cron 定时任务(CronCreate/Delete/List)| Active | ||
| AGENT_TRIGGERS_REMOTE| 云端 cron jobs(RemoteTriggerTool)| Active | ||
| MONITOR_TOOL| 后台任务监控| Active | ||
| FORK_SUBAGENT| Sub-agent forking + cache sharing| Active | ||
| UDS_INBOX| Unix Domain Socket 跨进程通信| Active | ||
| WORKFLOW_SCRIPTS| Workflow 脚本执行| Active | ||
| BUDDY| AI 宠物系统(18 物种 + 稀有度 + 动画)| Active | ||
| VOICE_MODE| 语音输入/输出| Active | ||
| TERMINAL_PANEL| 终端面板/侧边栏| Active | ||
| COORDINATOR_MODE| 多 agent 协调者模式| Active | ||
| BRIDGE_MODE| IDE/远程桥接模式| Active | ||
| CONTEXT_COLLAPSE| Context collapse 优化| Active | ||
| REACTIVE_COMPACT| 响应式压缩策略| Active | ||
| CACHED_MICROCOMPACT| 缓存微压缩| Active | ||
| HISTORY_SNIP| 历史裁剪| Active | ||
| TOKEN_BUDGET| Token 预算追踪| Active | ||
| EXTRACT_MEMORIES| 自动记忆提取| Active | ||
| TEAMMEM| 团队记忆共享| Active | ||
| BG_SESSIONS| 后台 session 管理| Active | ||
| COMMIT_ATTRIBUTION| Commit 归属追踪| Active | ||
| CHICAGO_MCP| MCP computer use 集成| Active | ||
| MCP_SKILLS| MCP-based skill 加载| Active | ||
| TRANSCRIPT_CLASSIFIER| Transcript 分类器| Active | ||
| BASH_CLASSIFIER| Bash 命令分类器| Active | ||
| REVIEW_ARTIFACT| Review artifact skill| Active | ||
| BUILDING_CLAUDE_APPS| Claude API builder skill| Active | ||
| UNATTENDED_RETRY| 无人值守重试| Active | ||
| NATIVE_CLIENT_ATTESTATION| Client 认证 header| Active | ||
| AWAY_SUMMARY| 离开后摘要生成| Exp | ||
| MESSAGE_ACTIONS| 消息动作菜单| Exp | ||
| HISTORY_PICKER| 历史搜索 picker UI| Exp | ||
| QUICK_SEARCH| 快速搜索快捷键| Exp | ||
| AUTO_THEME| 自动主题选择| Exp | ||
| WEB_BROWSER_TOOL| Web 浏览器交互 (Playwright)| Exp | ||
| DAEMON| 后台守护进程模式| Exp | ||
| ULTRAPLAN| 云端深度规划| Exp | ||
| ULTRATHINK| Ultra thinking 模式| Exp | ||
| TORCH| 调试/监控命令| Exp | ||
| LODESTONE| Unknown (interactive helpers)| Exp | ||
| TEMPLATES| Job 模板系统| Exp | ||
| EXPERIMENTAL_SKILL_SEARCH| 实验性 skill 搜索索引| Exp | ||
| MCP_RICH_OUTPUT| MCP 富输出格式化| Exp | ||
| PROMPT_CACHE_BREAK_DETECTION| Prompt cache 失效检测| Active | ||
| BREAK_CACHE_COMMAND| 缓存破坏命令注入| Exp | ||
| ANTI_DISTILLATION_CC| 防蒸馏控制| Exp | ||
| CONNECTOR_TEXT| Connector text block 支持| Exp | ||
| DOWNLOAD_USER_SETTINGS| 下载用户设置| Exp | ||
| UPLOAD_USER_SETTINGS| 上传用户设置| Exp | ||
| RUN_SKILL_GENERATOR| Skill 生成器执行| Exp | ||
| SKILL_IMPROVEMENT| Skill 改进追踪| Exp | ||
| CCR_REMOTE_SETUP| CCR 远程设置| Exp | ||
| SELF_HOSTED_RUNNER| 自托管 runner| Exp | ||
| BYOC_ENVIRONMENT_RUNNER| BYOC 环境执行| Exp | ||
| SSH_REMOTE| SSH 远程执行| Exp | ||
| DIRECT_CONNECT| 直连模式| Exp | ||
| VERIFICATION_AGENT| 验证 agent| Exp | ||
| BUILTIN_EXPLORE_PLAN_AGENTS| 内置探索/规划 agents| Exp | ||
| AGENT_MEMORY_SNAPSHOT| Agent 记忆快照| Exp | ||
| HOOK_PROMPTS| Hook prompt 注入| Exp | ||
| NEW_INIT| 新初始化流程| Exp | ||
| TREE_SITTER_BASH| Tree-sitter bash 解析| Exp | ||
| TREE_SITTER_BASH_SHADOW| Tree-sitter bash shadow 模式| Exp | ||
| STREAMLINED_OUTPUT| 精简输出格式化| Exp | ||
| POWERSHELL_AUTO_MODE| PowerShell auto-mode| Exp | ||
| NATIVE_CLIPBOARD_IMAGE| 原生剪贴板图片支持| Exp | ||
| FILE_PERSISTENCE| 文件持久层| Exp | ||
| COMPACTION_REMINDERS| 压缩提醒通知| Exp | ||
| PERFETTO_TRACING| Perfetto 追踪集成| Exp | ||
| ENHANCED_TELEMETRY_BETA| 增强遥测 beta| Exp | ||
| SLOW_OPERATION_LOGGING| 慢操作详细日志| Exp | ||
| MEMORY_SHAPE_TELEMETRY| 记忆形状遥测| Exp | ||
| COWORKER_TYPE_TELEMETRY| Coworker 类型遥测| Active | ||
| IS_LIBC_MUSL| MUSL libc 检测| Infra | ||
| IS_LIBC_GLIBC| GLIBC libc 检测| Infra | ||
| OVERFLOW_TEST_TOOL| 溢出处理测试工具| Test | ||
| ABLATION_BASELINE| 消融测试基线| Test | ||
| SHOT_STATS| Shot 统计追踪| Exp | ||
| ALLOW_TEST_VERSIONS| 允许测试版本| Test | ||
| HARD_FAIL| 硬失败模式| Test | ||
| CCR_AUTO_CONNECT| CCR 自动连接| Exp | ||
| All Tools (57) | ||
| Agent | ||
| Spawn sub-agents,支持 sync/async/fork/worktree 隔离 | ||
| deferredconcurrent | ||
| Bash | ||
| Shell 命令执行,streaming output,timeout + background | ||
| destructive | ||
| Read | ||
| 读取文件(text/image/PDF/Jupyter),offset+limit | ||
| read-onlyconcurrent | ||
| Edit | ||
| 精确字符串替换,find-and-replace + unified diff | ||
| destructive | ||
| Write | ||
| 创建/覆写整个文件 | ||
| destructive | ||
| Glob | ||
| 文件 pattern 匹配 (max 100 results) | ||
| read-onlyconcurrent | ||
| Grep | ||
| ripgrep 内容搜索,regex + context lines | ||
| read-onlyconcurrent | ||
| WebSearch | ||
| Web 搜索,支持 domain allow/block | ||
| read-onlyconcurrent | ||
| WebFetch | ||
| URL 内容获取 + prompt 提取 | ||
| read-onlydeferred | ||
| NotebookEdit | ||
| Jupyter cell 编辑 (replace/insert/delete) | ||
| destructive | ||
| Skill | ||
| 执行 bundled/user/MCP skills | ||
| deferred | ||
| ToolSearch | ||
| 搜索 deferred tool schemas by keyword | ||
| deferred | ||
| TaskCreate | ||
| 创建 task (v2 task system) | ||
| deferredconcurrent | ||
| TaskUpdate | ||
| 更新 task status/deps/metadata | ||
| deferredconcurrent | ||
| TaskStop | ||
| Kill 后台运行的 task/shell | ||
| deferred | ||
| SendMessage | ||
| Swarm agent 间发消息 / broadcast | ||
| deferredswarm | ||
| TeamCreate | ||
| 创建 multi-agent swarm team | ||
| deferredswarm | ||
| TeamDelete | ||
| 解散 swarm team + cleanup | ||
| deferredswarm | ||
| CronCreate | ||
| 定时 prompt 调度 (cron expression) | ||
| deferredAGENT_TRIGGERS | ||
| RemoteTrigger | ||
| 云端 cron agent (create/run/update) | ||
| deferredAGENT_TRIGGERS_REMOTE | ||
| Brief | ||
| Kairos 简报消息 + 附件 | ||
| KAIROS | ||
| Sleep | ||
| Proactive 定时唤醒 | ||
| PROACTIVE | ||
| PushNotification | ||
| 移动端推送通知 | ||
| KAIROS | ||
| SubscribePR | ||
| GitHub PR webhook 订阅 | ||
| KAIROS_GITHUB_WEBHOOKS | ||
| EnterWorktree | ||
| 创建 git worktree 隔离环境 | ||
| deferred | ||
| ExitWorktree | ||
| 退出 worktree (keep/remove) | ||
| deferred | ||
| EnterPlanMode | ||
| 进入 plan mode 设计复杂任务 | ||
| read-onlyconcurrent | ||
| ExitPlanMode | ||
| 退出 plan mode + 执行 | ||
| deferred | ||
| WebBrowser | ||
| Playwright 浏览器自动化 | ||
| deferredWEB_BROWSER_TOOL | ||
| ListMcpResources | ||
| 列出 MCP server resources | ||
| read-onlydeferred | ||
| MCPTool | ||
| Meta-tool wrapping 所有 MCP server tools | ||
| MCP | ||
| ListPeers | ||
| 列出 UDS connected peers | ||
| UDS_INBOX | ||
| Workflow | ||
| 执行 bundled workflow scripts | ||
| WORKFLOW_SCRIPTS | ||
| CtxInspect | ||
| 检查 context collapse 状态 | ||
| CONTEXT_COLLAPSE | ||
| TerminalCapture | ||
| 捕获终端面板输出 | ||
| TERMINAL_PANEL | ||
| Monitor | ||
| 后台任务监控 | ||
| MONITOR_TOOL | ||
| Snip | ||
| 历史裁剪/提取 | ||
| HISTORY_SNIP | ||
| StructuredOutput | ||
| SDK non-interactive JSON 输出 | ||
| read-only | ||
| REPL | ||
| Ant-only REPL 环境 (wraps Bash/Read/Edit) | ||
| ant-only | ||
| Config | ||
| Get/set 配置 (theme, model, permissions) | ||
| ant-only | ||
| Slash Commands (103+) | ||
| Conversation (10) | ||
| /resume恢复上次会话 | ||
| /clear清空对话 | ||
| /compact压缩上下文 | ||
| /branch分支对话 | ||
| /rename重命名 session | ||
| /rewind回退到某步 | ||
| /export导出对话 | ||
| /sessionSession 管理 | ||
| /plan进入 plan mode | ||
| /tasksTask 列表 | ||
| Code & Git (9) | ||
| /commitGit commit | ||
| /commit-push-prCommit+Push+PR | ||
| /review代码审查 | ||
| /ultrareview多 Agent 云端审查 | ||
| /security-review安全审查 | ||
| /diff查看 diff | ||
| /contextContext 操作 | ||
| /init项目初始化 | ||
| /init-verifiersVerifier 初始化 | ||
| Model & Mode (5+) | ||
| /model切换模型 | ||
| /effort调节 effort | ||
| /fastFast 模式 | ||
| /advisorOpus 顾问模型 | ||
| /voice语音模式 | ||
| /ultraplan云端深度规划 | ||
| Kairos / Proactive (5) | ||
| /briefKairos 简报 | ||
| /assistant助手模式 | ||
| /proactive主动模式 | ||
| /subscribe-prGitHub PR 订阅 | ||
| /buddyAI 宠物 | ||
| UI & Display (8) | ||
| /theme主题切换 | ||
| /color颜色设置 | ||
| /vimVim 模式 | ||
| /stickers贴纸 | ||
| /status状态显示 | ||
| /costToken 消耗 | ||
| /output-style输出风格 | ||
| /statusline状态栏配置 | ||
| Integrations (10) | ||
| /mcpMCP server 管理 | ||
| /plugin插件安装/管理 | ||
| /desktop桌面 App | ||
| /chromeChrome 集成 | ||
| /mobile移动端 | ||
| /ideIDE 集成 | ||
| /install-github-appGitHub App | ||
| /install-slack-appSlack App | ||
| /web-setupWeb 版设置 | ||
| /remote-control远程控制 | ||
| Memory & Config (8) | ||
| /memory记忆管理 | ||
| /think-back回忆 | ||
| /config配置管理 | ||
| /permissions权限设置 | ||
| /hooksHook 管理 | ||
| /keybindings快捷键 | ||
| /privacy-settings隐私设置 | ||
| /skillsSkill 列表 | ||
| Advanced / Hidden (8) | ||
| /forkFork sub-agent | ||
| /peersUDS peer list | ||
| /workflowsWorkflow scripts | ||
| /force-snip强制历史裁剪 | ||
| /torch调试/监控 | ||
| /teleport跨机器传送 | ||
| /break-cache破坏 prompt cache | ||
| /mock-limits模拟 rate limit | ||
| Public Feature-gated Internal | ||
| GrowthBook Runtime Flags (59 tengu_*) | ||
| A/B 测试 + 渐进式 rollout。名称经过混淆(whimsical codenames)。通过 GrowthBook SDK 在运行时获取,无需重新构建。 | ||
| Flag| Default| Description| Category | ||
| ---|---|---|--- | ||
| tengu_onyx_plover| false| Auto-Dream 记忆整理触发器(minHours/minSessions 配置)| Memory | ||
| tengu_passport_quail| false| Memory passport — 记忆跨项目传递| Memory | ||
| tengu_slate_thimble| false| Memory 组织优化| Memory | ||
| tengu_coral_fern| false| Memory 管理增强| Memory | ||
| tengu_herring_clock| false| Session memory 持久化| Memory | ||
| tengu_session_memory| false| 显式 session memory 开关| Memory | ||
| tengu_bramble_lintel| null| Memory extraction 配置(N 轮触发一次)| Memory | ||
| tengu_cobalt_raccoon| false| Reactive compaction 策略| Context | ||
| tengu_slate_prism| varies| UI/display 优化| UI | ||
| tengu_hive_evidence| false| Evidence 收集系统| Quality | ||
| tengu_chomp_inflection| false| Prompt suggestion 弹出| UX | ||
| tengu_birch_trellis| true| Bash permission 默认值| Permission | ||
| tengu_harbor| false| Channel allowlist(Kairos 渠道白名单)| Kairos | ||
| tengu_harbor_permissions| false| Channel 权限控制| Kairos | ||
| tengu_cobalt_harbor| false| Harbor 变体| Kairos | ||
| tengu_bridge_repl_v2| false| Bridge REPL v2 协议| Bridge | ||
| tengu_bridge_system_init| false| Bridge 系统初始化流程| Bridge | ||
| tengu_remote_backend| false| Remote TUI backend| Remote | ||
| tengu_quartz_lantern| false| File operation 优化| Perf | ||
| tengu_cobalt_lantern| false| Remote operation 变体| Remote | ||
| tengu_ccr_bridge| false| CCR bridge 连接| Remote | ||
| tengu_ccr_mirror| false| CCR 镜像模式| Remote | ||
| tengu_strap_foyer| false| Settings sync — 云端同步设置| Sync | ||
| tengu_auto_background_agents| false| 自动后台 agent 执行| Agent | ||
| tengu_surreal_dali| false| Remote agent scheduling(RemoteTriggerTool gate)| Agent | ||
| tengu_cicada_nap_ms| 0| 后台刷新节流(ms)| Perf | ||
| tengu_ultraplan_model| Opus 4.6| UltraPlan 使用的模型选择| Model | ||
| tengu_amber_stoat| true| Built-in agent 可用性| Agent | ||
| tengu_slim_subagent_claudemd| true| Subagent ClaudeMd 精简版| Agent | ||
| tengu_agent_list_attach| false| Agent list 附加到消息| Agent | ||
| tengu_amber_flint| true| Agent swarm 启用| Swarm | ||
| tengu_terminal_panel| false| Terminal panel (runtime gate)| UI | ||
| tengu_terminal_sidebar| false| Terminal sidebar 显示| UI | ||
| tengu_willow_mode| 'off'| Willow mode 配置| Mode | ||
| tengu_destructive_command_warning| false| 破坏性命令警告| Safety | ||
| tengu_glacier_2xr| false| Tool search 变体| Search | ||
| tengu_lapis_finch| false| Plugin hint 推荐| Plugin | ||
| tengu_amber_quartz_disabled| false| Voice mode 禁用开关| Voice | ||
| tengu_collage_kaleidoscope| true| Image paste 处理| Media | ||
| tengu_marble_fox| false| File persistence 变体| Storage | ||
| tengu_marble_sandcastle| false| Fast mode 优化| Perf | ||
| tengu_pebble_leaf_prune| false| Session storage 修剪| Storage | ||
| tengu_chrome_auto_enable| false| Chrome MCP 自动启用| Chrome | ||
| tengu_copper_bridge| false| Chrome MCP bridge| Chrome | ||
| tengu_lodestone_enabled| false| Lodestone feature gate| Exp | ||
| tengu_miraculo_the_bard| false| 后台服务(background service)| Service | ||
| tengu_amber_json_tools| false| JSON tool 变体| Tool | ||
| tengu_copper_panda| false| Skill improvement 追踪| Skill | ||
| tengu_immediate_model_command| false| 即时模型切换| Model | ||
| tengu_moth_copse| false| Attachment 处理| Media | ||
| tengu_basalt_3kr| false| MCP instructions delta| MCP | ||
| tengu_trace_lantern| false| Telemetry tracing| Telemetry | ||
| tengu_fgts| false| Fine-grained tool streaming| Streaming | ||
| tengu_turtle_carbon| true| Thinking mode 启用| Thinking | ||
| tengu_otk_slot_v1| false| OTK slot 变体| Auth | ||
| tengu_kairos_brief| false| Brief mode (runtime gate)| Kairos | ||
| tengu_jade_anvil_4| false| Rate limit 选项| API | ||
| tengu_attribution_header| true| 系统 attribution headers| System | ||
| tengu_1p_event_batch_config| —| First-party event 批处理配置| Telemetry | ||
| Environment Variables (102 CLAUDE_CODE_*) | ||
| 运行时配置,通过环境变量控制。覆盖 API provider、debug 模式、UI 行为、安全策略。 | ||
| Variable| Description| Category | ||
| ---|---|--- | ||
| USE_BEDROCK| 使用 AWS Bedrock 替代 Claude API| Provider | ||
| USE_VERTEX| 使用 Google Vertex AI| Provider | ||
| USE_FOUNDRY| 使用 Google Foundry| Provider | ||
| SKIP_BEDROCK_AUTH| 跳过 Bedrock 认证| Provider | ||
| SKIP_VERTEX_AUTH| 跳过 Vertex 认证| Provider | ||
| REMOTE| 远程执行模式 (CCR)| Remote | ||
| ACTION| Action mode 执行| Mode | ||
| BRIEF| Brief mode 输出| Mode | ||
| PROACTIVE| Proactive mode 覆写| Mode | ||
| SIMPLE| Simple/轻量模式| Mode | ||
| COORDINATOR_MODE| Coordinator agent mode 覆写| Mode | ||
| DISABLE_THINKING| 禁用 extended thinking| Thinking | ||
| DISABLE_ADAPTIVE_THINKING| 禁用 adaptive thinking| Thinking | ||
| DISABLE_FAST_MODE| 禁用 fast mode| Model | ||
| DISABLE_AUTO_MEMORY| 禁用自动记忆提取| Memory | ||
| DISABLE_CLAUDE_MDS| 禁用 CLAUDE.md 加载| Context | ||
| DISABLE_1M_CONTEXT| 禁用 1M context window| Context | ||
| DISABLE_GIT_INSTRUCTIONS| 禁用 git instructions| Context | ||
| DISABLE_BACKGROUND_TASKS| 禁用后台任务执行| Agent | ||
| DISABLE_CRON| 禁用 cron tool| Agent | ||
| DISABLE_COMMAND_INJECTION_CHECK| 禁用注入检测| Security | ||
| DISABLE_FILE_CHECKPOINTING| 禁用文件 checkpointing| Storage | ||
| DISABLE_ATTACHMENTS| 禁用文件附件| Media | ||
| DISABLE_TERMINAL_TITLE| 禁用终端标题更新| UI | ||
| DISABLE_MOUSE| 禁用鼠标支持| UI | ||
| ACCESSIBILITY| 无障碍模式| UI | ||
| NO_FLICKER| 减少视觉闪烁| UI | ||
| STREAMLINED_OUTPUT| 精简输出格式| UI | ||
| FORCE_FULL_LOGO| 始终显示完整 logo| UI | ||
| BASH_SANDBOX_SHOW_INDICATOR| 显示 sandbox 指示器| UI | ||
| BUBBLEWRAP| Bubblewrap sandbox| Sandbox | ||
| USE_POWERSHELL_TOOL| 用 PowerShell 替代 Bash| Shell | ||
| TWO_STAGE_CLASSIFIER| 两阶段 bash 分类| Permission | ||
| ENABLE_TELEMETRY| 启用 telemetry| Telemetry | ||
| ENABLE_SDK_FILE_CHECKPOINTING| SDK-based file checkpointing| Storage | ||
| ENABLE_CFC| Claude in Chrome 支持| Chrome | ||
| ENABLE_FINE_GRAINED_TOOL_STREAMING| 细粒度 tool streaming| Streaming | ||
| ENABLE_TASKS| 启用 task list| Task | ||
| ENABLE_XAA| XAA/IDP login| Auth | ||
| EXPERIMENTAL_AGENT_TEAMS| Agent team swarms| Swarm | ||
| PLAN_MODE_REQUIRED| 强制 plan mode| Mode | ||
| VERIFY_PLAN| 验证 plan 执行| Mode | ||
| UNATTENDED_RETRY| 无人值守重试覆写| Retry | ||
| UNDERCOVER| Undercover/隐身模式| Debug | ||
| DEBUG_REPAINTS| Debug Ink 组件重绘| Debug | ||
| PROFILE_STARTUP| Profile 启动性能| Debug | ||
| PROFILE_QUERY| Profile query 执行| Debug | ||
| DUMP_AUTO_MODE| Dump auto-mode 决策| Debug | ||
| DUMP_SYSTEM_PROMPT| Dump system prompt| Debug | ||
| TERMINAL_RECORDING| 终端录制| Debug | ||
| SAVE_HOOK_ADDITIONAL_CONTEXT| 保存 hook 额外 context| Debug | ||
| DISABLE_NONSTREAMING_FALLBACK| 禁用非 streaming fallback| API | ||
| SKIP_FAST_MODE_NETWORK_ERRORS| Fast mode 忽略网络错误| API | ||
| DISABLE_EXPERIMENTAL_BETAS| 禁用实验性 beta| Feature | ||
| DISABLE_FEEDBACK_SURVEY| 禁用反馈调查| UI | ||
| DISABLE_ADVISOR_TOOL| 禁用 advisor tool| Tool | ||
| DISABLE_POLICY_SKILLS| 禁用 policy-based skills| Skill | ||
| SYNC_PLUGIN_INSTALL| 同步 plugin 安装| Plugin | ||
| DISABLE_PRECOMPACT_SKIP| 禁用 precompact 跳过优化| Context | ||
| DISABLE_NONESSENTIAL_TRAFFIC| 减少非必要网络流量| Network | ||
| SUBPROCESS_ENV_SCRUB| 清理子进程环境变量| Security | ||
| PROVIDER_MANAGED_BY_HOST| Provider 由外部管理| Provider | ||
| DISABLE_LEGACY_MODEL_REMAP| 保留 legacy model 命名| Model | ||
| MCP_INSTR_DELTA| MCP instructions delta| MCP | ||
| USE_NATIVE_FILE_SEARCH| 原生文件搜索| Search | ||
| USE_CCR_V2| CCR V2 传输协议| Remote | ||
| AUTO_CONNECT_IDE| 自动连接 IDE| IDE | ||
| IDE_SKIP_VALID_CHECK| 跳过 IDE 验证| IDE | ||
| IDE_SKIP_AUTO_INSTALL| 跳过 IDE 自动安装| IDE | ||
| GB_BASE_URL| GrowthBook API base URL| Infra | ||
| HOST_PLATFORM| 覆写 host platform| Infra | ||
| ENTRYPOINT| 入口类型 (local-agent 等)| Infra | ||
| CONTAINER_ID| Container ID 追踪| Infra | ||
| REMOTE_SESSION_ID| Remote session ID| Infra | ||
| TAGS| Session tags| Infra | ||
| COWORKER_TYPE| Coworker 类型分类| Infra | ||
| REMOTE_ENVIRONMENT_TYPE| 远程环境分类| Infra | ||
| PROXY_RESOLVES_HOSTS| Proxy 解析主机名| Network | ||
| PLUGIN_USE_ZIP_CACHE| Plugin zip cache| Plugin | ||
| ADDITIONAL_DIRECTORIES_CLAUDE_MD| 额外目录扫描| Context | ||
| EXIT_AFTER_FIRST_RENDER| 首次渲染后退出 (testing)| Test | ||
| INCLUDE_PARTIAL_MESSAGES| Telemetry 包含 partial messages| Telemetry | ||
| EMIT_SESSION_STATE_EVENTS| Emit session state events| Event | ||
| SKIP_PROMPT_HISTORY| 跳过 prompt history| History | ||
| REMOTE_SEND_KEEPALIVES| Remote keepalive 消息| Remote | ||
| AGENT_LIST_IN_MESSAGES| 消息中列出 agents| Agent | ||
| ALWAYS_ENABLE_EFFORT| 始终启用 effort mode| Mode | ||
| EAGERLY_FLUSH| Eager state flushing| Perf | ||
| CCR_MIRROR| CCR mirror 覆写| Remote | ||
| NEW_INIT| 新初始化流程覆写| Init | ||
| BRIEF_UPLOAD| Brief mode upload| Mode | ||
| IS_COWORK| Cowork 环境标记| Infra | ||
| USE_COWORK_PLUGINS| 使用 cowork plugins| Plugin | ||
| DISABLE_VIRTUAL_SCROLL| 禁用虚拟滚动| UI | ||
| DISABLE_MESSAGE_ACTIONS| 禁用消息动作菜单| UI | ||
| DISABLE_MOUSE_CLICKS| 禁用鼠标点击| UI | ||
| 所有变量前缀 `CLAUDE_CODE_` 已省略。完整变量名 = `CLAUDE_CODE_` \+ 表中名称。 | ||
| 基于 1,902 个 src 文件、512,685 行源码的 snapshot 发现页 · Revised 2026-04-04 | ||
| [← Back to Architecture Learnings Index](index.html) | ||
| > `*` = feature-gated `†` = internal |
+305
-164
@@ -1,236 +0,377 @@ | ||
| [← Back to Index](index.html) | ||
| # 01. Agent Spawning & Orchestration | ||
| # 01\. Agent Spawning & Orchestration Critical | ||
| > `AgentTool` — foreground / async / fork / teammate / remote five-path dispatch. | ||
| AgentTool 的 foreground / async / fork / teammate / remote 五路分发 | ||
| ## Key Files | ||
| R | ||
| ``` | ||
| src/tools/AgentTool/AgentTool.tsx — main tool (82-1100) | ||
| src/tools/AgentTool/forkSubagent.ts — cache-safe fork (107-169) | ||
| src/tools/AgentTool/agentToolUtils.ts — async lifecycle (508-686) | ||
| src/tools/AgentTool/builtInAgents.ts — agent type definitions | ||
| src/tools/AgentTool/runAgent.ts — agent execution entry | ||
| src/tools/AgentTool/constants.ts — type name constants | ||
| src/tasks/LocalAgentTask/LocalAgentTask.tsx — async registration (466-515) | ||
| src/utils/forkedAgent.ts — context isolation (345-462) | ||
| src/utils/agentContext.ts — AsyncLocalStorage (24-109) | ||
| src/utils/worktree.ts — worktree isolation | ||
| src/utils/messageQueueManager.ts — notification queue + priority | ||
| src/coordinator/coordinatorMode.ts — coordinator prompt (111-336) | ||
| src/constants/tools.ts — allowed tools list (36-112) | ||
| ``` | ||
| ## Feature Reference [→ Full Flag Index](00-feature-discovery.html#flags-build) | ||
| --- | ||
| ### AgentTool Input Schema | ||
| ## AgentTool Input Schema (`src/tools/AgentTool/AgentTool.tsx:82-125`) | ||
| // src/tools/AgentTool/AgentTool.tsx:82-125 { description: string (required) // 3-5 word task description prompt: string (required) // task for the agent subagent_type: string (optional) // agent specialization model: 'sonnet'|'opus'|'haiku' (optional) run_in_background: boolean (optional) // async execution isolation: 'worktree'|'remote' (optional) cwd: string (optional) // KAIROS-only name: string (optional) // addressable via SendMessage team_name: string (optional) // swarm team mode: string (optional) // e.g. "plan" } | ||
| ```typescript | ||
| { | ||
| description: string (required) // 3-5 word task description | ||
| prompt: string (required) // task for the agent | ||
| subagent_type: string (optional) // agent specialization | ||
| model: 'sonnet'|'opus'|'haiku' (optional) | ||
| run_in_background: boolean (optional) // async execution | ||
| isolation: 'worktree'|'remote' (optional) | ||
| cwd: string (optional) // KAIROS-only | ||
| name: string (optional) // addressable via SendMessage | ||
| team_name: string (optional) // swarm team | ||
| mode: string (optional) // e.g. "plan" | ||
| } | ||
| ``` | ||
| ### Built-in Agents + Special Spawn Modes | ||
| ## AgentTool Output Schema (3 return types) | ||
| Standard built-ins (conditional): general-purpose — default Explore — codebase exploration Plan — implementation planning Verification — verify changes claude-code-guide — Claude Code Q&A; StatuslineSetup — statusline config Special spawn paths: fork — implicit fork mode when subagent_type omitted + gate on worker — coordinator-mode worker type 注意: fork 不是 builtInAgents.ts 注册出来的普通 built-in worker 也不是常驻 built-in list;它来自 coordinator agent surface | ||
| ```typescript | ||
| // Synchronous — sync spawn completes | ||
| { status: 'completed', prompt: string, data: string } | ||
| ### Feature Flags | ||
| // Asynchronous — background spawn returns immediately | ||
| { status: 'async_launched', agentId: string, description: string, | ||
| outputFile: string, canReadOutputFile: boolean } | ||
| `FORK_SUBAGENT`| Cache-safe fork + /fork command | ||
| ---|--- | ||
| `COORDINATOR_MODE`| Orchestrate workers, task-notification XML | ||
| `BUILTIN_EXPLORE_PLAN_AGENTS`| Explore + Plan built-in agents | ||
| `VERIFICATION_AGENT`| Verification agent type | ||
| `WORKFLOW_SCRIPTS`| Controls if workflow tool blocked for agents | ||
| ### GrowthBook Flags | ||
| // Internal (Teammates / Remote) | ||
| { status: 'teammate_spawned' | 'remote_launched' } | ||
| ``` | ||
| `tengu_amber_stoat` (true)| Explore/Plan agent availability gate | ||
| ---|--- | ||
| `tengu_slim_subagent_claudemd` (true)| Sub-agent system prompt variant kill-switch | ||
| `tengu_auto_background_agents` (false)| Auto-background after 120s | ||
| `tengu_hive_evidence` (false)| Verification agent availability | ||
| `tengu_agent_list_attach` (false)| Agent list in messages vs prompt | ||
| ### Env Vars & Async Agent Allowed Tools | ||
| --- | ||
| // Env vars CLAUDE_CODE_DISABLE_BACKGROUND_TASKS — block run_in_background CLAUDE_AUTO_BACKGROUND_TASKS — auto-background after 120s CLAUDE_CODE_COORDINATOR_MODE — activate coordinator CLAUDE_CODE_SIMPLE — limit worker tools to Bash/Read/Edit // Async agent ALLOWED tools (src/constants/tools.ts:55-71) Read, Bash, PowerShell, Edit, Write, NotebookEdit, Grep, Glob, WebSearch, WebFetch, Skill, SyntheticOutput, ToolSearch, EnterWorktree, ExitWorktree // BLOCKED for async (prevent recursion/blocking) Agent, TaskOutput, ExitPlanMode, TaskStop, AskUserQuestion | ||
| ## Built-in Agents | ||
| ### Key Files | ||
| ``` | ||
| Standard built-ins (conditional): | ||
| general-purpose — default | ||
| Explore — codebase exploration | ||
| Plan — implementation planning | ||
| Verification — verify changes | ||
| claude-code-guide — Claude Code Q&A | ||
| StatuslineSetup — statusline config | ||
| src/tools/AgentTool/AgentTool.tsx — main tool (82-1100) src/tools/AgentTool/forkSubagent.ts — cache-safe fork (107-169) src/tools/AgentTool/agentToolUtils.ts — async lifecycle (508-686) src/tools/AgentTool/builtInAgents.ts — agent type definitions src/tools/AgentTool/runAgent.ts — agent execution entry src/tools/AgentTool/constants.ts — type name constants src/tasks/LocalAgentTask/LocalAgentTask.tsx — async registration (466-515) src/utils/forkedAgent.ts — context isolation (345-462) src/utils/agentContext.ts — AsyncLocalStorage (24-109) src/utils/worktree.ts — worktree isolation src/utils/messageQueueManager.ts — notification queue + priority src/coordinator/coordinatorMode.ts — coordinator prompt (111-336) src/constants/tools.ts — allowed tools list (36-112) | ||
| Special spawn paths: | ||
| fork — implicit when subagent_type omitted + FORK_SUBAGENT gate on | ||
| worker — coordinator-mode worker type (not in builtInAgents.ts) | ||
| ``` | ||
| ### Evidence Boundary | ||
| --- | ||
| src/tools/AgentTool/AgentTool.tsx:82-100 证明输入 schema;src/tools/AgentTool/AgentTool.tsx:319-322 证明“省略 `subagent_type` → fork path” 逻辑;src/tools/AgentTool/builtInAgents.ts:22-68 证明标准 built-ins 的真实来源;src/utils/forkedAgent.ts:57-80 证明 CacheSafeParams 与保存/读取 cache-safe prefix 的机制。 | ||
| ## Feature Flags | ||
| ### AgentTool Output Schema (3 种返回) | ||
| | Flag | Description | | ||
| |---|---| | ||
| | `FORK_SUBAGENT` | Cache-safe fork + /fork command | | ||
| | `COORDINATOR_MODE` | Orchestrate workers, task-notification XML | | ||
| | `BUILTIN_EXPLORE_PLAN_AGENTS` | Explore + Plan built-in agents | | ||
| | `VERIFICATION_AGENT` | Verification agent type | | ||
| | `WORKFLOW_SCRIPTS` | Controls if workflow tool is blocked for agents | | ||
| // Synchronous — sync spawn 完成后返回 { status: 'completed', prompt: string, data: string } // Asynchronous — background spawn 立即返回 { status: 'async_launched', agentId: string, description: string, outputFile: string, canReadOutputFile: boolean } // Internal (Teammates / Remote) { status: 'teammate_spawned' | 'remote_launched' } | ||
| ## GrowthBook Flags | ||
| ### AgentTool.call() 决策树 (L315-650) | ||
| | Flag | Default | Description | | ||
| |---|---|---| | ||
| | `tengu_amber_stoat` | true | Explore/Plan agent availability gate | | ||
| | `tengu_slim_subagent_claudemd` | true | Sub-agent system prompt variant kill-switch | | ||
| | `tengu_auto_background_agents` | false | Auto-background after 120s | | ||
| | `tengu_hive_evidence` | false | Verification agent availability | | ||
| | `tengu_agent_list_attach` | false | Agent list in messages vs prompt | | ||
| AgentTool.call(input) │ ├─ subagent_type 未指定 && FORK_SUBAGENT enabled? │ └─ YES → Fork Path (cache-safe, permissionMode='bubble') │ ├─ team_name + name 同时存在? │ └─ YES → Teammate Path → spawnTeammate() │ ├─ isolation === 'remote'? │ └─ YES → Remote CCR Path → registerRemoteTask() │ ├─ run_in_background === true || agent.background === true? │ └─ YES → Async Path → registerAsyncAgent() + fire-and-forget │ └─ DEFAULT → Foreground Path → registerAgentForeground() + sync wait // Agent 选择逻辑: effectiveType = subagent_type ?? (isForkSubagentEnabled() ? undefined : 'general-purpose') // undefined → fork, 否则查 built-in + custom agents | ||
| ## Env Vars & Async Agent Allowed Tools | ||
| ### CacheSafeParams — Prompt Cache 共享的核心 | ||
| ``` | ||
| // Env vars | ||
| CLAUDE_CODE_DISABLE_BACKGROUND_TASKS — block run_in_background | ||
| CLAUDE_AUTO_BACKGROUND_TASKS — auto-background after 120s | ||
| CLAUDE_CODE_COORDINATOR_MODE — activate coordinator | ||
| CLAUDE_CODE_SIMPLE — limit worker tools to Bash/Read/Edit | ||
| Anthropic API cache key = `(system_prompt, tools, model, messages_prefix, thinking_config)`。这 5 个必须 **byte-identical** 才能 cache hit。 | ||
| // Async agent ALLOWED tools (src/constants/tools.ts:55-71) | ||
| Read, Bash, PowerShell, Edit, Write, NotebookEdit, | ||
| Grep, Glob, WebSearch, WebFetch, Skill, | ||
| SyntheticOutput, ToolSearch, EnterWorktree, ExitWorktree | ||
| type CacheSafeParams = { systemPrompt: SystemPrompt // 必须完全匹配 parent userContext: { [k: string]: string } // prepended to messages systemContext: { [k: string]: string } // appended to system toolUseContext: ToolUseContext // tools, model, options forkContextMessages: Message[] // parent 的 message prefix } // 模块级 slot — 存储上一轮的 params let lastCacheSafeParams: CacheSafeParams | undefined // Parent turn 完成后 saveCacheSafeParams() // Fork 通过 getLastCacheSafeParams() 读取(不重新计算) // 所有 fork children 共享这个 prefix → API cache hit rate 极高 // Cache 失效条件: // - maxOutputTokens override → 改变 budget_tokens // - 不同 thinking config // - 不同 tools list // - 不同 system prompt | ||
| // BLOCKED for async (prevent recursion/blocking) | ||
| Agent, TaskOutput, ExitPlanMode, TaskStop, AskUserQuestion | ||
| ``` | ||
| ### createSubagentContext() — 精确的 Clone vs Share | ||
| --- | ||
| // src/utils/forkedAgent.ts:345 createSubagentContext(parentContext, overrides?): // CLONE (防污染): readFileState → cloneFileStateCache() // 防 cache 冲突 toolDecisions → undefined // 重置决策缓存 contentReplacement→ clone or fresh // 防 budget 干扰 discoveredSkillNames → new Set() // 独立 skill 发现 nestedMemoryAttachmentTriggers → new Set() // SHARE (性能优化): abortController → createChildAbortController(parent) // 单向级联 setAppStateForTasks → root reference // shell task 需要 // NO-OP (防副作用): setAppState → () => {} // 子不能改父 UI // (除非 shareSetAppState = true) // NEW (独立追踪): agentId → createAgentId() // 新 UUID queryTracking → { chainId: randomUUID(), depth: parent.depth + 1 } // SubagentContextOverrides 控制: { shareSetAppState: boolean // 允许状态修改 shareSetResponseLength: boolean // 贡献到父的 metrics shareAbortController: boolean // 联合 abort contentReplacementState: ... // resumeAgentBackground 用 } | ||
| ## Decision Tree: `AgentTool.call()` (L315-650) | ||
| ### LocalAgentTaskState — 异步 Agent 的完整状态机 | ||
| ``` | ||
| AgentTool.call(input) | ||
| │ | ||
| ├─ subagent_type unspecified && FORK_SUBAGENT enabled? | ||
| │ └─ YES → Fork Path (cache-safe, permissionMode='bubble') | ||
| │ | ||
| ├─ team_name + name both present? | ||
| │ └─ YES → Teammate Path → spawnTeammate() | ||
| │ | ||
| ├─ isolation === 'remote'? | ||
| │ └─ YES → Remote CCR Path → registerRemoteTask() | ||
| │ | ||
| ├─ run_in_background === true || agent.background === true? | ||
| │ └─ YES → Async Path → registerAsyncAgent() + fire-and-forget | ||
| │ | ||
| └─ DEFAULT → Foreground Path → registerAgentForeground() + sync wait | ||
| type LocalAgentTaskState = TaskStateBase & { type: 'local_agent' agentId: string prompt: string selectedAgent?: AgentDefinition agentType: string // e.g. 'general-purpose' model?: string abortController?: AbortController error?: string result?: AgentToolResult progress?: AgentProgress // { tokenCount, toolUseCount, summary } retrieved: boolean // 结果是否已被父读取 messages?: Message[] // Sidechain JSONL (磁盘) lastReportedToolCount: number lastReportedTokenCount: number isBackgrounded: boolean // true = 不显示在前台 UI pendingMessages: string[] // SendMessage 队列 retain: boolean // UI 正在持有此 task diskLoaded: boolean // 从磁盘恢复的 bootstrap evictAfter?: number // GC deadline (timestamp) } // 状态流转: // running → completed/failed/killed // foreground → isBackgrounded (auto-background timer) // !retrieved → retrieved (parent 读结果后标记) | ||
| // Agent selection logic: | ||
| effectiveType = subagent_type | ||
| ?? (isForkSubagentEnabled() ? undefined : 'general-purpose') | ||
| // undefined → fork, otherwise look up built-in + custom agents | ||
| ``` | ||
| ### 消息队列优先级系统 (messageQueueManager.ts) | ||
| --- | ||
| // 模块级队列 const commandQueue: QueuedCommand[] = [] // 优先级 (数字越小越先出队): const PRIORITY_ORDER = { now: 0, // 立即处理 next: 1, // 下一个 (用户输入默认) later: 2 // 稍后 (task-notification 默认) } // 入队操作: enqueue(command) // priority 默认 'next' enqueuePendingNotification() // priority 默认 'later' // → later 保证用户输入永远优先于 notification // 出队操作(带过滤): dequeue(filter?) // FIFO within priority // Main thread: dequeue(cmd => cmd.agentId === undefined) // Agent: dequeue(cmd => cmd.agentId === myId) // React 订阅: subscribeToCommandQueue → queueChanged signal getCommandQueueSnapshot → freeze([...queue]) | ||
| ## CacheSafeParams — Prompt Cache Sharing | ||
| ### 完整 Spawn-to-Completion Flow | ||
| Anthropic API cache key = `(system_prompt, tools, model, messages_prefix, thinking_config)`. All 5 must be **byte-identical** for a cache hit. | ||
| ═══ Synchronous (inline wait) ═══ AgentTool.call() ├─ resolveAgentDefinition(subagent_type) ├─ registerAgentForeground() → task + backgroundSignal ├─ Stream: runAgent → query() loop yields messages │ └─ 结果实时显示在前台 UI ├─ Return { status:'completed', data } └─ Task 保留在 AppState (history) ═══ Asynchronous (background) ═══ AgentTool.call() ├─ resolveAgentDefinition(subagent_type) ├─ registerAsyncAgent() → task backgrounded ├─ 🔥 fire-and-forget: runAsyncAgentLifecycle() │ ├─ Stream messages from query() loop │ ├─ Record to sidechain transcript (disk JSONL) │ ├─ updateAsyncAgentProgress() per message │ ├─ finalizeAgentTool() → extract result │ ├─ completeAsyncAgent() → status='completed' │ └─ enqueueAgentNotification() → XML to queue ├─ Return { status:'async_launched', agentId } └─ Main loop dequeues notification → LLM processes ═══ Fork (cache-safe) ═══ runForkedAgent() ├─ createSubagentContext() → cloned context ├─ query() with BYTE-IDENTICAL cacheSafeParams │ ├─ system + tools + model + messages prefix │ └─ 只有最后的 promptMessages 不同 → cache hit ├─ Record sidechain transcript (agentId-tagged) ├─ Log tengu_fork_agent_query analytics └─ Return messages + totalUsage ═══ Foreground → Background 转换 ═══ registerAgentForeground() ├─ isBackgrounded: false (initially) ├─ Creates backgroundSignal promise ├─ Auto-background timer (tengu_auto_background_agents) │ └─ 120s 后自动 → isBackgrounded: true └─ backgroundSignalResolvers.get(agentId) → resolve() | ||
| ```typescript | ||
| type CacheSafeParams = { | ||
| systemPrompt: SystemPrompt // must match parent exactly | ||
| userContext: { [k: string]: string } // prepended to messages | ||
| systemContext: { [k: string]: string } // appended to system | ||
| toolUseContext: ToolUseContext // tools, model, options | ||
| forkContextMessages: Message[] // parent's message prefix | ||
| } | ||
| ### Depth 追踪 — 无硬限制,纯分析 | ||
| // Module-level slot — stores params from last turn | ||
| let lastCacheSafeParams: CacheSafeParams | undefined | ||
| type QueryChainTracking = { chainId: string // UUID per fork chain depth: number // 0=main, 1=child, 2=grandchild... } // Depth 仅用于 analytics,不做硬限制 // 记录在 API logging: query_chain_id, query_depth // 真正的 "depth limit" = abort signal chain // → parent abort → 整棵子树全部 cascade abort | ||
| // Parent turn completes → saveCacheSafeParams() | ||
| // Fork reads via getLastCacheSafeParams() (no recomputation) | ||
| // All fork children share this prefix → high API cache hit rate | ||
| ### task-notification XML 完整格式 | ||
| // Cache invalidation conditions: | ||
| // - maxOutputTokens override → changes budget_tokens | ||
| // - Different thinking config | ||
| // - Different tools list | ||
| // - Different system prompt | ||
| ``` | ||
| <task-notification> <task-id>{agentId}</task-id> <tool-use-id>{toolUseId}</tool-use-id> <!-- optional --> <output-file>{outputPath}</output-file> <status>completed|failed|killed</status> <summary>{agent summary}</summary> <result>{final text response}</result> <!-- optional --> <usage> <total_tokens>N</total_tokens> <tool_uses>N</tool_uses> <duration_ms>N</duration_ms> </usage> <worktree-path>...</worktree-path> <!-- optional --> <worktree-branch>...</worktree-branch> <!-- optional --> </task-notification> // 双重防护: atomically check notified flag (prevent duplicates) // enqueuePendingNotification({ mode: 'task-notification' }) // 优先级 'later' → 用户输入永远优先 | ||
| --- | ||
| ### Coordinator Mode 内部机制 | ||
| ## `createSubagentContext()` — Clone vs Share (`src/utils/forkedAgent.ts:345`) | ||
| // 启用条件: isCoordinatorMode() = feature('COORDINATOR_MODE') && isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) // Coordinator 专属: // - 不同的 system prompt (描述 worker 为自主执行器) // - 只有 coordinator 能 spawn workers (via Agent tool) // - Workers 不能 spawn (flat roster) // - Coordinator 可 SendMessage 继续 workers // - workerToolsContext 注入 user context // Worker 工具集 = ASYNC_AGENT_ALLOWED_TOOLS // 减去: TEAM_CREATE, TEAM_DELETE, SendMessage, StructuredOutput // 加上: Bash, FileRead, FileEdit, MCP tools | ||
| ```typescript | ||
| createSubagentContext(parentContext, overrides?): | ||
| I | ||
| // CLONE (prevent contamination): | ||
| readFileState → cloneFileStateCache() | ||
| toolDecisions → undefined (reset decision cache) | ||
| contentReplacement → clone or fresh | ||
| discoveredSkillNames → new Set() | ||
| nestedMemoryAttachmentTriggers → new Set() | ||
| ## Comparison | ||
| // SHARE (performance): | ||
| abortController → createChildAbortController(parent) // one-way cascade | ||
| setAppStateForTasks → root reference // shell task needs it | ||
| Claude Code | ||
| // NO-OP (prevent side effects): | ||
| setAppState → () => {} // child cannot modify parent UI | ||
| // (unless shareSetAppState = true) | ||
| * **AgentTool** — 统一入口,支持 sync / async / fork 三种模式 | ||
| * **Context isolation** — createSubagentContext() 精确 clone vs share:readFileState clone、setAppState no-op、abort 单向级联 | ||
| * **Worktree isolation** — git worktree per agent,完全独立的文件系统视图 | ||
| * **Cache-safe fork** — 所有 fork children 共享 prompt prefix,API cache hit rate 极高 | ||
| * **Notification callback** — async agent 完成后注入 <task-notification> XML message 到父 thread | ||
| * **Abort chain** — parent → child cascade,child 不影响 parent | ||
| * **Coordinator mode** — 多 worker 并发,结果通过 task-notification 汇总 | ||
| * **AsyncLocalStorage** — 上下文自动传播到所有异步操作 | ||
| // NEW (independent tracking): | ||
| agentId → createAgentId() // new UUID | ||
| queryTracking → { chainId: randomUUID(), depth: parent.depth + 1 } | ||
| Mitsein | ||
| // SubagentContextOverrides: | ||
| { | ||
| shareSetAppState: boolean // allow state mutations | ||
| shareSetResponseLength: boolean // contribute to parent metrics | ||
| shareAbortController: boolean // joint abort | ||
| contentReplacementState: ... // used by resumeAgentBackground | ||
| } | ||
| ``` | ||
| * **spawn.py** — foreground (sync block) + background (asyncio.Task),两种模式 | ||
| * **registry.py** — SubagentRegistry 内存追踪 active subagents | ||
| * Background 模式无回调,完成后无通知机制 | ||
| * 无 context isolation — 子 agent 可能读写父级状态 | ||
| * 无 worktree isolation | ||
| * kill_subagent 是 registry 级别操作,非 signal 级联 | ||
| * spawn_agent / launch_agent 在 v4 设计中已定义,但回调机制待实现 | ||
| * Client tool (__client_tool__) 模式可复用于 launch_agent | ||
| --- | ||
| Feature | Claude Code | Mitsein | ||
| ---|---|--- | ||
| Sync spawn| ✓| ✓ | ||
| Async spawn| ✓| partial | ||
| Async callback| ✓ notification msg| ✗ | ||
| Context isolation| ✓ clone/share| ✗ | ||
| Abort cascade| ✓ signal chain| ✗ | ||
| Cache-safe fork| ✓ prefix sharing| ✗ | ||
| Launch → UI handoff| ✓ client tool| __client_tool__ exists | ||
| Depth tracking| ✓ queryTracking.depth| ✓ max_spawn_depth | ||
| II | ||
| ## `LocalAgentTaskState` — Async Agent State Machine | ||
| ## How to Implement | ||
| ```typescript | ||
| type LocalAgentTaskState = TaskStateBase & { | ||
| type: 'local_agent' | ||
| agentId: string | ||
| prompt: string | ||
| selectedAgent?: AgentDefinition | ||
| agentType: string // e.g. 'general-purpose' | ||
| model?: string | ||
| abortController?: AbortController | ||
| error?: string | ||
| result?: AgentToolResult | ||
| progress?: AgentProgress // { tokenCount, toolUseCount, summary } | ||
| retrieved: boolean // result read by parent? | ||
| messages?: Message[] // Sidechain JSONL (disk) | ||
| lastReportedToolCount: number | ||
| lastReportedTokenCount: number | ||
| isBackgrounded: boolean // true = not shown in foreground UI | ||
| pendingMessages: string[] // SendMessage queue | ||
| retain: boolean // UI holding this task | ||
| diskLoaded: boolean // bootstrapped from disk | ||
| evictAfter?: number // GC deadline (timestamp) | ||
| } | ||
| #### Step 1: SubagentContext dataclass | ||
| // State transitions: | ||
| // running → completed/failed/killed | ||
| // foreground → isBackgrounded (auto-background timer) | ||
| // !retrieved → retrieved (parent reads result) | ||
| ``` | ||
| 新建 mitsein/agents/context.py,定义 `SubagentContext`:clone parent 的 memory snapshot、thread metadata;设 `parent_state_writable = False`。Python 等价物用 `dataclasses.replace()` 做 shallow copy + override。 | ||
| --- | ||
| @dataclass(frozen=True) class SubagentContext: agent_config_id: str parent_thread_id: str | None memory_snapshot: dict # frozen copy at spawn time abort_event: asyncio.Event # child of parent event depth: int # parent.depth + 1 parent_state_writable: bool = False def fork_context(parent: SubagentContext, config_id: str) -> SubagentContext: child_abort = asyncio.Event() # Wire parent → child cascade (see Step 3) return SubagentContext( agent_config_id=config_id, parent_thread_id=parent.parent_thread_id, memory_snapshot=copy.deepcopy(parent.memory_snapshot), abort_event=child_abort, depth=parent.depth + 1, ) | ||
| ## Message Queue Priority System (`messageQueueManager.ts`) | ||
| #### Step 2: Notification message callback | ||
| ```typescript | ||
| // Module-level queue | ||
| const commandQueue: QueuedCommand[] = [] | ||
| background spawn 完成后,向父 thread 注入一条 system message。不需要新协议,复用现有 message 表。 | ||
| // Priority (lower = dequeued first): | ||
| const PRIORITY_ORDER = { | ||
| now: 0, // immediate | ||
| next: 1, // next (user input default) | ||
| later: 2 // later (task-notification default) | ||
| } | ||
| # spawn.py - background task wrapper async def _run_background_agent(config_id, context, parent_thread_id, task_id): try: result = await orchestrator.run(config_id, context) # Inject notification into parent thread await message_repo.create( thread_id=parent_thread_id, role="system", content={ "type": "task_notification", "task_id": task_id, "status": "completed", "summary": result.summary, "artifact_url": result.artifact_url, "token_usage": result.usage, } ) except asyncio.CancelledError: await message_repo.create( thread_id=parent_thread_id, role="system", content={"type": "task_notification", "task_id": task_id, "status": "cancelled"} ) | ||
| // Enqueue: | ||
| enqueue(command) // priority defaults to 'next' | ||
| enqueuePendingNotification() // priority defaults to 'later' | ||
| // → 'later' guarantees user input always takes priority over notifications | ||
| #### Step 3: Abort signal cascade | ||
| // Dequeue (with filter): | ||
| dequeue(filter?) // FIFO within priority | ||
| // Main thread: dequeue(cmd => cmd.agentId === undefined) | ||
| // Agent: dequeue(cmd => cmd.agentId === myId) | ||
| 用 Python `asyncio.Event` chain 实现单向级联。Parent set → 所有 children 的 listener 触发。 | ||
| // React subscription: | ||
| subscribeToCommandQueue → queueChanged signal | ||
| getCommandQueueSnapshot → freeze([...queue]) | ||
| ``` | ||
| # registry.py class SubagentRegistry: def _wire_abort_cascade(self, parent_event, child_event): async def _watcher(): await parent_event.wait() child_event.set() # cascade asyncio.create_task(_watcher()) def register(self, agent_id, parent_context): child_abort = asyncio.Event() self._wire_abort_cascade(parent_context.abort_event, child_abort) record = SubagentRecord( agent_id=agent_id, abort_event=child_abort, ... ) self._active[agent_id] = record return record def kill(self, agent_id): """Trigger abort for agent and all its children (cascade).""" record = self._active.get(agent_id) if record: record.abort_event.set() # children auto-cascade | ||
| --- | ||
| #### Step 4: Cache-safe prompt prefix | ||
| ## Complete Spawn-to-Completion Flows | ||
| 当同一轮 spawn 多个子 agent 时,共享 system prompt + parent context 前缀,只附加各自的 directive。 | ||
| ``` | ||
| ═══ Synchronous (inline wait) ═══ | ||
| AgentTool.call() | ||
| ├─ resolveAgentDefinition(subagent_type) | ||
| ├─ registerAgentForeground() → task + backgroundSignal | ||
| ├─ Stream: runAgent → query() loop yields messages | ||
| │ └─ results rendered in foreground UI | ||
| ├─ Return { status:'completed', data } | ||
| └─ Task retained in AppState (history) | ||
| # orchestrator.py class OrchestratorNext: def _build_cache_safe_prefix(self, parent_context): """Build once, share across all spawns in this turn.""" return { "system_prompt": self._render_system_prompt(parent_context), "context_messages": parent_context.memory_snapshot, "conversation_prefix": self._get_recent_messages(parent_context), } async def spawn_batch(self, spawn_requests, parent_context): prefix = self._build_cache_safe_prefix(parent_context) tasks = [] for req in spawn_requests: messages = [ *prefix["conversation_prefix"], {"role": "user", "content": req.directive}, ] tasks.append(self._run_with_prefix(prefix, messages, req)) return await asyncio.gather(*tasks) | ||
| ═══ Asynchronous (background) ═══ | ||
| AgentTool.call() | ||
| ├─ resolveAgentDefinition(subagent_type) | ||
| ├─ registerAsyncAgent() → task backgrounded | ||
| ├─ 🔥 fire-and-forget: runAsyncAgentLifecycle() | ||
| │ ├─ Stream messages from query() loop | ||
| │ ├─ Record to sidechain transcript (disk JSONL) | ||
| │ ├─ updateAsyncAgentProgress() per message | ||
| │ ├─ finalizeAgentTool() → extract result | ||
| │ ├─ completeAsyncAgent() → status='completed' | ||
| │ └─ enqueueAgentNotification() → XML to queue | ||
| ├─ Return { status:'async_launched', agentId } | ||
| └─ Main loop dequeues notification → LLM processes | ||
| #### Step 5: launch_agent as client tool | ||
| ═══ Fork (cache-safe) ═══ | ||
| runForkedAgent() | ||
| ├─ createSubagentContext() → cloned context | ||
| ├─ query() with BYTE-IDENTICAL cacheSafeParams | ||
| │ ├─ system + tools + model + messages prefix identical | ||
| │ └─ only final promptMessages differ → cache hit | ||
| ├─ Record sidechain transcript (agentId-tagged) | ||
| ├─ Log tengu_fork_agent_query analytics | ||
| └─ Return messages + totalUsage | ||
| 复用已有 `__client_tool__` 机制。launch = spawn + 通知前端打开 UI。 | ||
| ═══ Foreground → Background transition ═══ | ||
| registerAgentForeground() | ||
| ├─ isBackgrounded: false (initially) | ||
| ├─ Creates backgroundSignal promise | ||
| ├─ Auto-background timer (tengu_auto_background_agents) | ||
| │ └─ After 120s → isBackgrounded: true | ||
| └─ backgroundSignalResolvers.get(agentId) → resolve() | ||
| ``` | ||
| # tools/launch_agent.py class LaunchAgentTool: name = "launch_agent" is_client_tool = True # __client_tool__ 模式 async def execute(self, config_id, context_text, parent_context): # 1. Create thread thread = await thread_repo.create( agent_config_id=config_id, spawned_from=parent_context.parent_thread_id, ) # 2. Optional: spawn one round for pre-processing if self.config.pre_spawn: await orchestrator.run_single_turn(config_id, thread.id, context_text) # 3. Return client tool signal (frontend opens UI) return { "__client_tool__": True, "action": "open_agent", "thread_id": thread.id, "agent_config_id": config_id, "agent_name": self.config.name, } | ||
| --- | ||
| III | ||
| ## Depth Tracking — Analytics Only | ||
| ## How to Verify | ||
| ```typescript | ||
| type QueryChainTracking = { | ||
| chainId: string // UUID per fork chain | ||
| depth: number // 0=main, 1=child, 2=grandchild... | ||
| } | ||
| ### Unit Tests | ||
| // Depth is analytics-only, not enforced as a limit | ||
| // Recorded in API logging: query_chain_id, query_depth | ||
| // Real "depth limit" = abort signal chain | ||
| // → parent abort → entire subtree cascade aborts | ||
| ``` | ||
| * **test_context_isolation** — spawn 子 agent 后修改子 context 的 memory_snapshot,验证 parent 不受影响 | ||
| * **test_notification_callback** — background spawn 完成后,parent thread 中出现 task_notification message | ||
| * **test_abort_cascade** — set parent abort event,验证所有 children 的 abort event 也被 set | ||
| * **test_abort_isolation** — set child abort event,验证 parent 和 sibling 不受影响 | ||
| * **test_cache_prefix_sharing** — 同一轮 spawn 3 个子 agent,验证它们的 system_prompt 完全相同 | ||
| * **test_launch_returns_client_tool** — launch_agent 返回包含 __client_tool__ 标记的 response | ||
| * **test_depth_limit** — spawn depth 超过 max_spawn_depth 时,返回错误而非 crash | ||
| --- | ||
| ### Integration Tests | ||
| ## `task-notification` XML Format | ||
| * **test_workflow_spawn_capability** — 周报 Agent spawn PPT Agent,验证 PPT 生成完成后 notification 出现在周报 thread | ||
| * **test_companion_launch_agent** — Companion 调 launch_agent,验证前端收到 open_agent signal + 新 thread 已创建 | ||
| * **test_cancel_workflow_cascades** — 用户取消周报 Agent,验证正在运行的 PPT Agent 也被 cancel | ||
| * **test_concurrent_spawns** — 同时 spawn 3 个 capability agents,验证并发执行、所有 notifications 最终到达 | ||
| ```xml | ||
| <task-notification> | ||
| <task-id>{agentId}</task-id> | ||
| <tool-use-id>{toolUseId}</tool-use-id> <!-- optional --> | ||
| <output-file>{outputPath}</output-file> | ||
| <status>completed|failed|killed</status> | ||
| <summary>{agent summary}</summary> | ||
| <result>{final text response}</result> <!-- optional --> | ||
| <usage> | ||
| <total_tokens>N</total_tokens> | ||
| <tool_uses>N</tool_uses> | ||
| <duration_ms>N</duration_ms> | ||
| </usage> | ||
| <worktree-path>...</worktree-path> <!-- optional --> | ||
| <worktree-branch>...</worktree-branch> <!-- optional --> | ||
| </task-notification> | ||
| ### Smoke Test Scenario | ||
| // Double guard: atomically check notified flag (prevent duplicates) | ||
| // enqueuePendingNotification({ mode: 'task-notification' }) | ||
| // Priority 'later' → user input always takes precedence | ||
| ``` | ||
| Scenario: "周报 Agent spawn PPT Agent" 1\. 用户对周报 Agent 说 "帮我做本周周报" 2\. 周报 Agent 执行步骤 1-3(收集信息、整理、对话) 3\. 周报 Agent 调用 spawn_agent("ppt", "做周报PPT, 内容: ...") → 验证: 新 thread 创建在 PPT Agent 下 → 验证: 父 thread 不被 block 4\. PPT Agent 在后台完成 → 验证: 周报 thread 出现 task_notification message → 验证: notification 包含 artifact_url 5\. 周报 Agent 看到 notification,调 email_send 6\. 用户点 PPT Agent → 看到 "周报0331" thread → 可以继续修改 | ||
| --- | ||
| IV | ||
| ## Coordinator Mode Internals | ||
| ## User Perception | ||
| ```typescript | ||
| // Activation condition: | ||
| isCoordinatorMode() = | ||
| feature('COORDINATOR_MODE') && | ||
| isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) | ||
| Before (Current) | ||
| // Coordinator-specific behavior: | ||
| // - Different system prompt (describes workers as autonomous executors) | ||
| // - Only coordinator can spawn workers (via Agent tool) | ||
| // - Workers cannot spawn (flat roster) | ||
| // - Coordinator can SendMessage to continue workers | ||
| // - workerToolsContext injects user context | ||
| 用户: 帮我做本周周报 | ||
| 周报 Agent: 好的,我来整理信息... | ||
| 周报 Agent: 内容整理好了,现在生成 PPT... | ||
| [卡住 30 秒,等 PPT 生成完] | ||
| 周报 Agent: PPT 已生成。 | ||
| [用户无法在等待期间做其他事] | ||
| [如果取消,PPT 生成也没有被正确停止] | ||
| After (With Patterns) | ||
| 用户: 帮我做本周周报 | ||
| 周报 Agent: 好的,我来整理信息... | ||
| 周报 Agent: 内容整理好了,PPT 正在后台生成中... | ||
| 用户: 对了帮我也约一下明天的会 | ||
| 周报 Agent: 好的,明天几点? | ||
| [周报 Agent 不被 block,可继续对话] | ||
| 周报 Agent: ✓ PPT 已生成 → 点击查看/修改 | ||
| [用户点击 → 跳转到 PPT Agent 的 thread] | ||
| [如果取消周报 Agent,PPT 生成也自动停止] | ||
| **核心体感差异:** 从"等待 Agent 完成"变成"Agent 在后台工作,我可以继续聊天"。Companion 不被任何单个 agent 的执行 block,就像 macOS Spotlight 不会因为某个 App 卡住而无法使用。 | ||
| ### 用户可感知的变化 | ||
| * **Non-blocking spawn** — "PPT 正在生成中..." 后可以继续对话 | ||
| * **Completion notification** — "✓ PPT 已生成" 带可点击链接 | ||
| * **Cancel propagation** — 取消周报 = 取消所有子任务 | ||
| * **Artifact linkage** — PPT Agent 下能看到所有生成的 PPT(含被 spawn 的) | ||
| spawn.py registry.py orchestrator-next Phase 1 Phase 2 | ||
| // Worker toolset = ASYNC_AGENT_ALLOWED_TOOLS | ||
| // Minus: TEAM_CREATE, TEAM_DELETE, SendMessage, StructuredOutput | ||
| // Plus: Bash, FileRead, FileEdit, MCP tools | ||
| ``` |
+320
-112
@@ -1,182 +0,390 @@ | ||
| [← Back to Index](index.html) | ||
| # 02. Tool System Architecture | ||
| # 02\. Tool System Architecture Critical | ||
| ## Key Files | ||
| 从字符串列表到带类型、权限、并发声明的 execution unit | ||
| ``` | ||
| src/Tool.ts — type definitions (362-695) | ||
| src/tools.ts — registry + assembly (193-367) | ||
| src/services/tools/StreamingToolExecutor.ts — concurrency (129-519) | ||
| src/services/tools/toolExecution.ts — permission pipeline (599-1500) | ||
| src/services/tools/toolOrchestration.ts — partition + dispatch (19-177) | ||
| src/utils/toolResultStorage.ts — per-tool + per-msg budget | ||
| src/utils/toolSearch.ts — deferred tool loading | ||
| src/tools/ToolSearchTool/ToolSearchTool.ts — on-demand discovery | ||
| src/tools/MCPTool/MCPTool.ts — MCP tool template | ||
| src/services/mcp/client.ts — dynamic MCP instantiation | ||
| src/utils/toolErrors.ts — Zod error formatting | ||
| ``` | ||
| R | ||
| --- | ||
| ## Feature Reference [→ Full Tool Index](00-feature-discovery.html#tools) | ||
| ## Tool Type Signature (`src/Tool.ts:362-695`) | ||
| ### Tool Type Signature (src/Tool.ts:362-695) | ||
| ```typescript | ||
| Tool<Input, Output, Progress> = { | ||
| name: string | ||
| description(input, options): Promise<string> | ||
| inputSchema: Zod type | ||
| inputJSONSchema?: ToolInputJSONSchema | ||
| outputSchema?: z.ZodType | ||
| Tool<Input, Output, Progress> = { name: string description(input, options): Promise<string> inputSchema: Zod type inputJSONSchema?: ToolInputJSONSchema outputSchema?: z.ZodType call(args, ctx, canUseTool, parentMsg, onProgress?): Promise<ToolResult> isConcurrencySafe(input): boolean // can run parallel? isReadOnly(input): boolean // no side effects? isDestructive?(input): boolean // irreversible? isEnabled(): boolean // currently available? checkPermissions(input, ctx): Promise<PermissionResult> validateInput?(input, ctx): Promise<ValidationResult> interruptBehavior?(): 'cancel' | 'block' maxResultSizeChars: number strict?: boolean // JSON schema strict mode shouldDefer?: boolean // lazy-load schema } | ||
| call(args, ctx, canUseTool, parentMsg, onProgress?): Promise<ToolResult> | ||
| ### Concurrency Rule (StreamingToolExecutor:129-135) | ||
| isConcurrencySafe(input): boolean // can run in parallel? | ||
| isReadOnly(input): boolean // no side effects? | ||
| isDestructive?(input): boolean // irreversible? | ||
| isEnabled(): boolean // currently available? | ||
| canExecuteTool(isConcurrencySafe): executingTools.length === 0 → YES isConcurrencySafe AND all executing are safe → YES otherwise → NO (wait) Partition: [safe,safe,safe] → parallel | [unsafe] → serial | [safe,safe] → parallel | ||
| checkPermissions(input, ctx): Promise<PermissionResult> | ||
| validateInput?(input, ctx): Promise<ValidationResult> | ||
| interruptBehavior?(): 'cancel' | 'block' | ||
| ### Permission Gating Pipeline (toolExecution.ts:599-1128) | ||
| maxResultSizeChars: number | ||
| strict?: boolean // JSON schema strict mode | ||
| shouldDefer?: boolean // lazy-load schema | ||
| } | ||
| ``` | ||
| Layer 1: Input Validation → Zod parse + tool.validateInput() Layer 2: PreToolUse Hooks → can block/modify/override permission Layer 3: Bash Classifier → speculative auto-mode check (Bash only) Layer 4: Hook Permission Merge → resolveHookPermissionDecision() Layer 5: canUseTool() → auto-mode classifier OR interactive dialog Layer 6: Decision Check → behavior !== 'allow' → DENY Layer 7: Execute → tool.call() with final input Layer 8: PostToolUse Hooks → can modify output, add annotations | ||
| --- | ||
| ### Feature-Gated Tools (src/tools.ts:193-250) | ||
| ## Complete Tool Interface (`Tool.ts:391-700`) | ||
| PROACTIVE|KAIROS → SleepTool AGENT_TRIGGERS → CronCreate, CronDelete, CronList AGENT_TRIGGERS_REMOTE → RemoteTriggerTool MONITOR_TOOL → MonitorTool KAIROS → SendUserFileTool, PushNotificationTool KAIROS_GITHUB_WEBHOOKS → SubscribePRTool CONTEXT_COLLAPSE → CtxInspectTool TERMINAL_PANEL → TerminalCaptureTool WEB_BROWSER_TOOL → WebBrowserTool HISTORY_SNIP → SnipTool UDS_INBOX → ListPeersTool WORKFLOW_SCRIPTS → WorkflowTool OVERFLOW_TEST_TOOL → OverflowTestTool USER_TYPE=ant → REPLTool, ConfigTool, TungstenTool | ||
| ```typescript | ||
| Tool<Input, Output, P> = { | ||
| // ── Identity ── | ||
| name: string // unique identifier | ||
| aliases: string[] // backwards-compatible names | ||
| searchHint: string // keyword for ToolSearch (3-10 words) | ||
| userFacingName(): string // human-readable | ||
| userFacingNameBackgroundColor(): string // theme color | ||
| ### GrowthBook & Env Vars | ||
| // ── Schema ── | ||
| inputSchema: Zod schema // required, validates tool input | ||
| inputJSONSchema?: ToolInputJSONSchema // for MCP tools (already JSON) | ||
| outputSchema?: z.ZodType // optional output validation | ||
| backfillObservableInput(): void // in-place mutation for legacy fields | ||
| // GrowthBook tengu_amber_json_tools — strict/JSON schema tools tengu_glacier_2xr — tool search unsupported models tengu_fgts — fine-grained tool streaming tengu_toolref_defer_j8m — tool reference deferral tengu_amber_wren — file read limits // Env vars CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING CLAUDE_CODE_DISABLE_COMMAND_INJECTION_CHECK CLAUDE_CODE_DISABLE_BACKGROUND_TASKS CLAUDE_CODE_DISABLE_ADVISOR_TOOL CLAUDE_CODE_SIMPLE — force Bash/Read/Edit only ENABLE_LSP_TOOL — enable LSP operations | ||
| // ── Core Execution ── | ||
| call(args, ctx, canUseTool, msg, onProgress?): Promise<ToolResult> | ||
| description(input?, options?): Promise<string> // model-facing | ||
| prompt(input?, options?): Promise<string> // detailed instructions | ||
| ### Key Files | ||
| // ── Safety Declarations ── | ||
| isConcurrencySafe(input): boolean // DEFAULT=false (fail-closed!) | ||
| isReadOnly(input): boolean // DEFAULT=false (assume writes) | ||
| isDestructive(input): boolean // DEFAULT=false | ||
| isEnabled(): boolean // DEFAULT=true (runtime gating) | ||
| interruptBehavior(): 'cancel'|'block' // on user interrupt | ||
| src/Tool.ts — type definitions (362-695) src/tools.ts — registry + assembly (193-367) src/services/tools/StreamingToolExecutor.ts — concurrency (129-519) src/services/tools/toolExecution.ts — permission pipeline (599-1500) src/services/tools/toolOrchestration.ts — partition + dispatch (19-177) src/utils/toolResultStorage.ts — per-tool + per-msg budget src/utils/toolSearch.ts — deferred tool loading src/tools/ToolSearchTool/ToolSearchTool.ts — on-demand discovery src/tools/MCPTool/MCPTool.ts — MCP tool template src/services/mcp/client.ts — dynamic MCP instantiation src/utils/toolErrors.ts — Zod error formatting | ||
| // ── Permission ── | ||
| checkPermissions(input, ctx): Promise<PermissionResult> | ||
| validateInput(input, ctx): Promise<ValidationResult> | ||
| preparePermissionMatcher(): Promise // hook pattern preparation | ||
| ### COMPLETE Tool Interface (Tool.ts:391-700) | ||
| // ── Result Management ── | ||
| maxResultSizeChars: number // persistence threshold (or Infinity) | ||
| mapToolResultToToolResultBlockParam(): ... | ||
| isResultTruncated(output): boolean | ||
| Tool<Input, Output, P> = { // ── Identity ── name: string // unique identifier aliases: string[] // backwards-compatible names searchHint: string // keyword for ToolSearch (3-10 words) userFacingName(): string // human-readable userFacingNameBackgroundColor(): string // theme color // ── Schema ── inputSchema: Zod schema // required, validates tool input inputJSONSchema?: ToolInputJSONSchema // for MCP tools (already JSON) outputSchema?: z.ZodType // optional output validation backfillObservableInput(): void // in-place mutation for legacy fields // ── Core Execution ── call(args, ctx, canUseTool, msg, onProgress?): Promise<ToolResult> description(input?, options?): Promise<string> // model-facing prompt(input?, options?): Promise<string> // detailed instructions // ── Safety Declarations ── isConcurrencySafe(input): boolean // DEFAULT=false (fail-closed!) isReadOnly(input): boolean // DEFAULT=false (assume writes) isDestructive(input): boolean // DEFAULT=false isEnabled(): boolean // DEFAULT=true (runtime gating) interruptBehavior(): 'cancel'|'block' // on user interrupt // ── Permission ── checkPermissions(input, ctx): Promise<PermissionResult> validateInput(input, ctx): Promise<ValidationResult> preparePermissionMatcher(): Promise // hook pattern preparation // ── Result Management ── maxResultSizeChars: number // persistence threshold (or Infinity) mapToolResultToToolResultBlockParam(): ... // serialization isResultTruncated(output): boolean // ── Rendering (UI) ── renderToolUseMessage() renderToolResultMessage() renderToolUseProgressMessage() // ── Classification ── isSearchOrReadCommand(): boolean // UI collapsing hints isOpenWorld(): boolean // unrestricted output requiresUserInteraction(): boolean toAutoClassifierInput(): string // '' = skip classifier getPath(input): string // file path for permission rules // ── Deferred Loading ── shouldDefer: readonly boolean // lazy-load schema alwaysLoad: readonly boolean // never defer strict: readonly boolean // stricter API adherence // ── Special Markers ── isMcp: boolean // MCP tool isLsp: boolean // LSP tool inputsEquivalent(a, b): boolean // detect equivalent inputs } | ||
| // ── Rendering (UI) ── | ||
| renderToolUseMessage() | ||
| renderToolResultMessage() | ||
| renderToolUseProgressMessage() | ||
| ### ToolUseContext — 传递给所有 tool.call() 的上下文 (Tool.ts:156-340) | ||
| // ── Classification ── | ||
| isSearchOrReadCommand(): boolean // UI collapsing hints | ||
| isOpenWorld(): boolean // unrestricted output | ||
| requiresUserInteraction(): boolean | ||
| toAutoClassifierInput(): string // '' = skip classifier | ||
| getPath(input): string // file path for permission rules | ||
| ToolUseContext = { options: { commands: Command[] debug: boolean mainLoopModel: string tools: Tools verbose: boolean thinkingConfig: ThinkingConfig mcpClients: MCPServerConnection[] mcpResources: Record<string, ServerResource[]> isNonInteractiveSession: boolean agentDefinitions: AgentDefinitionsResult maxBudgetUsd?: number customSystemPrompt?: string appendSystemPrompt?: string refreshTools?: () => Tools // mid-query dynamic reload } abortController: AbortController readFileState: FileStateCache // file read cache per agent getAppState(): AppState setAppState(f): void // UI state mutations setAppStateForTasks?(f): void // background infrastructure handleElicitation?: ... // MCP elicitation setToolJSX?: SetToolJSXFn messages: Message[] // full conversation fileReadingLimits?: { maxTokens?, maxSizeBytes? } globLimits?: { maxResults? } toolDecisions?: Map<string, { source, decision, timestamp }> requestPrompt?: (source, summary) => ... // interactive permission contentReplacementState?: ContentReplacementState localDenialTracking?: DenialTrackingState queryTracking?: QueryChainTracking // depth, chainId agentId?: string agentType?: string // ...30+ additional fields for notifications, skill discovery, memory... } | ||
| // ── Deferred Loading ── | ||
| shouldDefer: readonly boolean // lazy-load schema | ||
| alwaysLoad: readonly boolean // never defer | ||
| strict: readonly boolean // stricter API adherence | ||
| ### buildTool() Factory — TOOL_DEFAULTS 值得注意! | ||
| // ── Special Markers ── | ||
| isMcp: boolean // MCP tool | ||
| isLsp: boolean // LSP tool | ||
| inputsEquivalent(a, b): boolean // detect equivalent inputs | ||
| } | ||
| ``` | ||
| // Tool.ts:750-850 const TOOL_DEFAULTS = { isEnabled: () => true, isConcurrencySafe: () => false, // 🔴 fail-closed! 新 tool 默认不并发 isReadOnly: () => false, // 🔴 assume writes isDestructive: () => false, checkPermissions: () => Promise.resolve({ behavior: 'allow' }), toAutoClassifierInput: () => '', // skip classifier userFacingName: () => def.name, } // buildTool() 就是展开 defaults + override export function buildTool<D>(def: D): BuiltTool<D> { return { ...TOOL_DEFAULTS, userFacingName: () => def.name, ...def } } // 关键设计:fail-closed concurrency // → 新 tool 如果没声明 isConcurrencySafe=true,就自动串行 // → 比 "默认并发,出 bug 再加锁" 安全得多 | ||
| --- | ||
| ### StreamingToolExecutor — 并发控制核心逻辑 | ||
| ## `TOOL_DEFAULTS` — `buildTool()` Factory (`Tool.ts:750-850`) | ||
| class StreamingToolExecutor { private tools: TrackedTool[] = [] addTool(block, assistantMessage) { // Parse input, check isConcurrencySafe() // Push to queue with status='queued' // Trigger processQueue() } canExecuteTool(isConcurrencySafe): boolean { const executing = this.tools.filter(t => t.status === 'executing') // Can execute if: // - No tools running, OR // - This tool is safe AND all executing tools are safe } processQueue() { for (const tool of this.tools) { if (tool.status !== 'queued') continue if (canExecuteTool(tool.isConcurrencySafe)) { executeTool(tool) // status → 'executing' } else if (!tool.isConcurrencySafe) { break // Non-concurrent tool blocks queue } } } // 关键行为: // 1. Progress messages 立即 yield (不等后续 tool) // 2. Results 按 tool-received 顺序 emit (不是 completion 顺序) // 3. Sibling error abort: Bash error → siblingAbortController fires // → 杀掉并行的子进程,但不结束这个 turn // 4. Streaming fallback: discard() 注入 synthetic error messages } | ||
| ```typescript | ||
| const TOOL_DEFAULTS = { | ||
| isEnabled: () => true, | ||
| isConcurrencySafe: () => false, // ⚠️ fail-closed! new tools default non-concurrent | ||
| isReadOnly: () => false, // ⚠️ assume writes | ||
| isDestructive: () => false, | ||
| checkPermissions: () => Promise.resolve({ behavior: 'allow' }), | ||
| toAutoClassifierInput: () => '', // skip classifier | ||
| userFacingName: () => def.name, | ||
| } | ||
| ### 5-Stage Permission Pipeline (toolExecution.ts:599-1250) | ||
| export function buildTool<D>(def: D): BuiltTool<D> { | ||
| return { ...TOOL_DEFAULTS, userFacingName: () => def.name, ...def } | ||
| } | ||
| checkPermissionsAndCallTool(): Stage 1: Zod Input Validation (L610-630) parsedInput = tool.inputSchema.safeParse(input) if (!success) → formatZodValidationError() → is_error: true // ⚠️ 检测 deferred tool schema 未发送: buildSchemaNotSentHint(tool, messages, tools) // "This tool was not in the discovered-tool set. Call ToolSearchTool first." Stage 2: Custom validateInput() (L680-705) isValidCall = await tool.validateInput(parsedInput.data, ctx) if (result === false) → is_error: true // 例: BashTool 检测 blocked sleep patterns (sync >2s) // 例: FileWriteTool 检测 team memory secret patterns // 例: NotebookEditTool 拒绝 UNC paths (NTLM leak protection) Stage 3: Pre-Tool Hooks (L706-800) preHooksDecision = await runPreToolUseHooks(tool, input, ...) // Hooks can: approve / deny / ask / transform input (updatedInput) Stage 4: Permission Decision (L801-950) permissionDecision = await canUseTool(tool, input, ctx, ...) // 来自 useCanUseTool.ts: // - Allow/deny/ask rules from toolPermissionContext // - Hook decisions (resolve conflicts) // - Denial tracking threshold fallback Stage 5: Tool Execution + Post-Hooks (L1001-1250) toolResult = await tool.call(parsedInput.data, ctx, canUseTool, msg, onProgress) postToolDecision = await runPostToolUseHooks(tool, result, ctx) // Post-hooks can transform result, reject, add newMessages toolResultBlock = await processToolResultBlock(tool, result, toolUseID) // → 检查 size vs maxResultSizeChars → 超限写磁盘 + preview | ||
| // Design: fail-closed concurrency | ||
| // → New tool without isConcurrencySafe=true → auto serial | ||
| // → Safer than "default parallel, add lock on bug" | ||
| ``` | ||
| ### MCP Tool 集成 — 动态实例化 | ||
| --- | ||
| // src/services/mcp/client.ts:100-350 // MCP server 连接时: 1\. ListTools RPC → 获取 server 的所有 tool 定义 2\. 对每个 tool: Clone MCPTool 模板 + Override { ...MCPTool, // template name: `mcp__${serverName}__${toolName}`, // 命名规则 mcpInfo: { serverName, toolName }, isMcp: true, call(args, ctx) { // 通过 MCP CallTool RPC 执行 const result = await client.callTool({ name: mcpToolDef.name, arguments: args, }) return { data: result, mcpMeta: result._meta } }, inputSchema: mcpToolDef.inputSchema, // 直接用 server 的 schema checkPermissions: () => ({ behavior: 'passthrough' }) } 3\. 注册到 appState.mcp.tools // ALL MCP tools 默认 deferred (shouldDefer=true) // 除非 server 声明 _meta['anthropic/alwaysLoad']=true // Model 只看到 ToolSearchTool → call 它发现 MCP tools | ||
| ## `ToolUseContext` — Passed to All `tool.call()` (`Tool.ts:156-340`) | ||
| ### Tool Result 存储 & Budget 控制 | ||
| ```typescript | ||
| ToolUseContext = { | ||
| options: { | ||
| commands: Command[] | ||
| debug: boolean | ||
| mainLoopModel: string | ||
| tools: Tools | ||
| verbose: boolean | ||
| thinkingConfig: ThinkingConfig | ||
| mcpClients: MCPServerConnection[] | ||
| mcpResources: Record<string, ServerResource[]> | ||
| isNonInteractiveSession: boolean | ||
| agentDefinitions: AgentDefinitionsResult | ||
| maxBudgetUsd?: number | ||
| customSystemPrompt?: string | ||
| appendSystemPrompt?: string | ||
| refreshTools?: () => Tools // mid-query dynamic reload | ||
| } | ||
| abortController: AbortController | ||
| readFileState: FileStateCache // file read cache per agent | ||
| getAppState(): AppState | ||
| setAppState(f): void // UI state mutations | ||
| setAppStateForTasks?(f): void // background infrastructure | ||
| handleElicitation?: ... // MCP elicitation | ||
| setToolJSX?: SetToolJSXFn | ||
| messages: Message[] // full conversation | ||
| fileReadingLimits?: { maxTokens?, maxSizeBytes? } | ||
| globLimits?: { maxResults? } | ||
| toolDecisions?: Map<string, { source, decision, timestamp }> | ||
| requestPrompt?: (source, summary) => ... // interactive permission | ||
| contentReplacementState?: ContentReplacementState | ||
| localDenialTracking?: DenialTrackingState | ||
| queryTracking?: QueryChainTracking // depth, chainId | ||
| agentId?: string | ||
| agentType?: string | ||
| // ...30+ additional fields for notifications, skill discovery, memory... | ||
| } | ||
| ``` | ||
| // src/utils/toolResultStorage.ts ═══ Per-Tool Persistence ═══ getPersistenceThreshold(tool): \- Infinity → never persist (Read tool, self-bounded) \- GrowthBook override: tengu_satin_quoll[toolName] \- Default: min(tool.maxResultSizeChars, 50KB) maybePersistLargeToolResult(): if size > threshold: 1\. Write to disk: {sessionId}/tool-results/{toolUseId}.txt 2\. Generate preview (first 2KB, truncate at newline boundary) 3\. Wrap in <persisted-output> tag: "Output too large (150KB). Saved to: /path/result.txt Preview (first 2KB): ..." ═══ Per-Message Aggregate Budget ═══ ContentReplacementState = { seenIds: Set<string> // tool use IDs already evaluated replacements: Map<string, string> // ID → cached preview } enforceToolResultBudget(): 1\. collectCandidatesByMessage() → group tool_results per API msg 2\. partitionByPriorDecision(): \- mustReapply: 已替换 → re-apply (byte-identical for cache) \- frozen: 已见未替换 → never replace (cache stability!) \- fresh: 新的 → eligible for replacement 3\. selectFreshToReplace(): \- Sort fresh by size (largest first) \- 替换到 frozen + remaining ≤ budget 4\. Persist selected, return messages with replacements Budget 默认: 320KB per message (tengu_hawthorn_window) ═══ Resume Reconstruction ═══ reconstructContentReplacementState(): \- Fork agents: 从 parent 继承 replacements \- Resume: 从磁盘 ContentReplacementRecord[] 重建 | ||
| --- | ||
| ### Deferred Tool Loading — 按需发现 | ||
| ## Concurrency Rule (`StreamingToolExecutor:129-135`) | ||
| // src/utils/toolSearch.ts isDeferredTool(tool): 1\. tool.alwaysLoad === true → NOT deferred 2\. tool.isMcp === true → ALWAYS deferred (workflow-specific) 3\. name === 'ToolSearchTool' → NOT deferred (discovery tool) 4\. FORK_SUBAGENT enabled && name === 'Agent' → NOT deferred 5\. KAIROS enabled && name === 'Brief' → NOT deferred 6\. tool.shouldDefer === true → deferred // Tool Search Mode (ENABLE_TOOL_SEARCH env): // unset/'true' → 'tst' (always defer MCP + shouldDefer) // 'false' → 'standard' (all tools visible inline) // 'auto' → 'tst-auto' (defer if count exceeds threshold) // 'auto:0' → 'tst' | 'auto:100' → 'standard' // API Call 时 (claude.ts:1150-1200): if (useToolSearch) { deferredToolNames = tools.filter(isDeferredTool).map(t => t.name) discoveredToolNames = extractDiscoveredToolNames(messages) // 扫描 tool_reference blocks 获取已发现的 tool filteredTools = tools.filter(t => !deferredToolNames.has(t.name) || // non-deferred always send t.name === 'ToolSearchTool' || // always available discoveredToolNames.has(t.name) // previously discovered ) } // ToolSearchTool.call(): // query='select:ToolName' → 直接返回指定 tool // query='keyword search' → semantic + text match // query='list all' → 返回所有 deferred tools | ||
| ``` | ||
| canExecuteTool(isConcurrencySafe): | ||
| executingTools.length === 0 → YES | ||
| isConcurrencySafe AND all executing are safe → YES | ||
| otherwise → NO (wait) | ||
| ### Tool Pool Assembly Flow (tools.ts) | ||
| Partition: [safe,safe,safe] → parallel | [unsafe] → serial | [safe,safe] → parallel | ||
| ``` | ||
| assembleToolPool(permissions, mcpTools): 1\. getTools(permissions) → built-in tools (feature-gated) \- CLAUDE_CODE_SIMPLE → only Bash, Read, Edit \- REPL mode → hides primitives (REPL wraps them) 2\. filterToolsByDenyRules(tools, denyRules) → deny rules strip tools before model sees them → "mcp__server" prefix rules strip entire server 3\. Filter MCP tools by deny rules 4\. Deduplicate by name (built-in wins on conflict) 5\. Sort for PROMPT CACHE STABILITY: → built-ins contiguous → global cache breakpoint after last built-in 6\. uniqBy() preserving insertion order (built-ins first) // getAllBaseTools() conditional includes: // USER_TYPE=ant → REPLTool, SuggestBackgroundPRTool // PROACTIVE|KAIROS → SleepTool // AGENT_TRIGGERS → CronCreate/Delete/List // MONITOR_TOOL → MonitorTool // WEB_BROWSER_TOOL → WebBrowserTool // isToolSearchEnabledOptimistic() → ToolSearchTool | ||
| ## `StreamingToolExecutor` — Concurrency Core | ||
| I | ||
| ```typescript | ||
| class StreamingToolExecutor { | ||
| private tools: TrackedTool[] = [] | ||
| ## Comparison | ||
| addTool(block, assistantMessage) { | ||
| // Parse input, check isConcurrencySafe() | ||
| // Push to queue with status='queued' | ||
| // Trigger processQueue() | ||
| } | ||
| Claude Code | ||
| canExecuteTool(isConcurrencySafe): boolean { | ||
| const executing = this.tools.filter(t => t.status === 'executing') | ||
| // Can execute if: | ||
| // - No tools running, OR | ||
| // - This tool is safe AND all executing tools are safe | ||
| } | ||
| * **Tool <Input, Output, Progress>** — 泛型定义,Zod schema 驱动 | ||
| * **buildTool()** — factory with defaults (isEnabled, isConcurrencySafe=false, isReadOnly=false, isDestructive=false) | ||
| * **validateInput()** — 两层:Zod schema → tool-specific semantic check | ||
| * **checkPermissions()** — tool 自带权限逻辑 | ||
| * **call()** 返回 `ToolResult{ data, newMessages, contextModifier }` | ||
| * **isConcurrencySafe(input)** — 按 input 动态判断(比如 Read 是 safe,Write 不是) | ||
| * **isDestructive(input)** — 标记不可撤回操作 | ||
| * **onProgress callback** — 流式报告执行进度 | ||
| * **renderToolUse / renderToolResult** — 自带 UI 渲染逻辑 | ||
| * **assembleToolPool()** — 动态组装 built-in + MCP,deny-rule 过滤 | ||
| processQueue() { | ||
| for (const tool of this.tools) { | ||
| if (tool.status !== 'queued') continue | ||
| if (canExecuteTool(tool.isConcurrencySafe)) { | ||
| executeTool(tool) // status → 'executing' | ||
| } else if (!tool.isConcurrencySafe) { | ||
| break // Non-concurrent tool blocks queue | ||
| } | ||
| } | ||
| } | ||
| Mitsein | ||
| // Key behaviors: | ||
| // 1. Progress messages yield immediately (don't wait for subsequent tools) | ||
| // 2. Results emitted in tool-received order (not completion order) | ||
| // 3. Sibling error abort: Bash error → siblingAbortController fires | ||
| // → kills parallel child processes, but does not end this turn | ||
| // 4. Streaming fallback: discard() injects synthetic error messages | ||
| } | ||
| ``` | ||
| * `tools: ["web_search", "ppt_generate"]` — config 中的字符串列表 | ||
| * 每个 tool 独立实现,无统一 interface / ABC | ||
| * 无 input schema validation — LLM 传错参数只有 runtime 才暴露 | ||
| * ToolPolicy: allow / deny 二元判断,无 tool-specific permission logic | ||
| * 无并发安全声明 — 不知道哪些 tools 可以 parallel 执行 | ||
| * 无 progress callback — 耗时操作只有 "运行中..." 状态 | ||
| * 无 destructive 标记 — xhs_publish / email_send 与 web_search 同等对待 | ||
| --- | ||
| II | ||
| ## 5-Stage Permission Pipeline (`toolExecution.ts:599-1250`) | ||
| ## How to Implement | ||
| ``` | ||
| checkPermissionsAndCallTool(): | ||
| #### Step 1: BaseTool Protocol | ||
| Stage 1: Zod Input Validation (L610-630) | ||
| parsedInput = tool.inputSchema.safeParse(input) | ||
| if (!success) → formatZodValidationError() → is_error: true | ||
| // Detect deferred tool schema not sent: | ||
| buildSchemaNotSentHint(tool, messages, tools) | ||
| // "This tool was not in the discovered-tool set. Call ToolSearchTool first." | ||
| 定义 Python Protocol / ABC,所有 tool 必须实现。 | ||
| Stage 2: Custom validateInput() (L680-705) | ||
| isValidCall = await tool.validateInput(parsedInput.data, ctx) | ||
| if (result === false) → is_error: true | ||
| // e.g. BashTool: detect blocked sleep patterns (sync >2s) | ||
| // e.g. FileWriteTool: detect team memory secret patterns | ||
| // e.g. NotebookEditTool: reject UNC paths (NTLM leak protection) | ||
| from typing import Protocol, Any from pydantic import BaseModel class ToolResult(BaseModel): data: Any new_messages: list[dict] | None = None class BaseTool(Protocol): name: str description: str input_schema: type[BaseModel] # Pydantic model def is_concurrency_safe(self, input: BaseModel) -> bool: ... def is_read_only(self, input: BaseModel) -> bool: ... def is_destructive(self, input: BaseModel) -> bool: ... async def validate_input(self, input: BaseModel) -> str | None: """Return error message or None.""" ... async def execute( self, input: BaseModel, context: ToolContext, on_progress: Callable[[dict], None] | None = None, ) -> ToolResult: ... | ||
| Stage 3: Pre-Tool Hooks (L706-800) | ||
| preHooksDecision = await runPreToolUseHooks(tool, input, ...) | ||
| // Hooks can: approve / deny / ask / transform input (updatedInput) | ||
| #### Step 2: Migrate existing tools | ||
| Stage 4: Permission Decision (L801-950) | ||
| permissionDecision = await canUseTool(tool, input, ctx, ...) | ||
| // From useCanUseTool.ts: | ||
| // - Allow/deny/ask rules from toolPermissionContext | ||
| // - Hook decisions (resolve conflicts) | ||
| // - Denial tracking threshold fallback | ||
| 逐步把现有 tools 适配 BaseTool。先做标记(safety declarations),再改 execute 签名。 | ||
| Stage 5: Tool Execution + Post-Hooks (L1001-1250) | ||
| toolResult = await tool.call(parsedInput.data, ctx, canUseTool, msg, onProgress) | ||
| postToolDecision = await runPostToolUseHooks(tool, result, ctx) | ||
| // Post-hooks can transform result, reject, add newMessages | ||
| toolResultBlock = await processToolResultBlock(tool, result, toolUseID) | ||
| // → check size vs maxResultSizeChars → if over limit: write to disk + preview | ||
| ``` | ||
| # tools/web_search.py class WebSearchInput(BaseModel): query: str max_results: int = 10 class WebSearchTool: name = "web_search" description = "Search the web" input_schema = WebSearchInput def is_concurrency_safe(self, input): return True # 无副作用 def is_read_only(self, input): return True def is_destructive(self, input): return False # tools/xhs_publish.py class XhsPublishTool: name = "xhs_publish" def is_concurrency_safe(self, input): return False # 有副作用 def is_read_only(self, input): return False def is_destructive(self, input): return True # 不可撤回! | ||
| --- | ||
| #### Step 3: Tool Registry & Assembly | ||
| ## Feature-Gated Tools (`src/tools.ts:193-250`) | ||
| 动态组装 tool pool,基于 agent config + permissions + 安装的 capabilities。 | ||
| ``` | ||
| PROACTIVE|KAIROS → SleepTool | ||
| AGENT_TRIGGERS → CronCreate, CronDelete, CronList | ||
| AGENT_TRIGGERS_REMOTE → RemoteTriggerTool | ||
| MONITOR_TOOL → MonitorTool | ||
| KAIROS → SendUserFileTool, PushNotificationTool | ||
| KAIROS_GITHUB_WEBHOOKS → SubscribePRTool | ||
| CONTEXT_COLLAPSE → CtxInspectTool | ||
| TERMINAL_PANEL → TerminalCaptureTool | ||
| WEB_BROWSER_TOOL → WebBrowserTool | ||
| HISTORY_SNIP → SnipTool | ||
| UDS_INBOX → ListPeersTool | ||
| WORKFLOW_SCRIPTS → WorkflowTool | ||
| OVERFLOW_TEST_TOOL → OverflowTestTool | ||
| USER_TYPE=ant → REPLTool, ConfigTool, TungstenTool | ||
| ``` | ||
| class ToolRegistry: _all_tools: dict[str, BaseTool] = {} def register(self, tool: BaseTool): self._all_tools[tool.name] = tool def assemble_pool( self, agent_config: AgentConfig, policy: ToolPolicy ) -> list[BaseTool]: requested = agent_config.tools # ["web_search", "spawn_agent"] pool = [] for name in requested: tool = self._all_tools.get(name) if not tool: logger.warning(f"Tool {name} not found, skipping") continue if policy.is_denied(tool): continue pool.append(tool) return pool | ||
| --- | ||
| #### Step 4: Concurrent execution partitioning | ||
| ## MCP Tool Integration — Dynamic Instantiation (`src/services/mcp/client.ts:100-350`) | ||
| 一轮中多个 tool calls 按 concurrency safety 分组执行。 | ||
| ```typescript | ||
| // On MCP server connection: | ||
| async def execute_tool_batch(tools_with_inputs, context): """Partition and execute tool calls with concurrency control.""" batches = partition_by_safety(tools_with_inputs) results = [] for batch in batches: if len(batch) == 1 and not batch[0].tool.is_concurrency_safe(batch[0].input): # Serial: unsafe tool r = await execute_single(batch[0], context) results.append(r) else: # Parallel: all safe tools rs = await asyncio.gather(*[ execute_single(item, context) for item in batch ]) results.extend(rs) return results def partition_by_safety(items): """Group consecutive safe tools; isolate unsafe ones.""" batches, current_safe = [], [] for item in items: if item.tool.is_concurrency_safe(item.input): current_safe.append(item) else: if current_safe: batches.append(current_safe) current_safe = [] batches.append([item]) if current_safe: batches.append(current_safe) return batches | ||
| 1. ListTools RPC → get all tool definitions from server | ||
| 2. For each tool: Clone MCPTool template + Override | ||
| { | ||
| ...MCPTool, // template | ||
| name: `mcp__${serverName}__${toolName}`, // naming convention | ||
| mcpInfo: { serverName, toolName }, | ||
| isMcp: true, | ||
| #### Step 5: Progress callback | ||
| call(args, ctx) { | ||
| const result = await client.callTool({ | ||
| name: mcpToolDef.name, | ||
| arguments: args, | ||
| }) | ||
| return { data: result, mcpMeta: result._meta } | ||
| }, | ||
| 耗时 tool 通过 callback 报告进度,orchestrator 转发到前端。 | ||
| inputSchema: mcpToolDef.inputSchema, // use server's schema directly | ||
| checkPermissions: () => ({ behavior: 'passthrough' }) | ||
| } | ||
| class PptGenerateTool: async def execute(self, input, context, on_progress=None): total_slides = len(input.slides) for i, slide in enumerate(input.slides): result = await self._generate_slide(slide) if on_progress: on_progress({ "tool": self.name, "status": "running", "slides_done": i + 1, "slides_total": total_slides, "percent": round((i + 1) / total_slides * 100), }) return ToolResult(data={"file_url": output_url}) | ||
| 3. Register to appState.mcp.tools | ||
| III | ||
| // ALL MCP tools default to deferred (shouldDefer=true) | ||
| // Unless server declares _meta['anthropic/alwaysLoad']=true | ||
| // Model only sees ToolSearchTool → calls it to discover MCP tools | ||
| ``` | ||
| ## How to Verify | ||
| --- | ||
| ### Unit Tests | ||
| ## Tool Result Storage & Budget Control (`src/utils/toolResultStorage.ts`) | ||
| * **test_all_tools_implement_protocol** — 遍历 registry 所有 tools,验证都实现了 BaseTool | ||
| * **test_input_validation** — 传 invalid input 给 ppt_generate(缺少 slides 字段),验证 Pydantic ValidationError 而非 runtime crash | ||
| * **test_safety_declarations_correct** — web_search.is_read_only() = True, xhs_publish.is_destructive() = True | ||
| * **test_partition_by_safety** — [safe, safe, unsafe, safe] → [[safe, safe], [unsafe], [safe]] | ||
| * **test_concurrent_execution** — 3 个 safe tools 并发执行,验证总耗时 ≈ max(单个) 而非 sum | ||
| * **test_progress_callback** — 12 页 PPT 生成过程中,on_progress 被调用 12 次 | ||
| * **test_tool_assembly_deny_rule** — ToolPolicy deny "xhs_publish",verify pool 不含该 tool | ||
| ```typescript | ||
| ═══ Per-Tool Persistence ═══ | ||
| getPersistenceThreshold(tool): | ||
| - Infinity → never persist (Read tool, self-bounded) | ||
| - GrowthBook override: tengu_satin_quoll[toolName] | ||
| - Default: min(tool.maxResultSizeChars, 50KB) | ||
| ### Integration Tests | ||
| maybePersistLargeToolResult(): | ||
| if size > threshold: | ||
| 1. Write to disk: {sessionId}/tool-results/{toolUseId}.txt | ||
| 2. Generate preview (first 2KB, truncate at newline boundary) | ||
| 3. Wrap in <persisted-output> tag: | ||
| "Output too large (150KB). Saved to: /path/result.txt | ||
| Preview (first 2KB): ..." | ||
| * **test_llm_invalid_input_handled** — LLM 调用 tool 时传 invalid params,验证返回 structured error(不 crash orchestrator) | ||
| * **test_destructive_tool_requires_confirmation** — xhs_publish 执行前触发 permission check | ||
| * **test_parallel_search_and_read** — 一轮中同时调 web_search + calendar_read,验证并发执行 | ||
| ═══ Per-Message Aggregate Budget ═══ | ||
| ContentReplacementState = { | ||
| seenIds: Set<string> // tool use IDs already evaluated | ||
| replacements: Map<string, string> // ID → cached preview | ||
| } | ||
| IV | ||
| enforceToolResultBudget(): | ||
| 1. collectCandidatesByMessage() → group tool_results per API msg | ||
| 2. partitionByPriorDecision(): | ||
| - mustReapply: already replaced → re-apply (byte-identical for cache) | ||
| - frozen: seen but not replaced → never replace (cache stability!) | ||
| - fresh: new → eligible for replacement | ||
| 3. selectFreshToReplace(): | ||
| - Sort fresh by size (largest first) | ||
| - Replace until frozen + remaining ≤ budget | ||
| 4. Persist selected, return messages with replacements | ||
| ## User Perception | ||
| Budget default: 320KB per message (tengu_hawthorn_window) | ||
| Before | ||
| ═══ Resume Reconstruction ═══ | ||
| reconstructContentReplacementState(): | ||
| - Fork agents: inherit replacements from parent | ||
| - Resume: rebuild from disk ContentReplacementRecord[] | ||
| ``` | ||
| 小红书 Agent: 正在生成配图... | ||
| --- | ||
| [等待 15 秒,无任何进度反馈] | ||
| ## GrowthBook & Env Vars | ||
| 小红书 Agent: 配图生成完成,现在搜索热点... | ||
| ``` | ||
| // GrowthBook | ||
| tengu_amber_json_tools — strict/JSON schema tools | ||
| tengu_glacier_2xr — tool search unsupported models | ||
| tengu_fgts — fine-grained tool streaming | ||
| tengu_toolref_defer_j8m — tool reference deferral | ||
| tengu_amber_wren — file read limits | ||
| [又等待 8 秒] | ||
| [两步串行执行,总耗时 23 秒] | ||
| After | ||
| 小红书 Agent: 同时搜索热点 + 生成配图... | ||
| [进度: 配图 2/4 张 | 热点搜索已完成 ✓] | ||
| [进度: 配图 4/4 张 ✓] | ||
| 小红书 Agent: 全部完成,开始排版 | ||
| [并发执行,总耗时 15 秒] | ||
| **核心体感差异:**(1) 并发执行让多 tool workflow 快 30-50%;(2) 进度反馈让用户知道 agent "在做什么"而非"卡住了";(3) destructive 标记让危险操作(发布/发送)有确认环节。 | ||
| BaseTool ToolPolicy concurrent Pydantic progress | ||
| // Env vars | ||
| CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING | ||
| CLAUDE_CODE_DISABLE_COMMAND_INJECTION_CHECK | ||
| CLAUDE_CODE_DISABLE_BACKGROUND_TASKS | ||
| CLAUDE_CODE_DISABLE_ADVISOR_TOOL | ||
| CLAUDE_CODE_SIMPLE — force Bash/Read/Edit only | ||
| ENABLE_LSP_TOOL — enable LSP operations | ||
| ``` |
+214
-141
@@ -1,204 +0,277 @@ | ||
| [← Back to Index](index.html) | ||
| # 03. Memory System | ||
| # 03\. Memory System Critical | ||
| > From `SESSION_END` write to continuous extraction + LLM relevance + staleness management. | ||
| 从 SESSION_END 写入到持续提取 + LLM Relevance + Staleness 管理 | ||
| ## Key Files | ||
| R | ||
| ``` | ||
| src/memdir/memdir.ts — entrypoint loading, prompt building | ||
| src/memdir/memoryScan.ts — file scanning + frontmatter parsing | ||
| src/memdir/findRelevantMemories.ts — Sonnet side-query relevance | ||
| src/memdir/memoryTypes.ts — 4 types: user/feedback/project/reference | ||
| src/memdir/memoryAge.ts — staleness warnings (days calculation) | ||
| src/memdir/paths.ts — memory dir resolution + security validation | ||
| src/services/extractMemories/ — auto-extraction pipeline | ||
| src/services/autoDream/autoDream.ts — background consolidation orchestration | ||
| src/services/autoDream/consolidationLock.ts — file-based distributed lock | ||
| src/services/autoDream/consolidationPrompt.ts — 4-phase prompt | ||
| src/services/autoDream/config.ts — feature gate + GB defaults | ||
| src/services/awaySummary.ts — away summary generation | ||
| src/tasks/DreamTask/DreamTask.ts — dream task state machine | ||
| ``` | ||
| ## Feature Reference [→ Full GrowthBook Index](00-feature-discovery.html#flags-gb) | ||
| --- | ||
| ### Constants | ||
| ## Constants | ||
| // src/memdir/memdir.ts MAX_ENTRYPOINT_LINES = 200 // MEMORY.md line limit MAX_ENTRYPOINT_BYTES = 25_000 // MEMORY.md byte limit // src/memdir/memoryScan.ts MAX_MEMORY_FILES = 200 // max files to scan (newest-first) FRONTMATTER_MAX_LINES = 30 // lines read per file for frontmatter // src/memdir/findRelevantMemories.ts max_results = 5 // top N memories selected model = Sonnet // getDefaultSonnetModel() max_tokens = 256 // relevance query output // src/services/extractMemories/ maxTurns = 5 // extraction agent hard cap // src/services/autoDream/config.ts minHours = 24 // hours since last consolidation minSessions = 5 // sessions touched since last SESSION_SCAN_INTERVAL = 10 min // scan throttle | ||
| ```typescript | ||
| // src/memdir/memdir.ts | ||
| MAX_ENTRYPOINT_LINES = 200 // MEMORY.md line limit | ||
| MAX_ENTRYPOINT_BYTES = 25_000 // MEMORY.md byte limit | ||
| ### Feature Flags | ||
| // src/memdir/memoryScan.ts | ||
| MAX_MEMORY_FILES = 200 // max files to scan (newest-first) | ||
| FRONTMATTER_MAX_LINES = 30 // lines read per file for frontmatter | ||
| `EXTRACT_MEMORIES`| Gates auto-extraction at query loop end | ||
| ---|--- | ||
| `TEAMMEM`| Team memory (shared dirs), dynamic require() | ||
| `KAIROS_DREAM`| Assistant daily-log prompt (replaces MEMORY.md) | ||
| `AWAY_SUMMARY`| 5-min blur → "while you were away" summary | ||
| ### GrowthBook Flags | ||
| // src/memdir/findRelevantMemories.ts | ||
| max_results = 5 // top N memories selected | ||
| model = Sonnet // getDefaultSonnetModel() | ||
| max_tokens = 256 // relevance query output | ||
| `tengu_onyx_plover` (null)| Auto-Dream config: {enabled, minHours:24, minSessions:5} | ||
| ---|--- | ||
| `tengu_bramble_lintel` (null→1)| Extraction throttle: run every N eligible turns | ||
| `tengu_passport_quail` (false)| Master gate for extractMemories execution | ||
| `tengu_slate_thimble` (false)| Allow extraction in non-interactive (--no-input) | ||
| `tengu_coral_fern` (false)| "Searching past context" grep instructions | ||
| `tengu_herring_clock` (false)| Team-memory feature gate | ||
| `tengu_session_memory` (false)| Session memory (conversation notes between turns) | ||
| `tengu_sedge_lantern` (false)| Away-summary (5-min blur → recap) | ||
| `tengu_moth_copse` (false)| Skip MEMORY.md index, show topic files only | ||
| ### Extraction Restricted Tool Access | ||
| // src/services/extractMemories/ | ||
| maxTurns = 5 // extraction agent hard cap | ||
| // src/services/extractMemories/extractMemories.ts:171-222 ALLOWED unrestricted: Read, Grep, Glob, REPL ALLOWED restricted: Bash (read-only: ls/find/grep/cat/stat/wc/head/tail) ALLOWED memory-only: Edit, Write (ONLY inside auto-memory directory) DENIED: All others (MCP, Agent, write-capable Bash, etc.) | ||
| // src/services/autoDream/config.ts | ||
| minHours = 24 // hours since last consolidation | ||
| minSessions = 5 // sessions touched since last | ||
| SESSION_SCAN_INTERVAL = 10 min // scan throttle | ||
| ``` | ||
| ### Env Vars | ||
| --- | ||
| CLAUDE_CODE_DISABLE_AUTO_MEMORY — 1/true=OFF, 0/false=ON CLAUDE_COWORK_MEMORY_EXTRA_GUIDELINES — injected into all memory prompts CLAUDE_COWORK_MEMORY_PATH_OVERRIDE — full override for memory dir path CLAUDE_CODE_REMOTE_MEMORY_DIR — CCR remote memory mount point CLAUDE_CODE_SIMPLE (--bare) — disables auto-memory entirely | ||
| ## Feature Flags | ||
| ### Key Files | ||
| | Flag | Description | | ||
| |---|---| | ||
| | `EXTRACT_MEMORIES` | Gates auto-extraction at query loop end | | ||
| | `TEAMMEM` | Team memory (shared dirs), dynamic require() | | ||
| | `KAIROS_DREAM` | Assistant daily-log prompt (replaces MEMORY.md) | | ||
| | `AWAY_SUMMARY` | 5-min blur → "while you were away" summary | | ||
| src/memdir/memdir.ts — entrypoint loading, prompt building src/memdir/memoryScan.ts — file scanning + frontmatter parsing src/memdir/findRelevantMemories.ts — Sonnet side-query relevance src/memdir/memoryTypes.ts — 4 types: user/feedback/project/reference src/memdir/memoryAge.ts — staleness warnings (days calculation) src/memdir/paths.ts — memory dir resolution + security validation src/services/extractMemories/ — auto-extraction pipeline src/services/autoDream/autoDream.ts — background consolidation orchestration src/services/autoDream/consolidationLock.ts — file-based distributed lock src/services/autoDream/consolidationPrompt.ts — 4-phase prompt src/services/autoDream/config.ts — feature gate + GB defaults src/services/awaySummary.ts — away summary generation src/tasks/DreamTask/DreamTask.ts — dream task state machine | ||
| ## GrowthBook Flags | ||
| ### Evidence Boundary | ||
| | Flag | Default | Description | | ||
| |---|---|---| | ||
| | `tengu_onyx_plover` | null | Auto-Dream config: `{enabled, minHours:24, minSessions:5}` | | ||
| | `tengu_bramble_lintel` | null→1 | Extraction throttle: run every N eligible turns | | ||
| | `tengu_passport_quail` | false | Master gate for extractMemories execution | | ||
| | `tengu_slate_thimble` | false | Allow extraction in non-interactive (--no-input) | | ||
| | `tengu_coral_fern` | false | "Searching past context" grep instructions | | ||
| | `tengu_herring_clock` | false | Team-memory feature gate | | ||
| | `tengu_session_memory` | false | Session memory (conversation notes between turns) | | ||
| | `tengu_sedge_lantern` | false | Away-summary (5-min blur → recap) | | ||
| | `tengu_moth_copse` | false | Skip MEMORY.md index, show topic files only | | ||
| src/memdir/memdir.ts 证明 entrypoint memory 如何进入 prompt;src/memdir/findRelevantMemories.ts 证明 relevance side-query 确实存在;src/services/extractMemories/extractMemories.ts 证明自动提取与受限 tool 权限;src/services/autoDream/autoDream.ts 证明后台 consolidation orchestration。03 章描述的是能力版图,12 章才是逐步 dataflow trace。 | ||
| ## Env Vars | ||
| ### Memory Type 完整定义 (memoryTypes.ts:15-120) | ||
| ``` | ||
| CLAUDE_CODE_DISABLE_AUTO_MEMORY — 1/true=OFF, 0/false=ON | ||
| CLAUDE_COWORK_MEMORY_EXTRA_GUIDELINES — injected into all memory prompts | ||
| CLAUDE_COWORK_MEMORY_PATH_OVERRIDE — full override for memory dir path | ||
| CLAUDE_CODE_REMOTE_MEMORY_DIR — CCR remote memory mount point | ||
| CLAUDE_CODE_SIMPLE (--bare) — disables auto-memory entirely | ||
| ``` | ||
| // 严格 4 类型分类法 (closed taxonomy) 1\. USER — 用户角色、目标、知识、偏好 → ALWAYS private (隐私数据) 2\. FEEDBACK — 方法指导、规则、纠正 (如 "少用 emoji") → default private, team for conventions 3\. PROJECT — 工作状态、bug、计划、deadline → bias toward team (跨 agent 共享) 4\. REFERENCE — 外部知识,非代码/git 可推导的 // 明确排除: 代码 patterns、architecture、git history、file structure // → 这些都可以从 codebase 推导,不需要占 memory 空间 // Frontmatter 格式: \--- type: user|feedback|project|reference description: One-line summary for recall selector \--- # Actual memory content | ||
| --- | ||
| ### Memory 路径安全验证 (paths.ts:92) | ||
| ## Memory Types (`memoryTypes.ts:15-120`) | ||
| validateMemoryPath(): ✗ Reject: relative paths (../) ✗ Reject: root-level paths (/) ✗ Reject: UNC paths (\\\\\\\server\\\share) ✗ Reject: null-byte injection (%00) ✗ Reject: $HOME expansion from env ✓ Allow: ~/ expansion only from settings ✓ Allow: CLAUDE_CODE_REMOTE_MEMORY_DIR isAutoMemoryEnabled() 优先级: 1\. CLAUDE_CODE_DISABLE_AUTO_MEMORY env (1/true=OFF) 2\. CLAUDE_CODE_SIMPLE (--bare mode) 3\. Remote mode without persistent storage 4\. settings.json autoMemoryEnabled field 5\. Default: ENABLED | ||
| Strict 4-type taxonomy (closed). | ||
| ### Memory Read — 静态 + 动态两层 | ||
| ``` | ||
| 1. USER — user role, goals, knowledge, preferences | ||
| → ALWAYS private (personal data) | ||
| ═══ Static Layer (system prompt, 每次都加载) ═══ MEMORY.md → truncateEntrypointContent(): 1\. Line cap: max 200 lines 2\. Byte cap: max 25KB (catches long-line indexes) 3\. Truncation at last newline boundary ═══ Dynamic Layer (per-turn) ═══ findRelevantMemories(): 1\. scanMemoryFiles(memoryDir) → MemoryHeader[] \- MAX_MEMORY_FILES = 200 \- FRONTMATTER_MAX_LINES = 30 per file \- Sorted newest-first by mtimeMs 2\. sideQuery() with Sonnet (cheap model): \- System: SELECT_MEMORIES_SYSTEM_PROMPT \- Input: manifest + current conversation context \- Output: up to 5 file paths + mtimeMs 3\. Result deduped against alreadySurfaced set 4\. Telemetry: logMemoryRecallShape() (even on empty) // Memory 注入方式: // - MEMORY.md → system prompt section (always) // - Topic files → user context messages (per-turn) // - Staleness caveat → append if age > 1 day | ||
| 2. FEEDBACK — method guidance, rules, corrections (e.g. "use fewer emojis") | ||
| → default private, team for conventions | ||
| ### Auto-Extraction 内部机制 (extractMemories.ts) | ||
| 3. PROJECT — work status, bugs, plans, deadlines | ||
| → bias toward team (shared across agents) | ||
| ═══ 触发时机 ═══ Turn ends + model 没有更多 tool calls → handleStopHooks → runForkedAgent() (共享 parent prompt cache) ═══ 互斥检查 ═══ hasMemoryWritesSince(cursor): 扫描 assistant messages 中 Write/Edit tool_use 如果 target path 是 auto-memory dir → SKIP extraction (主 agent 已经写了, 不需要重复提取) ═══ Tool 权限 (createAutoMemCanUseTool) ═══ ✅ Read, Grep, Glob — 无限制 ✅ Bash — 仅 isReadOnly() = true 的命令 ✅ Edit, Write — 仅 isAutoMemPath() 匹配的路径 ❌ 所有其他 (MCP, Agent, write-capable Bash) ═══ 提取 Prompt 结构 ═══ buildExtractAutoOnlyPrompt(): \- 列出 existing memories → 避免重复 \- 强调 "limited turn budget" \- 策略: "All reads in turn 1, all writes in turn 2" \- maxTurns = 5 buildExtractCombinedPrompt(): \- 同上 + team scope 支持 ═══ 状态隔离 ═══ initExtractMemories() — 闭包作用域 → 每个实例独立的 cursor tracking → Test-friendly (不依赖全局状态) | ||
| 4. REFERENCE — external knowledge not derivable from code/git | ||
| ### Auto-Dream 4-Phase 合并 (consolidationPrompt.ts) | ||
| // Explicitly excluded: code patterns, architecture, git history, file structure | ||
| // → These can be derived from the codebase, no need to consume memory space | ||
| ═══ Gate System (最便宜 → 最贵) ═══ Gate 1: Time — readLastConsolidatedAt() hoursSince > minHours (default 24h) Feature: tengu_onyx_plover Gate 2: Session — listSessionsTouchedSince() sessionCount > minSessions (default 5) Scan throttled: SESSION_SCAN_INTERVAL = 10min Gate 3: Lock — tryAcquireConsolidationLock() File-based mutex, 同一时间只有一个 dream 运行 ═══ 4 Phase Consolidation ═══ Phase 1 — Orient ├─ ls memory directory ├─ Read MEMORY.md index └─ Skim topic files Phase 2 — Gather recent signal ├─ Read logs/ subdirectory ├─ Grep transcripts for keywords └─ Budget: 不要在 transcripts 上耗完 read 额度 Phase 3 — Consolidate ├─ Merge signal into existing topic files ├─ Convert relative dates → absolute (yesterday → 2026-03-15) └─ Delete contradicted facts at source Phase 4 — Prune & Index ├─ Keep MEMORY.md under 200 lines ├─ One-line entries (~150 chars): │ "[Title](file.md) — hook" └─ Remove stale/superseded pointers | ||
| // Frontmatter format: | ||
| --- | ||
| type: user|feedback|project|reference | ||
| description: One-line summary for recall selector | ||
| --- | ||
| # Actual memory content | ||
| ``` | ||
| ### Memory CRUD — Model 驱动,全权控制 | ||
| --- | ||
| // Model 通过 FileWrite/FileEdit 工具直接操作 写入新 memory: FileWrite ~/claude/projects/{slug}/memory/topic.md (带 frontmatter: type + description) 更新 memory: FileEdit 已有文件 (append section / replace) 删除 memory: Write empty file 或 Bash rm 更新 index: Model 手动 append/edit MEMORY.md (one-line pointers) // 没有专门的 "memory tool" — 复用文件操作 tools // 简洁但依赖 model 知道 memory 目录结构 // extraction agent 也用同样的 FileWrite 写入 | ||
| ## Memory Path Security Validation (`paths.ts:92`) | ||
| I | ||
| ```typescript | ||
| validateMemoryPath(): | ||
| ✗ Reject: relative paths (../) | ||
| ✗ Reject: root-level paths (/) | ||
| ✗ Reject: UNC paths (\\server\share) | ||
| ✗ Reject: null-byte injection (%00) | ||
| ✗ Reject: $HOME expansion from env | ||
| ✓ Allow: ~/ expansion only from settings | ||
| ✓ Allow: CLAUDE_CODE_REMOTE_MEMORY_DIR | ||
| ## Comparison | ||
| isAutoMemoryEnabled() priority: | ||
| 1. CLAUDE_CODE_DISABLE_AUTO_MEMORY env (1/true=OFF) | ||
| 2. CLAUDE_CODE_SIMPLE (--bare mode) | ||
| 3. Remote mode without persistent storage | ||
| 4. settings.json autoMemoryEnabled field | ||
| 5. Default: ENABLED | ||
| ``` | ||
| Claude Code | ||
| --- | ||
| * **Filesystem storage** — ~/.claude/memory/*.md, frontmatter + body | ||
| * **4 types** : user / feedback / project / reference,每种有不同 recall 策略 | ||
| * **MEMORY.md index** — 200 lines cap,always loaded into context | ||
| * **Auto-extraction** — 每 N 轮用 restricted forked agent 提取,不等 SESSION_END | ||
| * **Cursor tracking** — lastMemoryMessageUuid 避免重复处理 | ||
| * **LLM relevance** — Sonnet side-query 从 manifest 中选 top 5 | ||
| * **Staleness warning** — >1 day old 加 system-reminder caveat | ||
| * **Restricted tool access** — extraction agent 只能 Read/Grep + 写 memory 目录 | ||
| * **Skip if already written** — 主 agent 已写 memory 就跳过 auto-extraction | ||
| ## Memory Read — Static + Dynamic Two Layers | ||
| Mitsein | ||
| ```typescript | ||
| ═══ Static Layer (system prompt, loaded every turn) ═══ | ||
| MEMORY.md → truncateEntrypointContent(): | ||
| 1. Line cap: max 200 lines | ||
| 2. Byte cap: max 25KB (catches long-line indexes) | ||
| 3. Truncation at last newline boundary | ||
| * **DB storage** — agent_memory 表 (user_id, slug, body, category) | ||
| * **Free-text category** : 未 enum 化 | ||
| * **SESSION_END hook** → 单次写入 memory | ||
| * 无自动提取 — 只在 session 结束时才保存 | ||
| * 无 relevance scoring — recall 时拉全部 or 按 category 过滤 | ||
| * 无 staleness 管理 — 过时 memory 与新 memory 同等对待 | ||
| * **memory-organizer daemon** — daily cron 做合并/归档 | ||
| * **跨 agent/thread/channel** — MemoryCard 是全局认知层 | ||
| ═══ Dynamic Layer (per-turn) ═══ | ||
| findRelevantMemories(): | ||
| 1. scanMemoryFiles(memoryDir) → MemoryHeader[] | ||
| - MAX_MEMORY_FILES = 200 | ||
| - FRONTMATTER_MAX_LINES = 30 per file | ||
| - Sorted newest-first by mtimeMs | ||
| 2. sideQuery() with Sonnet (cheap model): | ||
| - System: SELECT_MEMORIES_SYSTEM_PROMPT | ||
| - Input: manifest + current conversation context | ||
| - Output: up to 5 file paths + mtimeMs | ||
| 3. Result deduped against alreadySurfaced set | ||
| 4. Telemetry: logMemoryRecallShape() (even on empty) | ||
| II | ||
| // Memory injection: | ||
| // - MEMORY.md → system prompt section (always) | ||
| // - Topic files → user context messages (per-turn) | ||
| // - Staleness caveat → appended if age > 1 day | ||
| ``` | ||
| ## How to Implement | ||
| --- | ||
| #### Step 1: Memory type enum | ||
| ## Auto-Extraction Internals (`extractMemories.ts`) | ||
| agent_memory.category → enum 化,每种类型有明确的 recall 权重和 staleness 策略。 | ||
| ```typescript | ||
| ═══ Trigger Timing ═══ | ||
| Turn ends + model has no more tool calls → handleStopHooks | ||
| → runForkedAgent() (shares parent prompt cache) | ||
| class MemoryType(str, Enum): USER = "user" # role, preferences, knowledge FEEDBACK = "feedback" # "don't do X" + why PROJECT = "project" # deadlines, decisions (stales fast) REFERENCE = "reference" # pointers to external systems # Migration ALTER TABLE agent_memory ALTER COLUMN category TYPE text; \-- Then validate existing data and map to enum values | ||
| ═══ Mutex Check ═══ | ||
| hasMemoryWritesSince(cursor): | ||
| Scan assistant messages for Write/Edit tool_use | ||
| If target path is auto-memory dir → SKIP extraction | ||
| (main agent already wrote, no need to extract again) | ||
| #### Step 2: Continuous extraction (every N turns) | ||
| ═══ Tool Permissions (createAutoMemCanUseTool) ═══ | ||
| ✅ Read, Grep, Glob — unrestricted | ||
| ✅ Bash — only isReadOnly() = true commands | ||
| ✅ Edit, Write — only paths matching isAutoMemPath() | ||
| ❌ All others (MCP, Agent, write-capable Bash) | ||
| 不等 SESSION_END。每 3-5 轮对话后,用一个 restricted context 提取 memory。 | ||
| ═══ Extraction Prompt Structure ═══ | ||
| buildExtractAutoOnlyPrompt(): | ||
| - List existing memories → avoid duplicates | ||
| - Emphasize "limited turn budget" | ||
| - Strategy: "All reads in turn 1, all writes in turn 2" | ||
| - maxTurns = 5 | ||
| class MemoryExtractor: def __init__(self, thread_id: str, user_id: str): self._cursor: str | None = None # last processed message id self._turns_since_extract = 0 self._extract_interval = 3 # every 3 turns async def maybe_extract(self, messages: list[Message]): self._turns_since_extract += 1 if self._turns_since_extract < self._extract_interval: return new_messages = self._get_messages_since_cursor(messages) if not new_messages: return # Skip if main agent already wrote memory this round if self._has_memory_writes(new_messages): self._advance_cursor(messages) return self._turns_since_extract = 0 # Get existing memory manifest for dedup existing = await memory_repo.list_for_user(self.user_id) manifest = self._format_manifest(existing) # Run extraction with restricted permissions extracted = await self._run_extraction_agent( new_messages, manifest ) for mem in extracted: await memory_repo.upsert( user_id=self.user_id, slug=mem.slug, body=mem.body, category=mem.category, # now enum ) self._advance_cursor(messages) | ||
| buildExtractCombinedPrompt(): | ||
| - Same as above + team scope support | ||
| #### Step 3: LLM-based relevance scoring | ||
| ═══ State Isolation ═══ | ||
| initExtractMemories() — closure scope | ||
| → Each instance has independent cursor tracking | ||
| → Test-friendly (no global state) | ||
| ``` | ||
| Recall 时用 cheap model 选 top-N 相关 memories,不需要 vector DB。 | ||
| --- | ||
| async def recall_relevant_memories( user_id: str, query: str, max_results: int = 5 ) -> list[MemoryCard]: all_memories = await memory_repo.list_for_user(user_id) if not all_memories: return [] # Build manifest: slug + description + type + age manifest = "\n".join( f"- {m.slug} ({m.category}): {m.title} [{days_old(m)}d old]" for m in all_memories ) # Side-query to cheap model response = await llm.query( model="claude-sonnet", system="Select up to 5 most relevant memories for the query. " "Return JSON: {\"selected\": [\"slug1\", \"slug2\"]}", messages=[{"role": "user", "content": f"Query: {query}\n\nMemories:\n{manifest}"}], max_tokens=200, ) selected_slugs = parse_json(response)["selected"] return [m for m in all_memories if m.slug in selected_slugs] | ||
| ## Auto-Dream 4-Phase Consolidation (`consolidationPrompt.ts`) | ||
| #### Step 4: Staleness warning injection | ||
| ```typescript | ||
| ═══ Gate System (cheapest → most expensive) ═══ | ||
| Gate 1: Time — readLastConsolidatedAt() | ||
| hoursSince > minHours (default 24h) | ||
| Feature: tengu_onyx_plover | ||
| Recall 时对老 memory 加 caveat,防止 agent 基于过时信息行动。 | ||
| Gate 2: Session — listSessionsTouchedSince() | ||
| sessionCount > minSessions (default 5) | ||
| Scan throttled: SESSION_SCAN_INTERVAL = 10min | ||
| def attach_memory_to_context(memory: MemoryCard) -> str: age_days = (datetime.now() - memory.updated_at).days body = memory.body if age_days > 7 and memory.category == MemoryType.PROJECT: body += ( f"\n\n⚠️ This memory is {age_days} days old. " "Project context changes quickly — verify before acting." ) elif age_days > 30: body += ( f"\n\n⚠️ This memory is {age_days} days old. " "It may be outdated — verify current state before recommending." ) return body | ||
| Gate 3: Lock — tryAcquireConsolidationLock() | ||
| File-based mutex, only one dream runs at a time | ||
| #### Step 5: Upgrade memory-organizer daemon | ||
| ═══ 4 Phase Consolidation ═══ | ||
| Phase 1 — Orient | ||
| ├─ ls memory directory | ||
| ├─ Read MEMORY.md index | ||
| └─ Skim topic files | ||
| Daily cron 增加 staleness 标记、重复检测、type 验证。 | ||
| Phase 2 — Gather recent signal | ||
| ├─ Read logs/ subdirectory | ||
| ├─ Grep transcripts for keywords | ||
| └─ Budget: don't exhaust read quota on transcripts | ||
| async def organize_memories(user_id: str): memories = await memory_repo.list_for_user(user_id) # 1. Detect duplicates (similar body, different slug) duplicates = find_similar_pairs(memories, threshold=0.9) for dup_pair in duplicates: await merge_memories(dup_pair) # 2. Archive very old project memories (>90 days) for m in memories: if m.category == MemoryType.PROJECT: if days_old(m) > 90: await memory_repo.archive(m.slug) # 3. Validate type field for m in memories: if m.category not in MemoryType.__members__.values(): await memory_repo.update(m.slug, category=MemoryType.REFERENCE) | ||
| Phase 3 — Consolidate | ||
| ├─ Merge signal into existing topic files | ||
| ├─ Convert relative dates → absolute (yesterday → 2026-03-15) | ||
| └─ Delete contradicted facts at source | ||
| III | ||
| Phase 4 — Prune & Index | ||
| ├─ Keep MEMORY.md under 200 lines | ||
| ├─ One-line entries (~150 chars): | ||
| │ "[Title](file.md) — hook" | ||
| └─ Remove stale/superseded pointers | ||
| ``` | ||
| ## How to Verify | ||
| --- | ||
| ### Unit Tests | ||
| ## Memory CRUD — Model-Driven, Full Control | ||
| * **test_extraction_cursor_tracking** — 提取后再添加 2 条 messages,第二次提取只处理新增的 2 条 | ||
| * **test_skip_if_already_written** — 主 agent 在对话中写了 memory,extractor 跳过本轮 | ||
| * **test_relevance_top_n** — 给定 20 条 memories + query "做PPT",返回 PPT 相关的 top 5 | ||
| * **test_staleness_warning** — 30 天前的 memory attach 时带 ⚠️ 标记 | ||
| * **test_project_memory_staleness_7d** — PROJECT 类型 7 天就加 warning(比其他类型更严格) | ||
| * **test_organizer_merges_duplicates** — 两条 body 相似度 >90% 的 memory 被 merge | ||
| * **test_type_enum_validation** — 无效 category 被修正为 REFERENCE | ||
| ```typescript | ||
| // Model operates directly via FileWrite/FileEdit tools | ||
| ### Integration Tests | ||
| Write new memory: | ||
| FileWrite ~/claude/projects/{slug}/memory/topic.md | ||
| (with frontmatter: type + description) | ||
| * **test_cross_channel_memory** — 微信 thread 写入 memory "跳槽决定",Web thread recall 时能看到 | ||
| * **test_continuous_extraction** — 与 PPT Agent 对话 6 轮,验证第 3 轮后自动提取了 memory | ||
| * **test_recall_affects_output** — 第 1 次说 "少用 emoji",第 5 次 recall 后输出确实没有 emoji | ||
| Update memory: | ||
| FileEdit existing file (append section / replace) | ||
| IV | ||
| Delete memory: | ||
| Write empty file OR Bash rm | ||
| ## User Perception | ||
| Update index: | ||
| Model manually appends/edits MEMORY.md (one-line pointers) | ||
| Before | ||
| // No dedicated "memory tool" — reuses file operation tools | ||
| // Concise but depends on model knowing memory directory structure | ||
| // Extraction agent uses same FileWrite for writes | ||
| ``` | ||
| 用户 (第 1 次): 帮我写小红书,少用 emoji,口语化 | ||
| --- | ||
| 小红书 Agent: [文案 v1,符合要求] | ||
| ## Extraction Restricted Tool Access (`extractMemories.ts:171-222`) | ||
| [SESSION_END → memory 写入] | ||
| 用户 (第 5 次): 帮我写小红书 | ||
| 小红书 Agent: 🎉✨ 今天给大家分享... | ||
| [全是 emoji...memory 没被 recall,或 recall 了全部 memory 淹没了关键偏好] | ||
| After | ||
| 用户 (第 1 次): 帮我写小红书,少用 emoji,口语化 | ||
| 小红书 Agent: [文案 v1,符合要求] | ||
| [对话进行中 → auto-extract: feedback memory "少用emoji, 口语化"] | ||
| 用户 (第 5 次): 帮我写小红书 | ||
| [relevance query → top memory: "xhs-style-pref"] | ||
| 小红书 Agent: 今天聊聊我最近踩的一个坑... | ||
| [无 emoji,口语化 ✓ — memory 生效] | ||
| **核心体感差异:** "Agent 记住了我的偏好"。不需要每次重复说明。feedback 类型 memory 自动 recall + 影响输出风格。Project 类型 memory 有 staleness 保护,不会基于过时信息行动。 | ||
| ### 用户可感知的变化 | ||
| * **记忆生效** — 说过一次的偏好,后续自动应用 | ||
| * **不打扰** — 自动提取,用户无感(不再需要 "保存到记忆" 按钮) | ||
| * **跨渠道同步** — 微信说的事,Web 端也知道 | ||
| * **时效保护** — 不会因为 30 天前的 memory 给出错误建议 | ||
| MemoryCard agent_memory auto-extraction relevance staleness | ||
| ``` | ||
| ALLOWED unrestricted: Read, Grep, Glob, REPL | ||
| ALLOWED restricted: Bash (read-only: ls/find/grep/cat/stat/wc/head/tail) | ||
| ALLOWED memory-only: Edit, Write (ONLY inside auto-memory directory) | ||
| DENIED: All others (MCP, Agent, write-capable Bash, etc.) | ||
| ``` |
+206
-96
@@ -1,157 +0,267 @@ | ||
| [← Back to Index](index.html) | ||
| # 04. Hook / Lifecycle Engine | ||
| # 04\. Hook / Lifecycle Engine High | ||
| > 23 lifecycle events, 5 hook types, pattern-matched execution. | ||
| 从 SESSION_END 单点到 15+ lifecycle events 的扩展体系 | ||
| ## Key Files | ||
| R | ||
| ``` | ||
| src/utils/hooks.ts — execution engine (~4100 lines, L1952+) | ||
| src/utils/hooks/hooksConfigManager.ts — hook settings loader | ||
| src/utils/hooks/hookEvents.ts — enable/disable events | ||
| src/utils/hooks/postSamplingHooks.ts — post-sampling hook registry | ||
| src/schemas/hooks.ts — command/prompt/http/agent schemas | ||
| src/types/hooks.ts — HookCallback (L211), HookCallbackContext | ||
| ``` | ||
| ## Feature Reference [→ Full Flag Index](00-feature-discovery.html#flags-build) | ||
| --- | ||
| ### Hook Events (23 total) | ||
| ## Hook Events (23 total) | ||
| // Sync (blocking) events: PreToolUse PostToolUse PostToolUseFailure UserPromptSubmit SessionStart SessionEnd Stop StopFailure Setup PermissionRequest PermissionDenied PreCompact PostCompact Elicitation ElicitationResult ConfigChange InstructionsLoaded // Async (fire-and-forget) events: Notification SubagentStart SubagentStop TeammateIdle TaskCreated TaskCompleted CwdChanged FileChanged WorktreeCreate WorktreeRemove | ||
| ```typescript | ||
| // Sync (blocking) events: | ||
| PreToolUse PostToolUse PostToolUseFailure | ||
| UserPromptSubmit SessionStart SessionEnd | ||
| Stop StopFailure Setup | ||
| PermissionRequest PermissionDenied PreCompact | ||
| PostCompact Elicitation ElicitationResult | ||
| ConfigChange InstructionsLoaded | ||
| ### Hook Command Types (5) | ||
| // Async (fire-and-forget) events: | ||
| Notification SubagentStart SubagentStop | ||
| TeammateIdle TaskCreated TaskCompleted | ||
| CwdChanged FileChanged WorktreeCreate | ||
| WorktreeRemove | ||
| ``` | ||
| command — shell command (bash/powershell) prompt — LLM-evaluated (small fast model) agent — agentic verification (sub-agent) http — HTTP POST to external endpoint callback — programmatic (internal) | ||
| ## Hook Command Types (5) | ||
| ### Constants | ||
| ``` | ||
| command — shell command (bash/powershell) | ||
| prompt — LLM-evaluated (small fast model) | ||
| agent — agentic verification (sub-agent) | ||
| http — HTTP POST to external endpoint | ||
| callback — programmatic (internal only) | ||
| ``` | ||
| TOOL_HOOK_EXECUTION_TIMEOUT_MS = 600_000 // 10 minutes (src/utils/hooks.ts:166) SessionEnd timeout = configurable via CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS (default 1.5s) // src/utils/hooks.ts:175 SLOW_PHASE_LOG_THRESHOLD_MS = 2_000 // 2 seconds (src/services/tools/toolExecution.ts:137, NOT hooks.ts) | ||
| --- | ||
| ### PreToolUse Hook Response | ||
| ## Constants | ||
| hookSpecificOutput: { permissionDecision: 'allow' | 'deny' | 'ask' // override permission permissionDecisionReason: string // explanation updatedInput: Record<string, unknown> // modify tool input additionalContext: string // inject context for model } | ||
| ```typescript | ||
| TOOL_HOOK_EXECUTION_TIMEOUT_MS = 600_000 // 10 minutes | ||
| SLOW_PHASE_LOG_THRESHOLD_MS = 2_000 // 2 seconds | ||
| SessionEnd timeout = configurable via CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS | ||
| // default 1.5s | ||
| ``` | ||
| ### Feature Flags: HOOK_PROMPTS | ||
| --- | ||
| // Build-time HOOK_PROMPTS — prompt-type hooks in frontmatter // Env vars CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS — override SessionEnd timeout CLAUDE_CODE_SAVE_HOOK_ADDITIONAL_CONTEXT — save extra hook context | ||
| ## Feature Flags & Env Vars | ||
| ### Key Files | ||
| ``` | ||
| // Build-time | ||
| HOOK_PROMPTS — prompt-type hooks in frontmatter | ||
| src/utils/hooks.ts — execution engine (~4100 lines, L1952+) src/utils/hooks/hooksConfigManager.ts — hook settings loader src/utils/hooks/hookEvents.ts — enable/disable events src/utils/hooks/postSamplingHooks.ts — post-sampling hook registry src/schemas/hooks.ts — command/prompt/http/agent schemas src/types/hooks.ts — HookCallback (L211), HookCallbackContext | ||
| // Env vars | ||
| CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS — override SessionEnd timeout | ||
| CLAUDE_CODE_SAVE_HOOK_ADDITIONAL_CONTEXT — save extra hook context | ||
| ``` | ||
| ### ALL Hook Events — 完整生命周期 (SDK types) | ||
| --- | ||
| ═══ Tool Lifecycle ═══ PreToolUse — tool 选中,执行前 (pattern match, permission override) PostToolUse — 成功完成后 PostToolUseFailure — tool 出错后 PermissionRequest — permission ask 前 PermissionDenied — permission 被拒后 ═══ Model Interaction ═══ UserPromptSubmit — 用户消息后,model call 前 SessionStart — session 启动 Setup — 初始化 (agent/plugin init) SessionEnd — session 清理 ═══ Sampling & Control ═══ Stop — Stop tool 执行前 StopFailure — Stop hook 失败 ═══ Agents ═══ SubagentStart — subagent spawned SubagentStop — subagent 完成 ═══ Compaction ═══ PreCompact — 压缩前 PostCompact — 压缩后 ═══ File/Workspace ═══ CwdChanged — 工作目录切换 FileChanged — watched file 被修改 WorktreeCreate — worktree 创建 ═══ Notifications & State ═══ Notification — 通用事件 TaskCreated — task spawned TaskCompleted — task 完成 TeammateIdle — 等待用户输入 ConfigChange — settings 修改 InstructionsLoaded — agent/skill frontmatter loaded ═══ MCP ═══ Elicitation — MCP server 请求用户输入 ElicitationResult — 用户对 elicitation 的回答 | ||
| ## All Hook Events — Complete Lifecycle | ||
| ### 5 种 Hook 类型 Schema (schemas/hooks.ts) | ||
| ```typescript | ||
| ═══ Tool Lifecycle ═══ | ||
| PreToolUse — tool selected, before execution (pattern match, permission override) | ||
| PostToolUse — successful completion | ||
| PostToolUseFailure — tool errored | ||
| PermissionRequest — before permission ask | ||
| PermissionDenied — after permission denied | ||
| Command — shell 命令 { command, shell: 'bash'|'powershell', timeout, statusMessage, once: boolean, async: boolean, asyncRewake: boolean } Prompt — LLM prompt (side-query) { prompt, model, timeout, once } HTTP — HTTP 请求 { url, headers, allowedEnvVars, once } Agent — 验证 agent (spawns agent for verification) { prompt (verification task), model, timeout, once } Callback — SDK-only, programmatic function (HookCallback type, 不存储在 config) ALL types 支持 'if' condition: { if: "Bash(git *)" } — permission rule 语法 | ||
| ═══ Model Interaction ═══ | ||
| UserPromptSubmit — after user message, before model call | ||
| SessionStart — session startup | ||
| Setup — initialization (agent/plugin init) | ||
| SessionEnd — session cleanup | ||
| ### Pattern Matching — PreToolUse 的 if 条件 | ||
| ═══ Sampling & Control ═══ | ||
| Stop — before Stop tool executes | ||
| StopFailure — Stop hook failed | ||
| matchesPattern() (L1634): "Write" → exact match (normalize legacy names) "Write|Edit" → pipe-separated, multiple exact matches "^Write.*" → regex, full evaluation if condition matching (L1714): Syntax: "Bash(git *)", "Read(*.ts)" 1\. Tool-name matched (normalizeLegacyToolName + regex) 2\. Input pattern matched (if provided) 3\. prepareIfConditionMatcher() 预编译 Deduplication (L2027-2090): Key = {pluginRoot}\0{shell/prompt/url}\0{if-condition} 碰撞: last-merged scope wins (project覆盖user) Callback hooks: 跳过去重 (each is unique) | ||
| ═══ Agents ═══ | ||
| SubagentStart — subagent spawned | ||
| SubagentStop — subagent completed | ||
| ### Hook 执行生命周期 (executeHooks L2153+) | ||
| ═══ Compaction ═══ | ||
| PreCompact — before compaction | ||
| PostCompact — after compaction | ||
| 1\. Config Assembly — getHooksConfig() (L1563) ├─ Merge: configSnapshot + registered(SDK) + sessionHooks └─ Check: managedOnly vs custom hooks policy 2\. Trust Check — shouldSkipHookDueToTrust() (L309) ├─ ALL hooks require workspace trust (defense-in-depth) ├─ Non-interactive (SDK): trust implicit └─ Interactive: checkHasTrustDialogAccepted() 3\. Matching — getMatchingHooks() (L1783) ├─ Build matchQuery from hook input ├─ Filter by matcher pattern ├─ Apply if conditions └─ Deduplicate 4\. Serial Execution — 按顺序,每个 event 一次: ├─ Command: execCommandHook() (L2294) ├─ Prompt: execPromptHook() ├─ Agent: execAgentHook() └─ HTTP: execHttpHook() 5\. Timeout Handling: TOOL_HOOK_EXECUTION_TIMEOUT_MS = 10min (L220) SESSION_END_HOOK_TIMEOUT_MS = 1500ms (L224, env override) Per-hook: hook.timeout (seconds) 6\. Output Parsing: Command → parseHookOutput() (L489) — JSON or plain text HTTP → parseHttpHookOutput() (L546) — JSON only Validation: hookJSONOutputSchema() Zod (L481) | ||
| ═══ File/Workspace ═══ | ||
| CwdChanged — working directory switched | ||
| FileChanged — watched file modified | ||
| WorktreeCreate — worktree created | ||
| ### Async Hook 机制 | ||
| ═══ Notifications & State ═══ | ||
| Notification — generic event | ||
| TaskCreated — task spawned | ||
| TaskCompleted — task completed | ||
| TeammateIdle — waiting for user input | ||
| ConfigChange — settings modified | ||
| InstructionsLoaded — agent/skill frontmatter loaded | ||
| ═══ Config-based async (L2444-2465) ═══ hook.async = true → 后台执行,不等结果 hook.asyncRewake = true → 后台执行,完成后唤醒 ═══ Runtime async detection (L2570-2600) ═══ Hook 第一行输出 {"async": true, asyncTimeout?: N} → 自动转为后台执行 (executeInBackground) → Parser 只检查 first line ═══ asyncRewake 行为 (L153-167) ═══ exit code 2 (blocking error) → 注入 task-notification → Wakes model via useQueueProcessor (idle 时) → 或 queued_command attachments (busy 时) ═══ Command hook 执行细节 ═══ Shell resolution: hook.shell → DEFAULT_HOOK_SHELL (bash) Windows: Git Bash (findGitBashPath), POSIX path conversion PowerShell: -NoProfile -NonInteractive -Command Env inject: CLAUDE_PROJECT_DIR, CLAUDE_PLUGIN_ROOT, CLAUDE_ENV_FILE Stdin: JSON input + newline Stdout/stderr: in-memory via TaskOutput | ||
| ═══ MCP ═══ | ||
| Elicitation — MCP server requesting user input | ||
| ElicitationResult — user response to elicitation | ||
| ``` | ||
| ### Hook 输出 Schema (影响 permission 决策) | ||
| --- | ||
| SyncHookJSONOutput = { continue?: boolean // default true, false = stop suppressOutput?: boolean // hide stdout from user stopReason?: string // if continue=false decision?: "approve"|"block" // PreToolUse override reason?: string // explanation systemMessage?: string // warning to user hookSpecificOutput?: {...} // event-specific data } // PreToolUse permission decisions: permissionDecision: "allow" | "deny" | "ask" updatedInput: { ... } → override tool call parameters! // ⚠️ Hook 可以改 tool 的 input — 强大但危险 // Post-Sampling Hooks (内部用): registerPostSamplingHook(fn) → 在 model sampling 后、tool call 前执行 → 用于: extractMemories, autoDream, skill improvement → Error logged but don't fail execution | ||
| ## 5 Hook Type Schemas (`schemas/hooks.ts`) | ||
| I | ||
| ```typescript | ||
| Command: | ||
| { command, shell: 'bash'|'powershell', timeout, statusMessage, | ||
| once: boolean, async: boolean, asyncRewake: boolean } | ||
| ## Comparison | ||
| Prompt: | ||
| { prompt, model, timeout, once } | ||
| Claude Code | ||
| HTTP: | ||
| { url, headers, allowedEnvVars, once } | ||
| * **15+ lifecycle events** : PreToolUse, PostToolUse, SessionStart/End, SubagentStart/Stop, PreCompact, FileChanged... | ||
| * **5 hook types** : command (shell), prompt (LLM), agent (sub-agent verify), http (POST), callback (programmatic) | ||
| * **PreToolUse can** : block, modify input, inject context, override permission | ||
| * **PostToolUse can** : modify output, add annotations | ||
| * **Pattern matching** : "Bash(git *)", "Write(*.ts)" — 避免 fire 无关 hooks | ||
| * **Parallel execution** : 多个 hooks 并行执行,各自有 timeout | ||
| * **Deduplication** : 按 hook type + command + if 去重 | ||
| * **Skills can register hooks** : skill frontmatter 带 hooks 配置 | ||
| Agent: | ||
| { prompt (verification task), model, timeout, once } | ||
| Mitsein | ||
| Callback: | ||
| // SDK-only, programmatic function (HookCallback type, not stored in config) | ||
| * **HookEngine** : SESSION_END → Memory 写入(唯一 hook point) | ||
| * 无 pre/post tool execution hooks | ||
| * 无 pattern matching — 无法只 watch 特定 tool | ||
| * 无外部扩展点 — 用户/开发者不能注册自定义 hook | ||
| * 进化机制需要更多 lifecycle signals(检测 pattern, 建议 config 变更) | ||
| // ALL types support 'if' condition: | ||
| { if: "Bash(git *)" } // permission rule syntax | ||
| ``` | ||
| II | ||
| --- | ||
| ## How to Implement | ||
| ## PreToolUse Hook Response Schema | ||
| 不需要一步到位实现 15 个 events。**优先实现 3 个:PRE_TOOL_USE + POST_TOOL_USE + AGENT_COMPLETED** ,cover 80% 扩展需求。 | ||
| ```typescript | ||
| hookSpecificOutput: { | ||
| permissionDecision: 'allow' | 'deny' | 'ask' // override permission | ||
| permissionDecisionReason: string // explanation | ||
| updatedInput: Record<string, unknown> // modify tool input | ||
| additionalContext: string // inject context for model | ||
| } | ||
| ``` | ||
| #### Step 1: HookEvent enum & HookResult type | ||
| --- | ||
| class HookEvent(str, Enum): PRE_TOOL_USE = "pre_tool_use" POST_TOOL_USE = "post_tool_use" AGENT_COMPLETED = "agent_completed" SESSION_START = "session_start" SESSION_END = "session_end" # existing PRE_COMPACT = "pre_compact" USER_MESSAGE = "user_message" @dataclass class HookResult: continue_execution: bool = True block_reason: str | None = None updated_input: dict | None = None # for PRE_TOOL_USE updated_output: Any | None = None # for POST_TOOL_USE additional_context: str | None = None permission_override: str | None = None # "allow" / "deny" / "ask" | ||
| ## Pattern Matching — PreToolUse `if` Conditions (`L1634`) | ||
| #### Step 2: Hook registration with matcher | ||
| ```typescript | ||
| matchesPattern(): | ||
| "Write" → exact match (normalize legacy names) | ||
| "Write|Edit" → pipe-separated, multiple exact matches | ||
| "^Write.*" → regex, full evaluation | ||
| @dataclass class HookRegistration: event: HookEvent handler: Callable[..., Awaitable[HookResult]] matcher: str | None = None # "xhs_publish", "Bash(git *)", "*" timeout_seconds: float = 30.0 run_async: bool = False # fire-and-forget class HookEngine: _hooks: dict[HookEvent, list[HookRegistration]] = defaultdict(list) def register(self, hook: HookRegistration): self._hooks[hook.event].append(hook) def _matches(self, hook: HookRegistration, context: dict) -> bool: if hook.matcher is None or hook.matcher == "*": return True tool_name = context.get("tool_name", "") if "(" in hook.matcher: # Pattern: "Bash(git *)" → match tool + input match = re.match(r"(\w+)\\((.+)\\)", hook.matcher) if match: return (tool_name == match.group(1) and fnmatch(context.get("input_summary", ""), match.group(2))) return tool_name in hook.matcher.split("|") | ||
| // if condition matching (L1714): | ||
| // Syntax: "Bash(git *)", "Read(*.ts)" | ||
| // 1. Tool-name matched (normalizeLegacyToolName + regex) | ||
| // 2. Input pattern matched (if provided) | ||
| // 3. prepareIfConditionMatcher() precompiles | ||
| #### Step 3: Hook execution (parallel with timeout) | ||
| // Deduplication (L2027-2090): | ||
| // Key = {pluginRoot}\0{shell/prompt/url}\0{if-condition} | ||
| // Collision: last-merged scope wins (project overrides user) | ||
| // Callback hooks: skip dedup (each is unique) | ||
| ``` | ||
| class HookEngine: async def fire(self, event: HookEvent, context: dict) -> HookResult: matching = [h for h in self._hooks[event] if self._matches(h, context)] if not matching: return HookResult() # Separate sync (blocking) and async (fire-and-forget) hooks sync_hooks = [h for h in matching if not h.run_async] async_hooks = [h for h in matching if h.run_async] # Fire async hooks (don't wait) for h in async_hooks: asyncio.create_task(self._safe_execute(h, context)) # Execute sync hooks in parallel, with individual timeouts results = await asyncio.gather(*[ asyncio.wait_for( self._safe_execute(h, context), timeout=h.timeout_seconds ) for h in sync_hooks ], return_exceptions=True) # Merge results: any block → block; collect all updates return self._merge_results(results) | ||
| --- | ||
| #### Step 4: Wire into orchestrator tool execution | ||
| ## Hook Execution Lifecycle (`executeHooks L2153+`) | ||
| # orchestrator.py — tool execution loop async def execute_tool_call(self, tool, input, context): # PRE_TOOL_USE hooks pre_result = await self.hook_engine.fire( HookEvent.PRE_TOOL_USE, {"tool_name": tool.name, "input": input, "input_summary": str(input)}, ) if not pre_result.continue_execution: return ToolResult(error=pre_result.block_reason) # Apply input modifications from hooks final_input = pre_result.updated_input or input # Execute tool result = await tool.execute(final_input, context) # POST_TOOL_USE hooks post_result = await self.hook_engine.fire( HookEvent.POST_TOOL_USE, {"tool_name": tool.name, "input": final_input, "output": result.data}, ) if post_result.updated_output is not None: result = ToolResult(data=post_result.updated_output) return result | ||
| ```typescript | ||
| 1. Config Assembly — getHooksConfig() (L1563) | ||
| ├─ Merge: configSnapshot + registered(SDK) + sessionHooks | ||
| └─ Check: managedOnly vs custom hooks policy | ||
| #### Step 5: Built-in hooks for Mitsein use cases | ||
| 2. Trust Check — shouldSkipHookDueToTrust() (L309) | ||
| ├─ ALL hooks require workspace trust (defense-in-depth) | ||
| ├─ Non-interactive (SDK): trust implicit | ||
| └─ Interactive: checkHasTrustDialogAccepted() | ||
| # hooks/content_safety.py async def check_publish_content(context: dict) -> HookResult: """Block publish if content violates platform rules.""" content = context["input"].get("content", "") violations = await content_policy.check(content) if violations: return HookResult( continue_execution=False, block_reason=f"内容违规: {violations[0].reason}" ) return HookResult() # Register hook_engine.register(HookRegistration( event=HookEvent.PRE_TOOL_USE, handler=check_publish_content, matcher="xhs_publish|weibo_publish", )) # hooks/evolution_tracker.py async def track_tool_usage(context: dict) -> HookResult: """Track tool usage patterns for agent evolution.""" await analytics.log_tool_use( tool=context["tool_name"], agent=context.get("agent_config_id"), duration_ms=context.get("duration_ms"), ) return HookResult() hook_engine.register(HookRegistration( event=HookEvent.POST_TOOL_USE, handler=track_tool_usage, matcher="*", run_async=True, # don't block )) | ||
| 3. Matching — getMatchingHooks() (L1783) | ||
| ├─ Build matchQuery from hook input | ||
| ├─ Filter by matcher pattern | ||
| ├─ Apply if conditions | ||
| └─ Deduplicate | ||
| III | ||
| 4. Serial Execution — one per event in order: | ||
| ├─ Command: execCommandHook() (L2294) | ||
| ├─ Prompt: execPromptHook() | ||
| ├─ Agent: execAgentHook() | ||
| └─ HTTP: execHttpHook() | ||
| ## How to Verify | ||
| 5. Timeout Handling: | ||
| TOOL_HOOK_EXECUTION_TIMEOUT_MS = 10min (L220) | ||
| SESSION_END_HOOK_TIMEOUT_MS = 1500ms (L224, env override) | ||
| Per-hook: hook.timeout (seconds) | ||
| ### Unit Tests | ||
| 6. Output Parsing: | ||
| Command → parseHookOutput() (L489) — JSON or plain text | ||
| HTTP → parseHttpHookOutput() (L546) — JSON only | ||
| Validation: hookJSONOutputSchema() Zod (L481) | ||
| ``` | ||
| * **test_pre_hook_blocks_execution** — PRE_TOOL_USE 返回 continue=False,tool 不执行 | ||
| * **test_pre_hook_modifies_input** — hook 返回 updated_input,tool 收到修改后的 input | ||
| * **test_post_hook_modifies_output** — POST_TOOL_USE 返回 updated_output,最终 result 被替换 | ||
| * **test_matcher_filtering** — matcher="xhs_publish",fire web_search 事件时不触发 | ||
| * **test_pattern_matcher** — matcher="Bash(git *)",匹配 "git push" 但不匹配 "npm install" | ||
| * **test_parallel_execution** — 3 个 hooks 并行执行,总耗时 ≈ max(单个) | ||
| * **test_timeout_handling** — hook 超时后返回默认 HookResult(不 block) | ||
| * **test_async_hook_fire_and_forget** — run_async=True 的 hook 不 block 主流程 | ||
| --- | ||
| ### Integration Tests | ||
| ## Async Hook Mechanisms | ||
| * **test_content_safety_blocks_publish** — 违规内容调 xhs_publish 被 block,返回原因 | ||
| * **test_evolution_tracking** — 执行 5 轮 tool calls,analytics 表有 5 条记录 | ||
| * **test_hook_does_not_affect_unmatched** — xhs_publish hook 不影响 web_search 执行 | ||
| ```typescript | ||
| ═══ Config-based async (L2444-2465) ═══ | ||
| hook.async = true → background execution, don't wait for result | ||
| hook.asyncRewake = true → background execution, wake when complete | ||
| IV | ||
| ═══ Runtime async detection (L2570-2600) ═══ | ||
| Hook first line output: {"async": true, asyncTimeout?: N} | ||
| → Automatically switches to background (executeInBackground) | ||
| → Parser checks first line only | ||
| ## User Perception | ||
| ═══ asyncRewake behavior (L153-167) ═══ | ||
| exit code 2 (blocking error) → inject task-notification | ||
| → Wakes model via useQueueProcessor (when idle) | ||
| → Or queued_command attachments (when busy) | ||
| Before | ||
| ═══ Command hook execution details ═══ | ||
| Shell resolution: hook.shell → DEFAULT_HOOK_SHELL (bash) | ||
| Windows: Git Bash (findGitBashPath), POSIX path conversion | ||
| PowerShell: -NoProfile -NonInteractive -Command | ||
| Env inject: CLAUDE_PROJECT_DIR, CLAUDE_PLUGIN_ROOT, CLAUDE_ENV_FILE | ||
| Stdin: JSON input + newline | ||
| Stdout/stderr: in-memory via TaskOutput | ||
| ``` | ||
| 用户: 帮我发布这篇小红书 | ||
| --- | ||
| 小红书 Agent: 好的,已发布 ✓ | ||
| ## Hook Output Schema | ||
| [内容包含敏感词...已经发出去了 😱] | ||
| ```typescript | ||
| SyncHookJSONOutput = { | ||
| continue?: boolean // default true, false = stop | ||
| suppressOutput?: boolean // hide stdout from user | ||
| stopReason?: string // if continue=false | ||
| decision?: "approve"|"block" // PreToolUse override | ||
| reason?: string // explanation | ||
| systemMessage?: string // warning to user | ||
| hookSpecificOutput?: {...} // event-specific data | ||
| } | ||
| [没有任何 pre-check 机制] | ||
| // PreToolUse permission decisions: | ||
| permissionDecision: "allow" | "deny" | "ask" | ||
| updatedInput: { ... } // ⚠️ Hook can modify tool input parameters | ||
| After | ||
| 用户: 帮我发布这篇小红书 | ||
| [PRE_TOOL_USE hook: content check → 发现敏感词] | ||
| 小红书 Agent: ⚠️ 检测到内容可能违规: "xxx" — 建议修改后再发布。 | ||
| 用户: 改好了,发布 | ||
| 小红书 Agent: 已发布 ✓ | ||
| **核心体感差异:**(1) 危险操作有安全网(pre-check);(2) 用户不需要自己记得检查合规;(3) 进化追踪自动发生,为后续 "要不要调整 Agent" 提供数据基础。用户直接感知的是"Agent 更谨慎了"。 | ||
| HookEngine PRE_TOOL_USE POST_TOOL_USE content safety evolution | ||
| // Post-Sampling Hooks (internal use): | ||
| registerPostSamplingHook(fn) | ||
| → Executes after model sampling, before tool call | ||
| → Used for: extractMemories, autoDream, skill improvement | ||
| → Errors logged but do not fail execution | ||
| ``` |
+163
-124
@@ -1,169 +0,208 @@ | ||
| [← Back to Index](index.html) | ||
| # 05. Context Management & Compression | ||
| # 05\. Context Management & Compression Critical | ||
| > Multi-phase progressive compression + Token Budget + PTL Recovery. | ||
| 多阶段渐进式压缩 + Token Budget + PTL Recovery | ||
| ## Key Files | ||
| R | ||
| ``` | ||
| src/services/compact/compact.ts — main compaction + PTL recovery | ||
| src/services/compact/autoCompact.ts — trigger conditions + thresholds | ||
| src/services/compact/microCompact.ts — tool result compaction (3 paths) | ||
| src/services/compact/timeBasedMCConfig.ts — time-based micro-compact config | ||
| src/services/compact/prompt.ts — compaction prompts (9-section) | ||
| src/services/compact/grouping.ts — groupMessagesByApiRound() | ||
| src/services/compact/postCompactCleanup.ts — re-inject files/skills | ||
| src/query/tokenBudget.ts — budget parsing + tracking | ||
| src/commands/compact/compact.ts — /compact command implementation | ||
| ``` | ||
| ## Feature Reference [→ Full Env Var Index](00-feature-discovery.html#flags-env) | ||
| --- | ||
| ### Compaction Constants | ||
| ## Constants | ||
| // src/services/compact/compact.ts POST_COMPACT_MAX_FILES_TO_RESTORE = 5 POST_COMPACT_TOKEN_BUDGET = 50_000 POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000 POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000 POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000 MAX_COMPACT_STREAMING_RETRIES = 2 // src/services/compact/autoCompact.ts AUTOCOMPACT_BUFFER_TOKENS = 13_000 // trigger threshold buffer WARNING_THRESHOLD_BUFFER_TOKENS = 20_000 // UI warning ERROR_THRESHOLD_BUFFER_TOKENS = 20_000 // UI error MANUAL_COMPACT_BUFFER_TOKENS = 3_000 // /compact blocking limit MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3 // circuit breaker MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000 // reserved for compact output // src/query/tokenBudget.ts COMPLETION_THRESHOLD = 0.9 // stop at 90% budget DIMINISHING_THRESHOLD = 500 // <500 token delta = spinning | ||
| ```typescript | ||
| // src/services/compact/compact.ts | ||
| POST_COMPACT_MAX_FILES_TO_RESTORE = 5 | ||
| POST_COMPACT_TOKEN_BUDGET = 50_000 | ||
| POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000 | ||
| POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000 | ||
| POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000 | ||
| MAX_COMPACT_STREAMING_RETRIES = 2 | ||
| ### Feature Flags | ||
| // src/services/compact/autoCompact.ts | ||
| AUTOCOMPACT_BUFFER_TOKENS = 13_000 // trigger threshold buffer | ||
| WARNING_THRESHOLD_BUFFER_TOKENS = 20_000 // UI warning | ||
| ERROR_THRESHOLD_BUFFER_TOKENS = 20_000 // UI error | ||
| MANUAL_COMPACT_BUFFER_TOKENS = 3_000 // /compact blocking limit | ||
| MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3 // circuit breaker | ||
| MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000 // reserved for compact output | ||
| `CONTEXT_COLLAPSE`| Granular context management (suppresses autocompact) | ||
| ---|--- | ||
| `REACTIVE_COMPACT`| ANT-only. API prompt-too-long triggers compaction | ||
| `CACHED_MICROCOMPACT`| ANT-only. cache_edits API to delete tool results | ||
| `HISTORY_SNIP`| Remove old messages when gap > threshold | ||
| `TOKEN_BUDGET`| Budget continuation prompt in system context | ||
| `PROMPT_CACHE_BREAK_DETECTION`| Detect cache invalidation events | ||
| ### GrowthBook & Env Vars | ||
| // src/query/tokenBudget.ts | ||
| COMPLETION_THRESHOLD = 0.9 // stop at 90% budget | ||
| DIMINISHING_THRESHOLD = 500 // <500 token delta = spinning | ||
| ``` | ||
| // GrowthBook tengu_cobalt_raccoon (false) — reactive-only compaction mode // Env vars CLAUDE_CODE_DISABLE_PRECOMPACT_SKIP — disable pre-parse optimization CLAUDE_CODE_DISABLE_1M_CONTEXT — disable 1M context (HIPAA) DISABLE_COMPACT — disable ALL compaction DISABLE_AUTO_COMPACT — disable auto (keep manual /compact) CLAUDE_CODE_AUTO_COMPACT_WINDOW — cap effective context window (tokens) CLAUDE_AUTOCOMPACT_PCT_OVERRIDE — override threshold % (0-100) CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE — override /compact blocking limit CLAUDE_CODE_MAX_CONTEXT_TOKENS — ANT-only: cap context window | ||
| --- | ||
| ### Micro-Compactable Tools | ||
| ## Feature Flags | ||
| // src/services/compact/microCompact.ts — tools whose results get compacted Read, Bash, PowerShell, Grep, Glob, WebSearch, WebFetch, Edit, Write | ||
| | Flag | Description | | ||
| |---|---| | ||
| | `CONTEXT_COLLAPSE` | Granular context management (suppresses autocompact) | | ||
| | `REACTIVE_COMPACT` | ANT-only. API prompt-too-long triggers compaction | | ||
| | `CACHED_MICROCOMPACT` | ANT-only. cache_edits API to delete tool results | | ||
| | `HISTORY_SNIP` | Remove old messages when gap > threshold | | ||
| | `TOKEN_BUDGET` | Budget continuation prompt in system context | | ||
| | `PROMPT_CACHE_BREAK_DETECTION` | Detect cache invalidation events | | ||
| ### Token Budget Parse Patterns | ||
| ## GrowthBook & Env Vars | ||
| // src/query/tokenBudget.ts — user can specify budget in prompt "+500k" → 500,000 tokens (shorthand at start) "use 2M tokens" → 2,000,000 tokens (verbose) "spend 1.5b tokens" → 1,500,000,000 tokens (verbose) "do this +500k." → 500,000 tokens (shorthand at end) Decision logic: continue: turnTokens < budget × 0.9 AND NOT diminishing diminishing: 3+ continuations AND last 2 deltas < 500 tokens stop: budget reached OR diminishing returns detected | ||
| ``` | ||
| // GrowthBook | ||
| tengu_cobalt_raccoon (false) — reactive-only compaction mode | ||
| ### Key Files | ||
| // Env vars | ||
| CLAUDE_CODE_DISABLE_PRECOMPACT_SKIP — disable pre-parse optimization | ||
| CLAUDE_CODE_DISABLE_1M_CONTEXT — disable 1M context (HIPAA) | ||
| DISABLE_COMPACT — disable ALL compaction | ||
| DISABLE_AUTO_COMPACT — disable auto (keep manual /compact) | ||
| CLAUDE_CODE_AUTO_COMPACT_WINDOW — cap effective context window (tokens) | ||
| CLAUDE_AUTOCOMPACT_PCT_OVERRIDE — override threshold % (0-100) | ||
| CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE — override /compact blocking limit | ||
| CLAUDE_CODE_MAX_CONTEXT_TOKENS — ANT-only: cap context window | ||
| ``` | ||
| src/services/compact/compact.ts — main compaction + PTL recovery src/services/compact/autoCompact.ts — trigger conditions + thresholds src/services/compact/microCompact.ts — tool result compaction (3 paths) src/services/compact/timeBasedMCConfig.ts — time-based micro-compact config src/services/compact/prompt.ts — compaction prompts (9-section) src/services/compact/grouping.ts — groupMessagesByApiRound() src/services/compact/postCompactCleanup.ts — re-inject files/skills src/query/tokenBudget.ts — budget parsing + tracking src/commands/compact/compact.ts — /compact command implementation | ||
| --- | ||
| ### Token Budget 精确计算 (autoCompact.ts) | ||
| ## Token Budget Calculation (`autoCompact.ts`) | ||
| ═══ 窗口计算 ═══ effectiveContextWindow = getContextWindowForModel(model) \- MAX_OUTPUT_TOKENS_FOR_SUMMARY (20K) // Env override: CLAUDE_CODE_AUTO_COMPACT_WINDOW ═══ 阈值 ═══ autoCompactThreshold = effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS (13K) warningThreshold = autoCompactThreshold - 20K errorThreshold = autoCompactThreshold - 20K // Env override: CLAUDE_AUTOCOMPACT_PCT_OVERRIDE (percent) ═══ Circuit Breaker ═══ MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3 → 连续 3 次 compact 失败就停止 (防止 API 浪费无限循环) | ||
| ```typescript | ||
| ═══ Window Calculation ═══ | ||
| effectiveContextWindow = getContextWindowForModel(model) | ||
| - MAX_OUTPUT_TOKENS_FOR_SUMMARY (20K) | ||
| // Env override: CLAUDE_CODE_AUTO_COMPACT_WINDOW | ||
| ### Micro-Compact — 零 LLM 成本的 Phase 1 | ||
| ═══ Thresholds ═══ | ||
| autoCompactThreshold = effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS (13K) | ||
| warningThreshold = autoCompactThreshold - 20K | ||
| errorThreshold = autoCompactThreshold - 20K | ||
| // Env override: CLAUDE_AUTOCOMPACT_PCT_OVERRIDE (percent) | ||
| ═══ Image/Document Stripping (compact.ts:49-119) ═══ stripImagesFromMessages(): image blocks → '[image]' marker document blocks → '[document]' marker text content 保留 (only media dropped) ═══ Tool Result Clearing ═══ TIME_BASED_MC_CLEARED_MESSAGE = '[Old tool result content cleared]' ═══ Time-Based Micro-Compact (tengu_slate_heron) ═══ TimeBasedMCConfig = { enabled: boolean // master switch gapThresholdMinutes: 60 // 触发条件: gap since last message keepRecent: 5 // 保留最近 N 个 tool results } 触发时机: Assistant 最后一条消息 > 60min 假设: Server cache (1h TTL) 已过期,prefix 要全部重写 效果: 在 API call 前缩减 prefix size 安全: 60min threshold > 1h cache TTL | ||
| ═══ Circuit Breaker ═══ | ||
| MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3 | ||
| // 3 consecutive compact failures → stop (prevent infinite API waste loop) | ||
| ``` | ||
| ### LLM Compaction — Sonnet Summarization | ||
| --- | ||
| compactConversation() (compact.ts:200-500): 1\. groupMessagesByApiRound() → 按 API 调用分组 2\. Fork call to Sonnet + getCompactPrompt() 3\. SystemCompactBoundaryMessage wraps output 4\. Post-cleanup: re-inject files + skills ═══ Prompt 结构 (prompt.ts) ═══ NO_TOOLS_PREAMBLE (L18): "CRITICAL: Respond with TEXT ONLY. Do NOT call any tools." (Sonnet 4.6+ 有时会无视指令调 tools — 暴力阻止) BASE_COMPACT_PROMPT (L36) — 9 sections: 1\. Primary intent (用户最终目标) 2\. Key concepts (核心概念和术语) 3\. Files (正在编辑的文件) 4\. Errors (当前错误状态) 5\. Problem solving (已尝试的方案) 6\. User messages (关键用户指令) 7\. Pending tasks (未完成的任务) 8\. Current work (当前工作快照) 9\. Next steps (下一步计划) → 要求 <analysis> 块 (草稿) + <summary> 块 PARTIAL_COMPACT_PROMPT: 保留早期 context,只 summarize 最近部分 ═══ Post-Compact Cleanup ═══ POST_COMPACT_MAX_FILES_TO_RESTORE = 5 POST_COMPACT_TOKEN_BUDGET = 50,000 tokens POST_COMPACT_MAX_TOKENS_PER_FILE = 5,000 POST_COMPACT_SKILLS_TOKEN_BUDGET = 25,000 POST_COMPACT_MAX_TOKENS_PER_SKILL = 5,000 | ||
| ## Token Budget Parse Patterns (`src/query/tokenBudget.ts`) | ||
| ### PTL Recovery 精确机制 | ||
| ``` | ||
| "+500k" → 500,000 tokens (shorthand at start) | ||
| "use 2M tokens" → 2,000,000 tokens (verbose) | ||
| "spend 1.5b tokens" → 1,500,000,000 tokens (verbose) | ||
| "do this +500k." → 500,000 tokens (shorthand at end) | ||
| ═══ Error Detection ═══ PROMPT_TOO_LONG_ERROR_MESSAGE pattern match getPromptTooLongTokenGap() → 提取精确的 token 超出量 ═══ Recovery Loop ═══ recompactIfNeeded() → 用更紧 boundaries retry MAX_COMPACT_STREAMING_RETRIES = 2 retry 完还不行 → Throw (用户需手动 compact 或 cancel) ═══ Hook 集成 ═══ executePreCompactHooks() (L400) — model-driven, can block executePostCompactHooks() (L700) — cleanup after ═══ Stripping Priority (What goes first) ═══ 1\. Images/documents → [image] / [document] marker 2\. Old tool results → [Old tool result content cleared] 3\. Entire early messages → summarize if still over 4\. Partial compaction → keep recent, truncate older | ||
| Decision logic: | ||
| continue: turnTokens < budget × 0.9 AND NOT diminishing | ||
| diminishing: 3+ continuations AND last 2 deltas < 500 tokens | ||
| stop: budget reached OR diminishing returns detected | ||
| ``` | ||
| I | ||
| --- | ||
| ## Comparison | ||
| ## Micro-Compact — Zero LLM Cost Phase 1 | ||
| Claude Code | ||
| ```typescript | ||
| ═══ Image/Document Stripping (compact.ts:49-119) ═══ | ||
| stripImagesFromMessages(): | ||
| image blocks → '[image]' marker | ||
| document blocks → '[document]' marker | ||
| text content preserved (only media dropped) | ||
| * **4-phase compaction** : pre-hooks → strip images → micro-compact → LLM summarize | ||
| * **Cheapest first** : Phase 1-2 零 LLM 成本,Phase 3 才调 model | ||
| * **Cache-safe summarization** : compact agent fork 共享 prompt cache | ||
| * **Token budget** : 90% threshold + diminishing returns detection (3 continuations + <500 token delta) | ||
| * **PTL recovery** : prompt-too-long 时按 API round 截断,保语义完整 | ||
| * **Post-compact re-injection** : 压缩后重新注入 deferred tools, MCP state | ||
| * **Micro-compaction** : 折叠低价值 messages(progress, tool_result details) | ||
| ═══ Tool Result Clearing ═══ | ||
| TIME_BASED_MC_CLEARED_MESSAGE = '[Old tool result content cleared]' | ||
| Mitsein | ||
| ═══ Time-Based Micro-Compact (tengu_slate_heron) ═══ | ||
| TimeBasedMCConfig = { | ||
| enabled: boolean // master switch | ||
| gapThresholdMinutes: 60 // trigger condition: gap since last message | ||
| keepRecent: 5 // keep N most recent tool results | ||
| } | ||
| * **ContextEngine** : compact / summarize(具体策略未详述) | ||
| * Companion: 1 thread per channel, 永续 → context 会无限膨胀 | ||
| * 无 multi-phase 策略 — 不确定是否先做低成本操作 | ||
| * 无 token budget — workflow 执行没有 token 消耗上限 | ||
| * 无 PTL recovery — prompt 超长时的 graceful degradation | ||
| * 无 diminishing returns detection — agent 可能 spin 无穷 | ||
| // Trigger: assistant's last message > 60min ago | ||
| // Assumption: server cache (1h TTL) has expired, prefix must be rewritten | ||
| // Effect: reduce prefix size before API call | ||
| // Safe: 60min threshold > 1h cache TTL | ||
| ``` | ||
| II | ||
| ## Micro-Compactable Tools (`src/services/compact/microCompact.ts`) | ||
| ## How to Implement | ||
| ``` | ||
| Read, Bash, PowerShell, Grep, Glob, WebSearch, WebFetch, Edit, Write | ||
| ``` | ||
| #### Step 1: Multi-phase compaction pipeline | ||
| --- | ||
| class ContextCompactor: async def compact(self, messages: list[Message], budget: int) -> list[Message]: current = messages current_tokens = estimate_tokens(current) # Phase 0: Pre-compact hooks hook_result = await self.hook_engine.fire(HookEvent.PRE_COMPACT, {}) extra_context = hook_result.additional_context # Phase 1: Strip media (zero LLM cost) if current_tokens > budget * 0.8: current = self._strip_media(current) current_tokens = estimate_tokens(current) # Phase 2: Micro-compact (zero LLM cost) if current_tokens > budget * 0.8: current = self._micro_compact(current) current_tokens = estimate_tokens(current) # Phase 3: LLM summarization (has cost) if current_tokens > budget * 0.8: current = await self._llm_summarize(current, extra_context) # Post: Re-inject critical context current = self._reinject_system_context(current) return current | ||
| ## LLM Compaction — Sonnet Summarization | ||
| #### Step 2: Zero-cost compression phases | ||
| ```typescript | ||
| compactConversation() (compact.ts:200-500): | ||
| def _strip_media(self, messages: list[Message]) -> list[Message]: """Replace images/documents with placeholder text.""" result = [] for msg in messages: content = msg.content if isinstance(content, list): new_blocks = [] for block in content: if block.get("type") == "image": new_blocks.append({"type": "text", "text": "[image]"}) elif block.get("type") == "document": new_blocks.append({"type": "text", "text": "[document]"}) else: new_blocks.append(block) result.append(msg.copy(content=new_blocks)) else: result.append(msg) return result def _micro_compact(self, messages: list[Message]) -> list[Message]: """Collapse low-value messages: progress, verbose tool_results.""" result = [] for msg in messages: if msg.role == "system" and msg.content.get("type") == "progress": continue # Drop progress messages entirely if msg.role == "tool" and len(str(msg.content)) > 2000: # Truncate verbose tool results result.append(msg.copy( content=truncate_to(str(msg.content), 500) + "\n[...truncated]" )) else: result.append(msg) return result | ||
| 1. groupMessagesByApiRound() → group by API call | ||
| 2. Fork call to Sonnet + getCompactPrompt() | ||
| 3. SystemCompactBoundaryMessage wraps output | ||
| 4. Post-cleanup: re-inject files + skills | ||
| #### Step 3: Token budget tracker | ||
| ═══ Prompt Structure (prompt.ts) ═══ | ||
| NO_TOOLS_PREAMBLE (L18): | ||
| "CRITICAL: Respond with TEXT ONLY. Do NOT call any tools." | ||
| // Sonnet 4.6+ sometimes ignores instruction to call tools — brute-force block | ||
| COMPLETION_THRESHOLD = 0.9 DIMINISHING_THRESHOLD = 500 # tokens @dataclass class BudgetTracker: budget: int | None continuation_count: int = 0 last_delta_tokens: int = 0 last_total_tokens: int = 0 def check(self, current_tokens: int) -> str: """Returns: 'continue' | 'stop' | 'stop_diminishing'""" if self.budget is None: return "continue" delta = current_tokens - self.last_total_tokens pct = current_tokens / self.budget # Diminishing returns: 3+ continuations with tiny deltas is_diminishing = ( self.continuation_count >= 3 and delta < DIMINISHING_THRESHOLD and self.last_delta_tokens < DIMINISHING_THRESHOLD ) if is_diminishing: return "stop_diminishing" if pct >= COMPLETION_THRESHOLD: return "stop" # Continue self.continuation_count += 1 self.last_delta_tokens = delta self.last_total_tokens = current_tokens return "continue" | ||
| BASE_COMPACT_PROMPT (L36) — 9 sections: | ||
| 1. Primary intent (user's final goal) | ||
| 2. Key concepts (core concepts and terminology) | ||
| 3. Files (files being edited) | ||
| 4. Errors (current error state) | ||
| 5. Problem solving (approaches already tried) | ||
| 6. User messages (key user instructions) | ||
| 7. Pending tasks (incomplete tasks) | ||
| 8. Current work (current work snapshot) | ||
| 9. Next steps (next planned steps) | ||
| → Requires <analysis> block (draft) + <summary> block | ||
| #### Step 4: Prompt-too-long recovery | ||
| PARTIAL_COMPACT_PROMPT: | ||
| Retain early context, only summarize recent portion | ||
| def recover_from_ptl( messages: list[Message], error_response: dict ) -> list[Message] | None: """Drop oldest API rounds to fit context window.""" # Parse token gap from error token_gap = parse_token_gap(error_response) # Group messages by API round (user + assistant + tool_results) rounds = group_by_api_round(messages) if len(rounds) < 2: return None # Can't recover if token_gap: # Drop rounds until we've freed enough tokens accumulated = 0 drop_count = 0 for r in rounds: accumulated += estimate_tokens(r) drop_count += 1 if accumulated >= token_gap: break else: # Fallback: drop 20% of rounds drop_count = max(1, len(rounds) // 5) # Keep at least 1 round drop_count = min(drop_count, len(rounds) - 1) remaining = rounds[drop_count:] return flatten(remaining) def group_by_api_round(messages): """Group: [user_msg, assistant_msg, tool_result_msgs...]""" rounds, current = [], [] for msg in messages: if msg.role == "user" and current: rounds.append(current) current = [] current.append(msg) if current: rounds.append(current) return rounds | ||
| ═══ Post-Compact Cleanup ═══ | ||
| POST_COMPACT_MAX_FILES_TO_RESTORE = 5 | ||
| POST_COMPACT_TOKEN_BUDGET = 50,000 tokens | ||
| POST_COMPACT_MAX_TOKENS_PER_FILE = 5,000 | ||
| POST_COMPACT_SKILLS_TOKEN_BUDGET = 25,000 | ||
| POST_COMPACT_MAX_TOKENS_PER_SKILL = 5,000 | ||
| ``` | ||
| #### Step 5: Wire into orchestrator-next run loop | ||
| --- | ||
| # orchestrator.py — main loop async def run(self, agent_config, thread_id, initial_message): budget = BudgetTracker(budget=agent_config.max_tokens) compactor = ContextCompactor(self.hook_engine) while True: messages = await self._load_messages(thread_id) # Check context window tokens = estimate_tokens(messages) if tokens > self.context_window * 0.85: messages = await compactor.compact(messages, self.context_window * 0.7) try: response = await self.llm.query(messages=messages) except PromptTooLongError as e: recovered = recover_from_ptl(messages, e.details) if recovered: response = await self.llm.query(messages=recovered) else: raise # Process tool calls... total_tokens += response.usage.total # Budget check decision = budget.check(total_tokens) if decision == "stop_diminishing": await self._send("达到 token 预算且输出趋于收敛,自动停止。") break elif decision == "stop": break | ||
| ## PTL Recovery — Prompt-Too-Long Handling | ||
| III | ||
| ```typescript | ||
| ═══ Error Detection ═══ | ||
| PROMPT_TOO_LONG_ERROR_MESSAGE pattern match | ||
| getPromptTooLongTokenGap() → extract exact token overflow amount | ||
| ## How to Verify | ||
| ═══ Recovery Loop ═══ | ||
| recompactIfNeeded() → retry with tighter boundaries | ||
| MAX_COMPACT_STREAMING_RETRIES = 2 | ||
| Still failing after retry → Throw (user must manually compact or cancel) | ||
| ### Unit Tests | ||
| ═══ Hook Integration ═══ | ||
| executePreCompactHooks() (L400) — model-driven, can block | ||
| executePostCompactHooks() (L700) — cleanup after | ||
| * **test_strip_media** — 含 3 张图片的 messages 压缩后只有 "[image]" placeholder | ||
| * **test_micro_compact** — 5000 字的 tool_result 被截断到 500 字 | ||
| * **test_phase_ordering** — tokens 只超 80% 时只做 Phase 1,不触发 LLM summarize | ||
| * **test_budget_continue** — 50% budget 时返回 "continue" | ||
| * **test_budget_stop_at_90pct** — 90% budget 时返回 "stop" | ||
| * **test_diminishing_returns** — 4 次 continuation 每次 <500 tokens → "stop_diminishing" | ||
| * **test_ptl_recovery** — 10 轮对话,token_gap=5000,drop 前 3 轮 → 剩余 fit | ||
| * **test_ptl_semantic_coherence** — 截断后第一条 message 仍是完整的 user message(不是 tool_result 碎片) | ||
| ### Integration Tests | ||
| * **test_companion_long_session** — 与 Companion 对话 50 轮,验证 context 没有爆掉 | ||
| * **test_compact_preserves_recent** — compact 后最近 5 轮完整保留 | ||
| * **test_workflow_budget_stops** — workflow 设置 50k token budget,验证自动停止 | ||
| IV | ||
| ## User Perception | ||
| Before | ||
| 用户 (第 30 轮对话): 帮我... | ||
| [ERROR: prompt too long, context exceeded] | ||
| Companion: 对不起,对话太长了,请新开一个会话。 | ||
| [之前所有 context 丢失] | ||
| 用户启动周报 Agent | ||
| [周报 Agent 陷入循环,不停调 calendar_read] | ||
| [token 消耗飙升,用户不知道该不该手动停] | ||
| After | ||
| 用户 (第 30 轮对话): 帮我... | ||
| [Phase 1: strip images → saved 30%] | ||
| [Phase 2: micro-compact → saved 15%] | ||
| Companion: [正常回复,无感知中断] | ||
| [Companion thread 永续运行 ✓] | ||
| 用户启动周报 Agent | ||
| [Budget 90% → auto-stop] | ||
| 周报 Agent: 信息收集完毕,已生成周报。 | ||
| **核心体感差异:**(1) Companion 永续对话,不再 "对话太长请新开";(2) Workflow 不会无限 spin 烧 token;(3) 压缩静默发生,用户完全无感。 | ||
| ContextEngine compact token budget PTL recovery Companion | ||
| ═══ Stripping Priority (what goes first) ═══ | ||
| 1. Images/documents → [image] / [document] marker | ||
| 2. Old tool results → [Old tool result content cleared] | ||
| 3. Entire early messages → summarize if still over limit | ||
| 4. Partial compaction → keep recent, truncate older | ||
| ``` |
+173
-116
@@ -1,162 +0,219 @@ | ||
| [← Back to Index](index.html) | ||
| # 06. Skill System | ||
| # 06\. Skill System High | ||
| > From markdown skill unit to bundled / project / user / managed / plugin / MCP multi-source loading. | ||
| 从 markdown skill unit 到 bundled / project / user / managed / plugin / MCP 多入口装载 | ||
| ## Key Files | ||
| R | ||
| ``` | ||
| src/skills/loadSkillsDir.ts — loading + conditional activation (240-400+) | ||
| src/skills/bundledSkills.ts — bundled skill registration (14-120) | ||
| src/skills/bundled/index.ts — all bundled skill imports | ||
| src/skills/mcpSkillBuilders.ts — MCP skill builder registry (1-50) | ||
| src/tools/SkillTool/SkillTool.ts — skill execution as tool | ||
| src/utils/frontmatterParser.ts — frontmatter field parsing | ||
| ``` | ||
| ## Feature Reference [→ Full Command Index](00-feature-discovery.html#commands) | ||
| --- | ||
| ### Frontmatter Fields (16) | ||
| ## Frontmatter Fields (16) | ||
| description — skill description type — skill type when_to_use — activation hint for model allowed-tools — comma-separated or array argument-hint — usage hint version — semver model — model override ('inherit' supported) user-invocable — show in /skills list context — 'inline' | 'fork' agent — agent type when context=fork effort — 'low'|'medium'|'high'|'max'|integer paths — glob patterns for conditional activation shell — 'bash'|'powershell' for embedded scripts hooks — HooksSettings (PreToolUse/PostToolUse) skills — nested skill references hide-from-slash-command-tool — hide from ToolSearch | ||
| ```yaml | ||
| description — skill description | ||
| type — skill type | ||
| when_to_use — activation hint for model | ||
| allowed-tools — comma-separated or array | ||
| argument-hint — usage hint | ||
| version — semver | ||
| model — model override ('inherit' supported) | ||
| user-invocable — show in /skills list | ||
| context — 'inline' | 'fork' | ||
| agent — agent type when context=fork | ||
| effort — 'low'|'medium'|'high'|'max'|integer | ||
| paths — glob patterns for conditional activation | ||
| shell — 'bash'|'powershell' for embedded scripts | ||
| hooks — HooksSettings (PreToolUse/PostToolUse) | ||
| skills — nested skill references | ||
| hide-from-slash-command-tool — hide from ToolSearch | ||
| ``` | ||
| ### Skill Surfaces (settings scopes + bundled + plugin + MCP) | ||
| --- | ||
| 1\. Project: .claude/skills/ (highest priority) 2\. User: ~/.claude/skills/ 3\. Managed: <managed-path>/.claude/skills/ 4\. Bundled: compiled into binary 5\. Plugin: plugin markdown / SKILL.md 6\. MCP: runtime-built skill commands Resolution: \- on-disk settings scopes: project > user > managed \- bundled acts as fallback \- plugin / MCP are additional producers, not simple filesystem scopes | ||
| ## Skill Surfaces | ||
| ### Bundled Skills (17) | ||
| ``` | ||
| 1. Project: .claude/skills/ (highest priority) | ||
| 2. User: ~/.claude/skills/ | ||
| 3. Managed: <managed-path>/.claude/skills/ | ||
| 4. Bundled: compiled into binary | ||
| 5. Plugin: plugin markdown / SKILL.md | ||
| 6. MCP: runtime-built skill commands | ||
| // Always available (11): updateConfig, keybindings, verify, debug, loremIpsum, skillify, remember, simplify, batch, stuck, claudeInChrome // Feature-gated (6): dream → KAIROS hunter → REVIEW_ARTIFACT loop → AGENT_TRIGGERS scheduleRemoteAgents → AGENT_TRIGGERS_REMOTE claudeApi → BUILDING_CLAUDE_APPS runSkillGenerator → RUN_SKILL_GENERATOR | ||
| Resolution: | ||
| - on-disk settings scopes: project > user > managed | ||
| - bundled acts as fallback | ||
| - plugin / MCP are additional producers, not simple filesystem scopes | ||
| ``` | ||
| ### Feature Flags & GrowthBook | ||
| --- | ||
| // Build-time flags BUILDING_CLAUDE_APPS — Claude API builder skill REVIEW_ARTIFACT — review artifact skill RUN_SKILL_GENERATOR — skill generator execution SKILL_IMPROVEMENT — skill improvement tracking EXPERIMENTAL_SKILL_SEARCH — skill search indexing MCP_SKILLS — MCP-based skill loading // GrowthBook tengu_copper_panda (false) — skill improvement tracking tengu_lapis_finch (false) — plugin hint recommendations | ||
| ## Bundled Skills (17) | ||
| ### Key Files | ||
| ```typescript | ||
| // Always available (11): | ||
| updateConfig, keybindings, verify, debug, loremIpsum, | ||
| skillify, remember, simplify, batch, stuck, claudeInChrome | ||
| src/skills/loadSkillsDir.ts — loading + conditional activation (240-400+) src/skills/bundledSkills.ts — bundled skill registration (14-120) src/skills/bundled/index.ts — all bundled skill imports src/skills/mcpSkillBuilders.ts — MCP skill builder registry (1-50) src/tools/SkillTool/SkillTool.ts — skill execution as tool src/utils/frontmatterParser.ts — frontmatter field parsing | ||
| // Feature-gated (6): | ||
| dream → KAIROS | ||
| hunter → REVIEW_ARTIFACT | ||
| loop → AGENT_TRIGGERS | ||
| scheduleRemoteAgents → AGENT_TRIGGERS_REMOTE | ||
| claudeApi → BUILDING_CLAUDE_APPS | ||
| runSkillGenerator → RUN_SKILL_GENERATOR | ||
| ``` | ||
| ### Evidence Boundary | ||
| --- | ||
| src/skills/loadSkillsDir.ts:78-92 只直接证明了 project/user/managed 的 on-disk path 解析;plugin 与 MCP skills 不是这个 `getSkillsPath()` 返回的普通目录,而是由 src/utils/plugins/loadPluginCommands.ts 和 src/skills/mcpSkillBuilders.ts 额外注入。 | ||
| ## `BundledSkillDefinition` Type (`bundledSkills.ts:14-41`) | ||
| ### BundledSkillDefinition 完整类型 (bundledSkills.ts:14-41) | ||
| ```typescript | ||
| type BundledSkillDefinition = { | ||
| name: string | ||
| description: string | ||
| aliases?: string[] | ||
| whenToUse?: string // model-facing activation hint | ||
| argumentHint?: string // help text for arguments | ||
| allowedTools?: string[] // constrain which tools skill can invoke | ||
| model?: string // model override per-skill | ||
| disableModelInvocation?: boolean // pure prompt mode (no LLM call) | ||
| userInvocable?: boolean // gated availability | ||
| isEnabled?(): boolean // conditional activation callback | ||
| hooks?: HooksSettings // trigger hooks on invocation | ||
| context?: 'inline' | 'fork' // inline inject context vs run independent agent | ||
| agent?: string // delegate to agent | ||
| files?: Record<string, string> // bundled files (extracted at runtime) | ||
| } | ||
| ``` | ||
| type BundledSkillDefinition = { name: string description: string aliases?: string[] whenToUse?: string // model-facing activation hint argumentHint?: string // help text for arguments allowedTools?: string[] // constrain which tools skill can invoke model?: string // model override per-skill disableModelInvocation?: boolean // pure prompt mode (no LLM call) userInvocable?: boolean // gated availability isEnabled?(): boolean // conditional activation callback hooks?: HooksSettings // trigger hooks on invocation context?: 'inline' | 'fork' // inline注入context vs 跑独立agent agent?: string // delegate to agent files?: Record<string, string> // bundled files (extracted at runtime) } | ||
| --- | ||
| ### Frontmatter 所有支持字段 (loadSkillsDir.ts:162-213) | ||
| ## Frontmatter All Fields (`loadSkillsDir.ts:162-213`) | ||
| parseSkillFrontmatterFields(): name — skill identifier description — recall selector & model hint user-invocable — boolean: 是否可被用户直接调用 model — model override (e.g. "opus", "sonnet") effort — token budget (low/medium/high/max) allowed-tools — comma/space separated or YAML list argument-hint — help for arguments arguments — string or array when_to_use — activation description for model version — skill version disable-model-invocation — pure prompt mode context — 'fork' → runs as sub-agent agent — agent name override hooks — HooksSchema() (PreToolUse/PostToolUse) paths — glob patterns for conditional activation shell — bash/powershell config | ||
| ```typescript | ||
| parseSkillFrontmatterFields(): | ||
| name — skill identifier | ||
| description — recall selector & model hint | ||
| user-invocable — boolean: directly user-callable | ||
| model — model override (e.g. "opus", "sonnet") | ||
| effort — token budget (low/medium/high/max) | ||
| allowed-tools — comma/space separated or YAML list | ||
| argument-hint — help for arguments | ||
| arguments — string or array | ||
| when_to_use — activation description for model | ||
| version — skill version | ||
| disable-model-invocation — pure prompt mode | ||
| context — 'fork' → runs as sub-agent | ||
| agent — agent name override | ||
| hooks — HooksSchema() (PreToolUse/PostToolUse) | ||
| paths — glob patterns for conditional activation | ||
| shell — bash/powershell config | ||
| ``` | ||
| ### Conditional Activation by Path (parseSkillPaths L117-140) | ||
| --- | ||
| paths: /src/**, /tests/** → skill 只在匹配目录中工作时激活 实现: \- 使用 'ignore' library (same as .gitignore) \- 模式标准化: /src/** → /src (trailing /** stripped) \- 全 ** pattern → 视为无限制 (undefined) \- Path matching is project-scoped at query time // 例: python-pptx skill 只在 *.pptx 文件附近激活 // 例: react skill 只在 src/components/ 下激活 | ||
| ## Skill Scope Priority (`constants.ts getSkillsPath`) | ||
| ### Skill Scope 优先级 (constants.ts getSkillsPath) | ||
| ```typescript | ||
| SettingSource → Directory mapping: | ||
| policySettings → ${MANAGED_PATH}/.claude/skills (enterprise managed) | ||
| userSettings → ${CLAUDE_CONFIG_HOME}/skills (user global) | ||
| projectSettings → .claude/skills (project level) | ||
| plugin → 'plugin' (special case) | ||
| SettingSource → Directory 映射: policySettings → ${MANAGED_PATH}/.claude/skills (企业管理) userSettings → ${CLAUDE_CONFIG_HOME}/skills (用户全局) projectSettings → .claude/skills (项目级) plugin → 'plugin' (special case) // 注意: // - 当前 getSkillsPath() 不提供单独 .claude-local/skills 路径 // - plugin 不是普通目录,而是特殊 source tag // 优先级: project > user > managed > bundled // 同名 skill: deeper on-disk scope wins | ||
| // Notes: | ||
| // - getSkillsPath() does not provide a separate .claude-local/skills path | ||
| // - plugin is not a plain directory, it's a special source tag | ||
| // Priority: project > user > managed > bundled | ||
| // Same name: deeper on-disk scope wins | ||
| ``` | ||
| ### Skill Loading Pipeline (loadSkillsDir.ts:240-400+) | ||
| --- | ||
| 1\. Deduplication — getFileIdentity() uses realpath → 检测 symlink 重复 (同一文件不同路径) 2\. Frontmatter Parsing — parseSkillFrontmatterFields() → Zod-like schema validation 3\. Skill Creation — createSkillCommand() → 组装 Command object 4\. Argument Substitution — substituteArguments() → 展开 ${ARG} in prompt body 5\. Base Directory — getBundledSkillExtractDir(name) → bundled files extracted to runtime dir 6\. Shell Injection Prevention → executeShellCommandsInPrompt() only for non-MCP 7\. Token Estimation — estimateSkillFrontmatterTokens() → 只计 name, description, whenToUse (for context budget) | ||
| ## Conditional Activation by Path (`parseSkillPaths L117-140`) | ||
| ### MCP Skills — 动态加载 | ||
| ```yaml | ||
| paths: /src/**, /tests/** | ||
| → skill only activates when working in matching directories | ||
| ``` | ||
| // mcpSkillBuilders.ts // MCP server 连接后才注册 skill builders // 通过 getMCPSkillBuilders() 获取: // createSkillCommand, parseSkillFrontmatterFields // 避免循环依赖 (module init 时 delegate builders) // MCP skill 与普通 skill 区别: // - Schema 来自 MCP server (不是本地 frontmatter) // - executeShellCommandsInPrompt() 不执行 (安全) // - allowedTools 可能包含 MCP-specific tools | ||
| ```typescript | ||
| // Implementation: | ||
| // - Uses 'ignore' library (same as .gitignore) | ||
| // - Pattern normalization: /src/** → /src (trailing /** stripped) | ||
| // - Full ** pattern → treated as unrestricted (undefined) | ||
| // - Path matching is project-scoped at query time | ||
| I | ||
| // e.g. python-pptx skill only activates near *.pptx files | ||
| // e.g. react skill only activates under src/components/ | ||
| ``` | ||
| ## Comparison | ||
| --- | ||
| Claude Code | ||
| ## Skill Loading Pipeline (`loadSkillsDir.ts:240-400+`) | ||
| * **Skill = Markdown + YAML frontmatter** :description, when-to-use, allowed-tools, model, effort, hooks, paths | ||
| * **Conditional activation** :paths 字段匹配文件 pattern 时自动激活 | ||
| * **Priority override** :project > user > bundled,deeper path wins | ||
| * **Inline vs Fork** :inline 注入 context,fork 跑独立 sub-agent | ||
| * **Skills 可带 hooks** :frontmatter 里定义 PreToolUse/PostToolUse hooks | ||
| * **Model override** :per-skill 指定用哪个 model | ||
| * **Effort levels** :控制 token budget(low/medium/high/max) | ||
| * **Bundled + User + Project** :三层来源 | ||
| ```typescript | ||
| 1. Deduplication — getFileIdentity() uses realpath | ||
| → Detects symlink duplicates (same file, different paths) | ||
| Mitsein | ||
| 2. Frontmatter Parsing — parseSkillFrontmatterFields() | ||
| → Zod-like schema validation | ||
| * `skills: [xhs_copywriting, ppt_layout]` — config 字符串列表 | ||
| * Skill = prompt snippet,静态绑定到 agent config | ||
| * 无 conditional activation | ||
| * 无 inline/fork 区分 | ||
| * 无 per-skill hooks | ||
| * 无 model override per skill | ||
| * Skill 不可独立发布到 Store | ||
| 3. Skill Creation — createSkillCommand() | ||
| → Assemble Command object | ||
| II | ||
| 4. Argument Substitution — substituteArguments() | ||
| → Expand ${ARG} in prompt body | ||
| ## How to Implement | ||
| 5. Base Directory — getBundledSkillExtractDir(name) | ||
| → Bundled files extracted to runtime dir | ||
| #### Step 1: Skill file format | ||
| 6. Shell Injection Prevention | ||
| → executeShellCommandsInPrompt() only for non-MCP | ||
| 每个 skill 是独立 .md 文件,YAML frontmatter + body。 | ||
| 7. Token Estimation — estimateSkillFrontmatterTokens() | ||
| → Only counts name, description, whenToUse (for context budget) | ||
| ``` | ||
| skills/ ├── xhs_copywriting.md ├── xhs_layout.md ├── ppt_design.md ├── ppt_layout.md └── weekly_report_template.md # Example: skills/xhs_copywriting.md \--- name: xhs_copywriting description: 小红书文案撰写规则 when_to_use: 撰写小红书文案时 allowed_tools: [web_search, translate] model: null # inherit from agent context: inline # inject into agent context effort: medium hooks: PreToolUse: \- matcher: xhs_publish hooks: \- type: callback callback: content_policy_check \--- # 小红书文案规则 ## 标题 \- 不超过 20 字 \- 必须包含数字或疑问句 \- 禁止全大写 ## 正文 \- 800 字以内 \- 口语化,像跟朋友聊天 \- 分段,每段 2-3 句 ## 配图 \- 至少 3 张 \- 竖版优先 (3:4) \- 第一张必须抓眼球 | ||
| --- | ||
| #### Step 2: Skill loader with frontmatter parser | ||
| ## MCP Skills — Dynamic Loading (`mcpSkillBuilders.ts`) | ||
| import yaml from pathlib import Path @dataclass class SkillDefinition: name: str description: str body: str # markdown content when_to_use: str | None = None allowed_tools: list[str] | None = None model: str | None = None context: str = "inline" # "inline" | "fork" effort: str = "medium" paths: list[str] | None = None # conditional activation hooks: dict | None = None source: str = "platform" # "platform" | "user" | "store" def load_skill(filepath: Path) -> SkillDefinition: text = filepath.read_text() # Parse YAML frontmatter if text.startswith("---"): _, fm_raw, body = text.split("---", 2) frontmatter = yaml.safe_load(fm_raw) else: frontmatter, body = {}, text return SkillDefinition( name=frontmatter.get("name", filepath.stem), description=frontmatter.get("description", ""), body=body.strip(), **{k: frontmatter[k] for k in frontmatter if k in SkillDefinition.__dataclass_fields__ and k not in ("name", "description", "body")} ) def load_skills_dir(dirpath: Path) -> dict[str, SkillDefinition]: skills = {} for f in sorted(dirpath.glob("**/*.md")): skill = load_skill(f) skills[skill.name] = skill # later load overwrites earlier (deeper wins) return skills | ||
| ```typescript | ||
| // MCP server connects → registers skill builders | ||
| // Via getMCPSkillBuilders(): | ||
| // createSkillCommand, parseSkillFrontmatterFields | ||
| // Avoids circular dependency (delegates builders at module init) | ||
| #### Step 3: Conditional activation by path | ||
| // MCP skill vs regular skill differences: | ||
| // - Schema comes from MCP server (not local frontmatter) | ||
| // - executeShellCommandsInPrompt() not executed (security) | ||
| // - allowedTools may include MCP-specific tools | ||
| ``` | ||
| class SkillRegistry: _active: dict[str, SkillDefinition] = {} _conditional: dict[str, SkillDefinition] = {} def load(self, skills: dict[str, SkillDefinition]): for name, skill in skills.items(): if skill.paths: self._conditional[name] = skill else: self._active[name] = skill def activate_for_paths(self, file_paths: list[str]) -> list[str]: """Check conditional skills against file paths, return newly activated.""" newly_activated = [] for name, skill in list(self._conditional.items()): for fp in file_paths: if any(Path(fp).match(p) for p in skill.paths): self._active[name] = skill del self._conditional[name] newly_activated.append(name) break return newly_activated # Example: skill with paths: ["*.pptx", "presentations/**"] # → auto-activated when user opens/mentions a .pptx file | ||
| --- | ||
| #### Step 4: Inline vs Fork execution | ||
| ## Feature Flags & GrowthBook | ||
| class SkillExecutor: async def execute(self, skill: SkillDefinition, agent_context: AgentContext): if skill.context == "inline": # Inject skill body directly into agent's system prompt return self._inject_inline(skill, agent_context) elif skill.context == "fork": # Run as sub-agent with own context + token budget return await self._run_forked(skill, agent_context) def _inject_inline(self, skill, ctx): """Append skill content to system prompt.""" ctx.system_prompt += f"\n\n# Skill: {skill.name}\n{skill.body}" if skill.allowed_tools: # Restrict tool pool for this skill's scope ctx.tool_hints[skill.name] = skill.allowed_tools async def _run_forked(self, skill, ctx): """Run skill as independent sub-agent.""" child_context = fork_context(ctx, skill.name) child_context.system_prompt = skill.body child_context.max_tokens = EFFORT_BUDGET[skill.effort] if skill.model: child_context.model = skill.model return await orchestrator.run_single_turn(child_context) EFFORT_BUDGET = { "low": 2_000, "medium": 8_000, "high": 30_000, "max": 100_000, } | ||
| ``` | ||
| // Build-time flags | ||
| BUILDING_CLAUDE_APPS — Claude API builder skill | ||
| REVIEW_ARTIFACT — review artifact skill | ||
| RUN_SKILL_GENERATOR — skill generator execution | ||
| SKILL_IMPROVEMENT — skill improvement tracking | ||
| EXPERIMENTAL_SKILL_SEARCH — skill search indexing | ||
| MCP_SKILLS — MCP-based skill loading | ||
| #### Step 5: Agent config 从字符串到引用 | ||
| # Before: static string list # skills: [xhs_copywriting, ppt_layout] # After: SkillRegistry resolves at runtime class AgentConfigResolver: def resolve_skills(self, config: AgentConfig) -> list[SkillDefinition]: skills = [] for name in config.skills: skill = self.skill_registry.get(name) if skill: skills.append(skill) else: logger.warning(f"Skill '{name}' not found") # Also include auto-activated conditional skills skills.extend(self.skill_registry.get_active_for_agent(config.id)) return skills | ||
| III | ||
| ## How to Verify | ||
| ### Unit Tests | ||
| * **test_load_skill_frontmatter** — 解析 .md 文件,验证 name/description/body/hooks 正确提取 | ||
| * **test_conditional_activation** — skill with paths=["*.pptx"],传 "report.pptx" → activated | ||
| * **test_conditional_no_match** — 传 "report.docx" → not activated | ||
| * **test_deeper_path_wins** — user skill 和 project skill 同名,project skill 覆盖 user | ||
| * **test_inline_injection** — inline skill body 出现在 agent system prompt 中 | ||
| * **test_fork_isolation** — fork skill 有独立 token budget,不消耗主 agent 额度 | ||
| * **test_effort_budget_mapping** — effort=low → 2000 tokens max | ||
| * **test_hook_registration_from_skill** — skill frontmatter 带 hooks,verify hook_engine 注册了对应 hook | ||
| ### Integration Tests | ||
| * **test_xhs_agent_with_skill** — 小红书 Agent 加载 xhs_copywriting skill,输出符合规则(800字内、有数字标题) | ||
| * **test_skill_publish_to_store** — 用户创建 skill .md → 上传到 Store → 另一用户安装 → 功能正常 | ||
| IV | ||
| ## User Perception | ||
| Before | ||
| 用户: 我想给小红书 Agent 加一个"选题研究"能力 | ||
| [需要修改 agent config YAML,手动加 skill 名称] | ||
| [skill 内容硬编码在某处,无法独立编辑] | ||
| [无法分享给其他用户] | ||
| After | ||
| 用户: 我想给小红书 Agent 加一个"选题研究"能力 | ||
| Companion: 创建一个 skill 文件: | ||
| [生成 skills/xhs_topic_research.md] | ||
| [frontmatter: when_to_use, allowed_tools, effort] | ||
| Companion: 已创建,下次小红书 Agent 自动可用 | ||
| 用户: 分享到 Store | ||
| Companion: 已发布 ✓ 其他用户可搜索安装 | ||
| **核心体感差异:** Skills 从"开发者配置项"变成"用户可创建、可安装、可分享的能力包"。就像 App 可以安装插件一样,Agent 可以安装 skills。conditional activation 让 skill 在需要时自动出现,不需要手动绑定。 | ||
| skills frontmatter conditional activation Agent Store inline/fork | ||
| // GrowthBook | ||
| tengu_copper_panda (false) — skill improvement tracking | ||
| tengu_lapis_finch (false) — plugin hint recommendations | ||
| ``` |
+165
-105
@@ -1,148 +0,208 @@ | ||
| [← Back to Index](index.html) | ||
| # 07. Permission & Security Model | ||
| # 07\. Permission & Security Model Critical | ||
| > From binary allow/deny to 5-layer permission pipeline + agent-type tiering. | ||
| 从二元 allow/deny 到 5 层 permission pipeline + agent-type 分级 | ||
| ## Key Files | ||
| R | ||
| ``` | ||
| src/hooks/toolPermission/PermissionContext.ts — context creation (96-147) | ||
| src/hooks/useCanUseTool.tsx — main checker (3 handlers) | ||
| src/utils/permissions/PermissionMode.ts — mode enum + cycle | ||
| src/utils/permissions/PermissionRule.ts — rule definitions + sources | ||
| src/utils/permissions/permissions.ts — hasPermissionsToUseTool (L100-550) | ||
| src/utils/permissions/permissionsLoader.ts — loadAllPermissionRulesFromDisk | ||
| src/utils/permissions/permissionSetup.ts — auto mode gate | ||
| src/utils/permissions/getNextPermissionMode.ts — Shift+Tab cycle | ||
| src/utils/permissions/denialTracking.ts — denial count per session | ||
| src/services/tools/toolExecution.ts — gating pipeline (599-1128) | ||
| ``` | ||
| ## Feature Reference [→ Full Flag Index](00-feature-discovery.html#flags-build) | ||
| --- | ||
| ### Permission Modes (6) | ||
| ## Permission Modes (6) | ||
| // src/utils/permissions/PermissionMode.ts acceptEdits — trust read/write, ask for destructive bypassPermissions — auto-approve everything (dangerous) default — prompt for every tool dontAsk — alias for bypassPermissions plan — require explicit plan approval auto — ML classifier decides (TRANSCRIPT_CLASSIFIER only) | ||
| ```typescript | ||
| // src/utils/permissions/PermissionMode.ts | ||
| acceptEdits — trust read/write, ask for destructive | ||
| bypassPermissions — auto-approve everything (dangerous) | ||
| default — prompt for every tool | ||
| dontAsk — alias for bypassPermissions | ||
| plan — require explicit plan approval | ||
| auto — ML classifier decides (TRANSCRIPT_CLASSIFIER only) | ||
| ``` | ||
| ### Permission Decision Sources | ||
| ## All Permission Modes | ||
| 'classifier' — auto-mode ML decision 'hook' — permission hook rule 'user' — interactive prompt (permanent or temporary) 'user_abort' — user interrupted 'user_reject' — user explicitly denied with feedback 'config' — settings allowlist/denylist | ||
| ```typescript | ||
| PERMISSION_MODES = [ | ||
| 'default', // ask prompt on every action | ||
| 'plan', // pause execution, review before running tools (ant-only) | ||
| 'acceptEdits', // allow file edits in working dir, ask for others | ||
| 'bypassPermissions', // manual trust, still obeys deny rules | ||
| 'dontAsk', // all ask → deny (auto-deny) | ||
| 'auto', // classifier evaluates safety (ant only) | ||
| 'bubble', // fork subagent: bubble permission up to parent | ||
| ] | ||
| ### Feature Flags & GrowthBook | ||
| // Externally visible: | ||
| EXTERNAL_PERMISSION_MODES = [default, plan, acceptEdits, bypassPermissions, dontAsk] | ||
| // Build-time BASH_CLASSIFIER — Bash command classification for auto-mode TRANSCRIPT_CLASSIFIER — enables 'auto' permission mode // GrowthBook tengu_birch_trellis (true) — Bash permission defaults tengu_destructive_command_warning (false) — destructive command warnings // Env vars CLAUDE_CODE_TWO_STAGE_CLASSIFIER — two-stage bash classification CLAUDE_CODE_DUMP_AUTO_MODE — dump classifier decisions (debug) | ||
| // Mode cycle (Shift+Tab): | ||
| default → acceptEdits → plan → bypassPermissions → auto → default | ||
| // Ants skip acceptEdits, plan (auto mode replaces) | ||
| // Non-ants cannot reach auto | ||
| ``` | ||
| ### Key Files | ||
| --- | ||
| src/hooks/toolPermission/PermissionContext.ts — context creation (96-147) src/hooks/useCanUseTool.tsx — main checker (3 handlers) src/utils/permissions/PermissionMode.ts — mode enum + cycle src/utils/permissions/PermissionRule.ts — rule definitions + sources src/utils/permissions/permissions.ts — hasPermissionsToUseTool (L100-550) src/utils/permissions/permissionsLoader.ts — loadAllPermissionRulesFromDisk src/utils/permissions/permissionSetup.ts — auto mode gate src/utils/permissions/getNextPermissionMode.ts — Shift+Tab cycle src/utils/permissions/denialTracking.ts — denial count per session src/services/tools/toolExecution.ts — gating pipeline (599-1128) | ||
| ## Permission Decision Sources | ||
| ### ALL Permission Modes (PermissionMode.ts) | ||
| ```typescript | ||
| 'classifier' — auto-mode ML decision | ||
| 'hook' — permission hook rule | ||
| 'user' — interactive prompt (permanent or temporary) | ||
| 'user_abort' — user interrupted | ||
| 'user_reject' — user explicitly denied with feedback | ||
| 'config' — settings allowlist/denylist | ||
| ``` | ||
| PERMISSION_MODES = [ 'default', // 每次 ask 行为都弹 prompt 'plan', // 暂停执行,review 后才 run tools (ant-only) 'acceptEdits', // 允许工作目录的文件编辑,其他 ask 'bypassPermissions', // 手动信任,仍遵守 deny rules 'dontAsk', // 所有 ask → deny (自动拒绝) 'auto', // classifier 评估安全性 (ant only) 'bubble', // fork subagent: 弹 permission 到 parent ] // 外部可见: EXTERNAL_PERMISSION_MODES = [default, plan, acceptEdits, bypassPermissions, dontAsk] // Mode 切换循环 (Shift+Tab): default → acceptEdits → plan → bypassPermissions → auto → default // Ants skip acceptEdits, plan (auto mode 取代) // Non-ants can't reach auto | ||
| --- | ||
| ### Permission Rule 完整结构 | ||
| ## Permission Rule Structure | ||
| PermissionRule = { source: PermissionRuleSource ruleBehavior: 'allow' | 'deny' | 'ask' ruleValue: { toolName: string // "Bash", "Agent(Explore)", "mcp__server__tool" ruleContent?: string // "prefix:git", "python:*" } } // 8 种 Source: 'session' — 临时, 内存 (用户说 "once") 'localSettings' — ~/.claude-code/settings.json 'userSettings' — .claude-code/settings.json 'projectSettings' — project-level rules 'cliArg' — --allow-bash CLI flag 'policySettings' — Admin/MDM policy 'flagSettings' — GrowthBook feature flags 'hook' — Pre/PostToolUse hook decision 'command' — CLI command-level // Rule Matching for Bash: "git *" → command starts with "git" "*import*" → command contains substring → preparePermissionMatcher() 预编译 → hook if 条件复用同样的 pattern 语法 | ||
| ```typescript | ||
| PermissionRule = { | ||
| source: PermissionRuleSource | ||
| ruleBehavior: 'allow' | 'deny' | 'ask' | ||
| ruleValue: { | ||
| toolName: string // "Bash", "Agent(Explore)", "mcp__server__tool" | ||
| ruleContent?: string // "prefix:git", "python:*" | ||
| } | ||
| } | ||
| ### Complete Permission Flow (permissions.ts:100-550) | ||
| // 8 Source types: | ||
| 'session' — temporary, in-memory (user said "once") | ||
| 'localSettings' — ~/.claude-code/settings.json | ||
| 'userSettings' — .claude-code/settings.json | ||
| 'projectSettings'— project-level rules | ||
| 'cliArg' — --allow-bash CLI flag | ||
| 'policySettings' — Admin/MDM policy | ||
| 'flagSettings' — GrowthBook feature flags | ||
| 'hook' — Pre/PostToolUse hook decision | ||
| 'command' — CLI command-level | ||
| hasPermissionsToUseTool(): Step 1: Check Allow Rules (L234-250) toolAlwaysAllowedRule(tool) match? → YES: { behavior: 'allow' } (fast path) Step 2: Check Deny Rules (L251-266) getDenyRuleForTool(tool) match? → YES: { behavior: 'deny' } Step 3: Tool-Specific checkPermissions() (L267-320) 每个 tool 自带 permission logic e.g. Bash prefix rules, file path patterns → Returns allow/deny/ask Step 4: Auto Mode Classifier (L320-490, ant-only) if mode==='auto' && result==='ask': 1\. Denial tracking check (fallback if limits exceeded) 2\. acceptEdits mode fast-path 3\. Safe allowlist check 4\. classifyYoloAction() with full transcript 5\. Log: tengu_auto_mode_decision Step 5: Permission Prompt (L450-550) if mode==='default' && result==='ask': Present UI: ask / allow-always / deny options Optional: executePermissionRequestHooks() → Returns user decision + classification // PermissionResult: { behavior: 'allow'|'deny'|'ask'|'passthrough' updatedInput?: Record<string, unknown> // hooks 可以改 input! message?: string decisionReason?: PermissionDecisionReason decisionClassification?: 'once'|'always'|'once_then_ask' metadata?: { source, ruleName, ruleValue } } | ||
| // Rule Matching for Bash: | ||
| "git *" → command starts with "git" | ||
| "*import*" → command contains substring | ||
| → preparePermissionMatcher() precompiles | ||
| → hook if conditions reuse same pattern syntax | ||
| ``` | ||
| ### Denial Tracking — Evolution Signal | ||
| --- | ||
| DenialTrackingState = { consecutiveDenials: number // reset on any success totalDenials: number // never decrements } DENIAL_LIMITS = { maxConsecutive: 3, // 连续 3 次 deny → fallback maxTotal: 20 // 总共 20 次 → fallback } shouldFallbackToPrompting(state): consecutiveDenials >= 3 || totalDenials >= 20 → 自动转为 prompting (防止无限 deny 循环) // 记录: per-tool, per-session (bootstrap/state.ts) // Background agents: localDenialTracking (isolated) | ||
| ## Complete Permission Flow (`permissions.ts:100-550`) | ||
| ### Rules 加载优先级 (permissionsLoader.ts:59-73) | ||
| ```typescript | ||
| hasPermissionsToUseTool(): | ||
| loadAllPermissionRulesFromDisk(): if allowManagedPermissionRulesOnly = true: → ONLY load from policySettings (企业锁定) else: → Load from ALL enabled sources: [userSettings, projectSettings, localSettings, flagSettings, policySettings] (last source wins on conflict) // Dangerous Permission 过滤: isDangerousBashPermission(): python:*, node:*, ruby:*, ... isDangerousPowerShellPermission(): iex, Invoke-Expression, Start-Process, ... // Auto mode 入口时自动 strip 这些 (防 code-exec bypass) | ||
| Step 1: Check Allow Rules (L234-250) | ||
| toolAlwaysAllowedRule(tool) match? | ||
| → YES: { behavior: 'allow' } (fast path) | ||
| I | ||
| Step 2: Check Deny Rules (L251-266) | ||
| getDenyRuleForTool(tool) match? | ||
| → YES: { behavior: 'deny' } | ||
| ## Comparison | ||
| Step 3: Tool-Specific checkPermissions() (L267-320) | ||
| Each tool has its own permission logic | ||
| e.g. Bash prefix rules, file path patterns | ||
| → Returns allow/deny/ask | ||
| Claude Code | ||
| Step 4: Auto Mode Classifier (L320-490, ant-only) | ||
| if mode==='auto' && result==='ask': | ||
| 1. Denial tracking check (fallback if limits exceeded) | ||
| 2. acceptEdits mode fast-path | ||
| 3. Safe allowlist check | ||
| 4. classifyYoloAction() with full transcript | ||
| 5. Log: tengu_auto_mode_decision | ||
| * **5-layer pipeline** : Config rules → Validation → Pre-hooks → Permission handler → Execution | ||
| * **Agent-type modes** : interactive / acceptEdits / auto / restricted | ||
| * **Spawned agents** : shouldAvoidPermissionPrompts=true(不弹 UI) | ||
| * **Auto-mode classifier** : ML model 自动 allow/deny,human fallback | ||
| * **Permission persistence** : temporary (session) vs permanent (settings.json) | ||
| * **Denial tracking** : 统计 deny 次数 → 建议 "always deny" | ||
| * **Deny rules filter tools** : model 都不会看到被 deny 的 tool | ||
| * **Tool-specific checkPermissions()** : 每个 tool 自带权限逻辑 | ||
| Step 5: Permission Prompt (L450-550) | ||
| if mode==='default' && result==='ask': | ||
| Present UI: ask / allow-always / deny options | ||
| Optional: executePermissionRequestHooks() | ||
| → Returns user decision + classification | ||
| Mitsein | ||
| // PermissionResult: | ||
| { | ||
| behavior: 'allow'|'deny'|'ask'|'passthrough' | ||
| updatedInput?: Record<string, unknown> // hooks can modify input! | ||
| message?: string | ||
| decisionReason?: PermissionDecisionReason | ||
| decisionClassification?: 'once'|'always'|'once_then_ask' | ||
| metadata?: { source, ruleName, ruleValue } | ||
| } | ||
| ``` | ||
| * **ToolPolicy** : allow / deny 二元判断 | ||
| * 所有 agent 类型用同一套 policy | ||
| * spawned agent 和 main agent 权限相同 | ||
| * 无 auto-mode / classifier | ||
| * 无 permission persistence(每次重新判断) | ||
| * 无 denial tracking | ||
| * 进化机制需要 permission data 作为信号 | ||
| --- | ||
| II | ||
| ## Denial Tracking — Evolution Signal | ||
| ## How to Implement | ||
| ```typescript | ||
| DenialTrackingState = { | ||
| consecutiveDenials: number // reset on any success | ||
| totalDenials: number // never decrements | ||
| } | ||
| #### Step 1: Permission mode per agent type | ||
| DENIAL_LIMITS = { | ||
| maxConsecutive: 3, // 3 consecutive denials → fallback | ||
| maxTotal: 20 // total 20 denials → fallback | ||
| } | ||
| class PermissionMode(str, Enum): INTERACTIVE = "interactive" # user confirms every action ACCEPT_EDITS = "accept_edits" # trust read/write, ask for destructive AUTO = "auto" # classifier decides RESTRICTED = "restricted" # deny all write, allow reads only AGENT_TYPE_DEFAULTS = { "companion": PermissionMode.INTERACTIVE, "capability": PermissionMode.ACCEPT_EDITS, "workflow": PermissionMode.AUTO, "daemon": PermissionMode.RESTRICTED, } # Spawned agents inherit parent permission but skip UI prompts @dataclass class PermissionContext: mode: PermissionMode avoid_prompts: bool = False # True for spawned agents deny_rules: list[str] = field(default_factory=list) allow_rules: list[str] = field(default_factory=list) | ||
| shouldFallbackToPrompting(state): | ||
| consecutiveDenials >= 3 || totalDenials >= 20 | ||
| → Automatically switch to prompting (prevent infinite deny loop) | ||
| #### Step 2: Multi-layer permission pipeline | ||
| // Recorded: per-tool, per-session (bootstrap/state.ts) | ||
| // Background agents: localDenialTracking (isolated) | ||
| ``` | ||
| class PermissionPipeline: async def check( self, tool: BaseTool, input: BaseModel, context: PermissionContext ) -> PermissionDecision: # Layer 1: Static deny rules (before model sees tool) if self._matches_deny_rule(tool.name, context.deny_rules): return PermissionDecision(behavior="deny", source="config") # Layer 2: Validation (Pydantic + semantic) error = await tool.validate_input(input) if error: return PermissionDecision(behavior="deny", source="validation", reason=error) # Layer 3: Pre-tool hooks hook_result = await self.hook_engine.fire( HookEvent.PRE_TOOL_USE, {"tool_name": tool.name, "input": input} ) if hook_result.permission_override: return PermissionDecision( behavior=hook_result.permission_override, source="hook" ) # Layer 4: Mode-based decision if context.mode == PermissionMode.RESTRICTED: if not tool.is_read_only(input): return PermissionDecision(behavior="deny", source="mode") if context.mode == PermissionMode.ACCEPT_EDITS: if tool.is_destructive(input): if context.avoid_prompts: return PermissionDecision(behavior="deny", source="mode") return PermissionDecision(behavior="ask", source="mode") if context.mode == PermissionMode.INTERACTIVE: if context.avoid_prompts: return PermissionDecision(behavior="allow", source="inherited") return PermissionDecision(behavior="ask", source="mode") # Layer 5: Allow return PermissionDecision(behavior="allow", source="default") | ||
| --- | ||
| #### Step 3: Denial tracking for evolution signals | ||
| ## Rules Loading Priority (`permissionsLoader.ts:59-73`) | ||
| class DenialTracker: """Track tool denials per agent per session for evolution.""" _denials: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int)) def record_denial(self, agent_config_id: str, tool_name: str): self._denials[agent_config_id][tool_name] += 1 def get_patterns(self, agent_config_id: str) -> list[dict]: """Return tools with repeated denials → evolution signals.""" patterns = [] for tool, count in self._denials[agent_config_id].items(): if count >= 3: # threshold patterns.append({ "tool": tool, "denial_count": count, "suggestion": f"用户多次拒绝 {tool},建议从 Agent config 中移除", }) return patterns # Integration with evolution mechanism: # memory-organizer daemon reads denial patterns # → writes to MemoryCard: "用户对 xhs Agent 的 image_generate 多次拒绝" # → Companion 建议: "要不要从小红书 Agent 中移除图片生成步骤?" | ||
| ```typescript | ||
| loadAllPermissionRulesFromDisk(): | ||
| #### Step 4: Tool pool filtering by deny rules | ||
| if allowManagedPermissionRulesOnly = true: | ||
| → ONLY load from policySettings (enterprise lockdown) | ||
| else: | ||
| → Load from ALL enabled sources: | ||
| [userSettings, projectSettings, localSettings, | ||
| flagSettings, policySettings] | ||
| (last source wins on conflict) | ||
| class ToolAssembler: def assemble(self, config: AgentConfig, policy: PermissionContext) -> list[BaseTool]: """Filter tools BEFORE model sees them.""" pool = [] for tool_name in config.tools: tool = self.registry.get(tool_name) if not tool: continue # Deny rules filter at assembly time — model never sees denied tools if self._matches_deny_rule(tool.name, policy.deny_rules): continue pool.append(tool) return pool # Impact: if daemon agent has deny_rules=["*_publish", "*_send"], # the LLM won't even know email_send or xhs_publish exist. # This is more secure than runtime denial (model can't try to bypass). | ||
| // Dangerous Permission filtering: | ||
| isDangerousBashPermission(): | ||
| python:*, node:*, ruby:*, ... | ||
| isDangerousPowerShellPermission(): | ||
| iex, Invoke-Expression, Start-Process, ... | ||
| // Auto mode entry automatically strips these (prevent code-exec bypass) | ||
| ``` | ||
| III | ||
| --- | ||
| ## How to Verify | ||
| ## Feature Flags & GrowthBook | ||
| ### Unit Tests | ||
| ```typescript | ||
| // Build-time | ||
| BASH_CLASSIFIER — Bash command classification for auto-mode | ||
| TRANSCRIPT_CLASSIFIER — enables 'auto' permission mode | ||
| * **test_daemon_cannot_write** — daemon mode → tool.is_read_only()=False → denied | ||
| * **test_capability_asks_for_destructive** — capability mode + xhs_publish (destructive) → behavior="ask" | ||
| * **test_spawned_agent_skips_prompt** — avoid_prompts=True → no "ask", auto allow or deny | ||
| * **test_deny_rule_removes_tool** — deny_rules=["email_send"] → tool not in assembled pool | ||
| * **test_hook_overrides_permission** — pre-hook returns permission_override="deny" → tool not executed | ||
| * **test_denial_tracking** — deny tool 3 times → get_patterns returns suggestion | ||
| * **test_pipeline_layer_order** — deny rule checked before validation before hooks | ||
| // GrowthBook | ||
| tengu_birch_trellis (true) — Bash permission defaults | ||
| tengu_destructive_command_warning (false) — destructive command warnings | ||
| IV | ||
| ## User Perception | ||
| Before | ||
| 用户: 帮我发这篇文章到小红书 | ||
| 小红书 Agent: 已发布 ✓ | ||
| [无确认环节,误发了敏感内容] | ||
| [health-check daemon 意外调了 email_send] | ||
| [daemon 不应该有发送能力,但 ToolPolicy 没区分] | ||
| After | ||
| 用户: 帮我发这篇文章到小红书 | ||
| 小红书 Agent: 即将发布到小红书,确认吗? [确认/取消] | ||
| 用户: 确认 | ||
| 小红书 Agent: 已发布 ✓ | ||
| [daemon 的 tool pool 里根本没有 email_send] | ||
| [安全 by design,不依赖 runtime check] | ||
| [用户第 3 次拒绝图片生成] | ||
| Companion: 你多次跳过了图片生成,要不要从小红书 Agent 中移除这一步? | ||
| **核心体感差异:**(1) 破坏性操作必须确认 — 不会误发/误删;(2) 后台 agent 天然安全,不会越权;(3) 重复拒绝触发进化建议 — Agent 自动适应用户习惯。 | ||
| ToolPolicy PermissionMode Layer 0 保护 进化机制 denial tracking | ||
| // Env vars | ||
| CLAUDE_CODE_TWO_STAGE_CLASSIFIER — two-stage bash classification | ||
| CLAUDE_CODE_DUMP_AUTO_MODE — dump classifier decisions (debug) | ||
| ``` |
+139
-76
@@ -1,117 +0,180 @@ | ||
| [← Back to Index](index.html) | ||
| # 08. Configuration System | ||
| # 08\. Configuration System Medium | ||
| > Multi-source settings cascade + Zod validation + cache + change detection. | ||
| Multi-source settings cascade + Zod validation + cache + change detection | ||
| ## Key Files | ||
| R | ||
| ``` | ||
| src/utils/settings/settings.ts — load/get/cache settings | ||
| src/utils/settings/types.ts — SettingsJson schema type | ||
| src/utils/settings/constants.ts — SETTING_SOURCES priority order | ||
| src/utils/settings/settingsCache.ts — 3-layer cache system | ||
| src/utils/settings/mdm/settings.ts — MDM priority chain (5 levels) | ||
| src/utils/settings/mdm/rawRead.ts — subprocess plist/registry read | ||
| src/utils/settings/changeDetector.ts — file watch for changes | ||
| src/services/settingsSync/index.ts — cloud sync | ||
| ``` | ||
| ## Feature Reference [→ Full GrowthBook Index](00-feature-discovery.html#flags-gb) | ||
| --- | ||
| ### Settings File Locations | ||
| ## Settings File Locations | ||
| // Project scope .claude/settings.json — project settings .claude/settings.local.json — local overrides (gitignored) // User scope ~/.claude/settings.json — user global settings ~/.cowork/cowork_settings.json — cowork variant // Managed scope (enterprise) <managed-path>/.claude/settings.json managed-settings.json | ||
| ``` | ||
| // Project scope | ||
| .claude/settings.json — project settings | ||
| .claude/settings.local.json — local overrides (gitignored) | ||
| ### MDM (Enterprise) Read Paths | ||
| // User scope | ||
| ~/.claude/settings.json — user global settings | ||
| ~/.cowork/cowork_settings.json — cowork variant | ||
| // macOS /Library/Managed Preferences/com.anthropic.claudecode.plist Preference domain: com.anthropic.claudecode // Windows HKLM\Software\Anthropic\ClaudeCode (admin) HKCU\Software\Anthropic\ClaudeCode (user) | ||
| // Managed scope (enterprise) | ||
| <managed-path>/.claude/settings.json | ||
| managed-settings.json | ||
| ``` | ||
| ### Feature Flags & GrowthBook | ||
| --- | ||
| // Build-time DOWNLOAD_USER_SETTINGS — download settings from cloud UPLOAD_USER_SETTINGS — upload settings to cloud // GrowthBook tengu_strap_foyer (false) — settings sync feature gate | ||
| ## MDM (Enterprise) Read Paths | ||
| ### Key Files | ||
| ``` | ||
| // macOS | ||
| /Library/Managed Preferences/com.anthropic.claudecode.plist | ||
| Preference domain: com.anthropic.claudecode | ||
| src/utils/settings/settings.ts — load/get/cache settings src/utils/settings/types.ts — SettingsJson schema type src/utils/settings/constants.ts — SETTING_SOURCES priority order src/utils/settings/settingsCache.ts — 3-layer cache system src/utils/settings/mdm/settings.ts — MDM priority chain (5 levels) src/utils/settings/mdm/rawRead.ts — subprocess plist/registry read src/utils/settings/changeDetector.ts — file watch for changes src/services/settingsSync/index.ts — cloud sync | ||
| // Windows | ||
| HKLM\Software\Anthropic\ClaudeCode (admin) | ||
| HKCU\Software\Anthropic\ClaudeCode (user) | ||
| ``` | ||
| ### Evidence Boundary | ||
| --- | ||
| src/utils/settings/constants.ts:7-20 定义 settings source 顺序;src/utils/settings/settings.ts:309-358 证明 per-source 读取与 flag inline merge;src/utils/settings/settingsCache.ts:1-57 证明 3 层 cache;src/utils/settings/changeDetector.ts:1-120 证明是 chokidar + polling 的变更检测,不是抽象层上的“热重载”口号。 | ||
| ## `SETTING_SOURCES` Priority Chain (`constants.ts`) | ||
| ### SETTING_SOURCES 优先级链 (constants.ts) | ||
| ```typescript | ||
| SETTING_SOURCES (order matters — last wins): | ||
| ['userSettings', 'projectSettings', 'localSettings', | ||
| 'flagSettings', 'policySettings'] | ||
| SETTING_SOURCES (order matters — last wins): ['userSettings', 'projectSettings', 'localSettings', 'flagSettings', 'policySettings'] // 对应 settings 文件: getSettingsFilePathForSource(source): userSettings → ~/.claude/settings.json projectSettings → .claude/settings.json localSettings → .claude/settings.local.json flagSettings → --settings file path (optional) policySettings → 特殊: remote / MDM / managed file 链 // 注意: // - 这是 settings cascade,不等于 agents/skills 的 override 顺序 // - policySettings 内部是 first-source-wins,不是 merge 全部管理源 | ||
| // Corresponding settings files: | ||
| getSettingsFilePathForSource(source): | ||
| userSettings → ~/.claude/settings.json | ||
| projectSettings → .claude/settings.json | ||
| localSettings → .claude/settings.local.json | ||
| flagSettings → --settings file path (optional) | ||
| policySettings → special: remote / MDM / managed file chain | ||
| ### SettingsJson Schema (types.ts) | ||
| // Note: | ||
| // - This is settings cascade, not the same as agents/skills override order | ||
| // - policySettings internally is first-source-wins, not merge-all managed sources | ||
| ``` | ||
| SettingsJson = { // Customization surfaces skills?: [...] agents?: [...] commands?: [...] // Environment environmentVariables?: Record<string, string> defaultModel?: string // Permissions permissions?: { allow?: string[] // "Bash", "Bash(prefix:git)" deny?: string[] ask?: string[] defaultMode?: PermissionMode disableBypassPermissionsMode?: 'disable' disableAutoMode?: 'disable' (ant-only) additionalDirectories?: string[] } // Memory & MCP autoMemoryEnabled?: boolean managedMcpServers?: [{ serverName, serverCommand, serverUrl }] allowedMcpServers?: [...] deniedMcpServers?: [...] // Enterprise lockdown strictPluginOnlyCustomization?: boolean allowManagedPermissionRulesOnly?: boolean // disables user rules! // Hooks hooks?: HooksSettings } | ||
| --- | ||
| ### Settings 加载 & 缓存 3 层 | ||
| ## `SettingsJson` Schema (`types.ts`) | ||
| ═══ 3-Layer Cache (settingsCache.ts) ═══ Layer 1: sessionSettingsCache — 合并后的最终结果 Layer 2: perSourceCache — per-source parsed JSON Layer 3: parseFileCache — disk file → parsed Reset triggers: \- Settings file write \- Plugin init \- Hook refresh \- --add-dir command ═══ 加载流程 (settings.ts:200-250) ═══ getSettingsForSource(source): 1\. Check cache first (setCachedSettingsForSource) 2\. Parse file + validate Zod schema 3\. Cache results Special sources: policySettings → loadManagedFileSettings(): managed-settings.json + managed-settings.d/*.json (sorted alpha) → Merge via settingsMergeCustomizer() flagSettings → --settings flag inline JSON Merge strategy: permissions.allow/deny/ask → array merge (no dupes) managedMcpServers → append arrays Scalars → last source wins | ||
| ```typescript | ||
| SettingsJson = { | ||
| // Customization surfaces | ||
| skills?: [...] | ||
| agents?: [...] | ||
| commands?: [...] | ||
| ### MDM 优先级链 (mdm/settings.ts:40-95) | ||
| // Environment | ||
| environmentVariables?: Record<string, string> | ||
| defaultModel?: string | ||
| MDM Profile Priority (highest → lowest): 1\. Remote (via API) 2\. macOS: /Library/Managed Preferences/ (plist, admin-only) 3\. Windows: HKLM\SOFTWARE\Policies\ClaudeCode (registry, admin) 4\. managed-settings.json file 5\. Windows: HKCU\SOFTWARE\Policies\ClaudeCode (user-writable) Async Load Pipeline: startMdmSettingsLoad() — startup 时 fire (subprocess) ensureMdmSettingsLoaded() — await before first read Cache: mdmCache, hkcuCache rawRead.ts — 子进程读 plist/registry → fires early, zero heavy imports → parallel with other startup work | ||
| // Permissions | ||
| permissions?: { | ||
| allow?: string[] // "Bash", "Bash(prefix:git)" | ||
| deny?: string[] | ||
| ask?: string[] | ||
| defaultMode?: PermissionMode | ||
| disableBypassPermissionsMode?: 'disable' | ||
| disableAutoMode?: 'disable' // ant-only | ||
| additionalDirectories?: string[] | ||
| } | ||
| I | ||
| // Memory & MCP | ||
| autoMemoryEnabled?: boolean | ||
| managedMcpServers?: [{ serverName, serverCommand, serverUrl }] | ||
| allowedMcpServers?: [...] | ||
| deniedMcpServers?: [...] | ||
| ## Comparison | ||
| // Enterprise lockdown | ||
| strictPluginOnlyCustomization?: boolean | ||
| allowManagedPermissionRulesOnly?: boolean // disables user rules! | ||
| Claude Code | ||
| // Hooks | ||
| hooks?: HooksSettings | ||
| } | ||
| ``` | ||
| * **Settings cascade** : plugin base + user/project/local/flag/policy,later source wins | ||
| * **Zod validation** : load-time schema check,invalid config → clear error | ||
| * **MDM (enterprise)** : 管理员可覆写用户配置 | ||
| * **Hot-reload** : changeDetector 监听 settings.json 变更 | ||
| * **Settings scopes** : global / project / local,higher scope overrides lower | ||
| * **Deny rule sources** : 每条规则标记来源(config/user/mdm/hook) | ||
| --- | ||
| Mitsein | ||
| ## Settings Loading & 3-Layer Cache (`settingsCache.ts`) | ||
| * **config.py** : AgentConfig from YAML + DB 两种来源 | ||
| * 无 priority resolution — YAML 和 DB 二选一,没有 merge | ||
| * 无 load-time validation — invalid config 只在 runtime 报错 | ||
| * 无 hot-reload — 改 config 要重启 | ||
| * Phase 5 计划: agent_config 表 + YAML fallback | ||
| ```typescript | ||
| ═══ 3-Layer Cache ═══ | ||
| Layer 1: sessionSettingsCache — merged final result | ||
| Layer 2: perSourceCache — per-source parsed JSON | ||
| Layer 3: parseFileCache — disk file → parsed | ||
| II | ||
| Reset triggers: | ||
| - Settings file write | ||
| - Plugin init | ||
| - Hook refresh | ||
| - --add-dir command | ||
| ## How to Implement | ||
| ═══ Loading Flow (settings.ts:200-250) ═══ | ||
| getSettingsForSource(source): | ||
| 1. Check cache first (setCachedSettingsForSource) | ||
| 2. Parse file + validate Zod schema | ||
| 3. Cache results | ||
| #### Step 1: Pydantic model for AgentConfig | ||
| Special sources: | ||
| policySettings → loadManagedFileSettings(): | ||
| managed-settings.json + managed-settings.d/*.json (sorted alpha) | ||
| → Merge via settingsMergeCustomizer() | ||
| flagSettings → --settings flag inline JSON | ||
| from pydantic import BaseModel, field_validator from enum import Enum class AgentType(str, Enum): COMPANION = "companion" CAPABILITY = "capability" WORKFLOW = "workflow" DAEMON = "daemon" class DependsOn(BaseModel): output_type: str class AgentConfig(BaseModel): id: str slug: str type: AgentType name: str | None = None icon: str | None = None output_type: str | None = None spawnable: bool = False depends_on: list[DependsOn] | None = None system_prompt: str | None = None tools: list[str] = [] skills: list[str] = [] source: str = "platform" user_id: str | None = None @field_validator("tools") @classmethod def validate_tools_exist(cls, v): from mitsein.tools import TOOL_REGISTRY for name in v: if name not in TOOL_REGISTRY: raise ValueError(f"Tool '{name}' not found in registry") return v @field_validator("type", "spawnable") @classmethod def validate_companion_not_spawnable(cls, v, info): if info.data.get("type") == "companion" and v is True: raise ValueError("Companion cannot be spawnable") return v | ||
| Merge strategy: | ||
| permissions.allow/deny/ask → array merge (no dupes) | ||
| managedMcpServers → append arrays | ||
| Scalars → last source wins | ||
| ``` | ||
| #### Step 2: Priority-based resolution | ||
| --- | ||
| class ConfigResolver: """Resolve AgentConfig from multiple sources with priority.""" PRIORITY = [ "runtime_override", # API request params (highest) "user_private", # agent_config WHERE user_id = current_user "store_installed", # agent_config WHERE source = 'store' "platform", # agent_config WHERE source = 'platform' "yaml_fallback", # agents/*.yaml (lowest, development only) ] async def resolve(self, slug: str, user_id: str) -> AgentConfig: for source in self.PRIORITY: config = await self._load_from_source(source, slug, user_id) if config: return config raise ConfigNotFoundError(f"No config for '{slug}'") async def _load_from_source(self, source, slug, user_id): if source == "runtime_override": return self._overrides.get(slug) elif source == "user_private": return await self.db.get_config(slug=slug, user_id=user_id) elif source == "store_installed": return await self.db.get_config(slug=slug, source="store") elif source == "platform": return await self.db.get_config(slug=slug, source="platform") elif source == "yaml_fallback": return self._yaml_configs.get(slug) | ||
| ## MDM Priority Chain (`mdm/settings.ts:40-95`) | ||
| #### Step 3: Config change detection | ||
| ```typescript | ||
| MDM Profile Priority (highest → lowest): | ||
| 1. Remote (via API) | ||
| 2. macOS: /Library/Managed Preferences/ (plist, admin-only) | ||
| 3. Windows: HKLM\SOFTWARE\Policies\ClaudeCode (registry, admin) | ||
| 4. managed-settings.json file | ||
| 5. Windows: HKCU\SOFTWARE\Policies\ClaudeCode (user-writable) | ||
| class ConfigWatcher: """Watch for config changes and notify subscribers.""" def __init__(self, yaml_dir: Path): self._yaml_dir = yaml_dir self._last_hashes: dict[str, str] = {} self._subscribers: list[Callable] = [] async def check_for_changes(self): """Called periodically or by file watcher.""" for yaml_file in self._yaml_dir.glob("*.yaml"): current_hash = hashlib.md5(yaml_file.read_bytes()).hexdigest() slug = yaml_file.stem if slug in self._last_hashes and self._last_hashes[slug] != current_hash: await self._notify_change(slug) self._last_hashes[slug] = current_hash async def _notify_change(self, slug: str): config = load_yaml_config(self._yaml_dir / f"{slug}.yaml") validated = AgentConfig(**config) # Pydantic validation for sub in self._subscribers: await sub(slug, validated) | ||
| Async Load Pipeline: | ||
| startMdmSettingsLoad() — fires at startup (subprocess) | ||
| ensureMdmSettingsLoaded() — await before first read | ||
| Cache: mdmCache, hkcuCache | ||
| III | ||
| rawRead.ts — subprocess reads plist/registry | ||
| → fires early, zero heavy imports | ||
| → parallel with other startup work | ||
| ``` | ||
| ## How to Verify | ||
| --- | ||
| * **test_invalid_tool_rejected** — AgentConfig with tools=["nonexistent"] → Pydantic ValidationError | ||
| * **test_companion_not_spawnable** — type=companion + spawnable=True → ValidationError | ||
| * **test_priority_resolution** — user override > platform default for same slug | ||
| * **test_yaml_fallback** — DB 无记录时 fallback 到 YAML 文件 | ||
| * **test_config_change_detected** — 修改 YAML 文件后 subscriber 被通知 | ||
| * **test_invalid_yaml_change_rejected** — 改成 invalid YAML → validation error,不影响运行中的 config | ||
| ## Feature Flags & GrowthBook | ||
| IV | ||
| ``` | ||
| // Build-time | ||
| DOWNLOAD_USER_SETTINGS — download settings from cloud | ||
| UPLOAD_USER_SETTINGS — upload settings to cloud | ||
| ## User Perception | ||
| Before | ||
| [YAML 里写了 tools: ["web_serach"] (typo)] | ||
| [启动 Agent,运行到调 web_search 时才报错] | ||
| [浪费了前面所有 LLM calls 的 token] | ||
| After | ||
| [YAML 里写了 tools: ["web_serach"] (typo)] | ||
| [Load-time validation error: Tool 'web_serach' not found] | ||
| [Agent 不启动,0 token 浪费] | ||
| **核心体感差异:** Config 错误在加载时就被发现,而非运行时。用户 override platform config 时有明确的优先级规则。开发时改 YAML 不需要重启。 | ||
| config.py AgentConfig Pydantic Phase 5 hot-reload | ||
| // GrowthBook | ||
| tengu_strap_foyer (false) — settings sync feature gate | ||
| ``` |
+154
-90
@@ -1,129 +0,193 @@ | ||
| [← Back to Index](index.html) | ||
| # 09. Streaming & Progress Tracking | ||
| # 09\. Streaming & Progress Tracking High | ||
| > Async generator pipeline + structured progress + backpressure. | ||
| Async generator pipeline + structured progress + backpressure | ||
| ## Key Files | ||
| R | ||
| ``` | ||
| src/services/api/claude.ts — API streaming generator (L800-2100+) | ||
| src/services/api/withRetry.ts — retry logic (L150-400) | ||
| src/services/api/errors.ts — error classification | ||
| src/services/api/logging.ts — API telemetry (L213-700) | ||
| src/services/api/client.ts — Anthropic client creation | ||
| src/services/tools/StreamingToolExecutor.ts — concurrent tool streaming | ||
| src/services/tools/toolOrchestration.ts — partition + dispatch | ||
| ``` | ||
| ## Feature Reference [→ Full Flag Index](00-feature-discovery.html#flags-build) | ||
| --- | ||
| ### Streaming Function | ||
| ## Core Streaming Function (`src/services/api/claude.ts`) | ||
| // src/services/api/claude.ts queryModelWithStreaming( messages, tools, systemPrompt, model, maxTokens, betas, thinkingConfig, ... ): AsyncGenerator<Stream<BetaMessage>> // Retry: exponential backoff 500ms → 120s // DEFAULT_MAX_RETRIES = 10 // 429 (rate limit) + 529 (capacity) → retryable // 529 only retries for FOREGROUND_529_RETRY_SOURCES | ||
| ```typescript | ||
| queryModelWithStreaming( | ||
| messages, tools, systemPrompt, model, | ||
| maxTokens, betas, thinkingConfig, ... | ||
| ): AsyncGenerator<Stream<BetaMessage>> | ||
| ### Progress Types | ||
| // Retry: exponential backoff 500ms → 120s | ||
| // DEFAULT_MAX_RETRIES = 10 | ||
| // 429 (rate limit) + 529 (capacity) → retryable | ||
| // 529 only retries for FOREGROUND_529_RETRY_SOURCES | ||
| ``` | ||
| ToolCallProgress<P extends ToolProgressData> = { toolName: string toolUseID: string data: P // tool-specific progress } ToolProgressData = | bash_progress // shell output streaming | powershell_progress // PowerShell output | mcp_progress // MCP tool progress | sleep_progress // sleep countdown | ||
| --- | ||
| ### Feature Flags & Env Vars | ||
| ## `claude()` Generator — Core Signature (`claude.ts:800-810`) | ||
| // Build-time CONNECTOR_TEXT — text block summarization // GrowthBook tengu_fgts (false) — fine-grained tool streaming // Env vars CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK — disable non-streaming fallback | ||
| ```typescript | ||
| export async function* claude(options: Options): | ||
| AsyncGenerator<StreamEvent, Message> | ||
| ### Key Files | ||
| Options = { | ||
| getToolPermissionContext: () => Promise<ToolPermissionContext> | ||
| model: string | ||
| toolChoice?: BetaToolChoiceTool | BetaToolChoiceAuto | ||
| isNonInteractiveSession: boolean | ||
| extraToolSchemas?: BetaToolUnion[] | ||
| maxOutputTokensOverride?: number | ||
| fallbackModel?: string | ||
| onStreamingFallback?: () => void | ||
| querySource: QuerySource // e.g. 'repl_main_thread' | ||
| agents: AgentDefinition[] | ||
| allowedAgentTypes?: string[] | ||
| hasAppendSystemPrompt: boolean | ||
| enablePromptCaching?: boolean | ||
| skipCacheWrite?: boolean | ||
| temperatureOverride?: number | ||
| effortValue?: EffortValue | ||
| mcpTools: Tools | ||
| hasPendingMcpServers?: boolean | ||
| outputFormat?: BetaJSONOutputFormat | ||
| fastMode?: boolean | ||
| advisorModel?: string | ||
| thinkingConfig: ThinkingConfig | ||
| webSearchEnabled?: boolean | ||
| } | ||
| src/services/api/claude.ts — API streaming generator (L800-2100+) src/services/api/withRetry.ts — retry logic (L150-400) src/services/api/errors.ts — error classification src/services/api/logging.ts — API telemetry (L213-700) src/services/api/client.ts — Anthropic client creation src/services/tools/StreamingToolExecutor.ts — concurrent tool streaming src/services/tools/toolOrchestration.ts — partition + dispatch | ||
| // Stream event loop (L1100-1400): | ||
| // content_block_start → initialize | ||
| // content_block_delta → accumulate | ||
| // CONNECTOR_TEXT: delta.connector_text += ... | ||
| // content_block_stop → finalize + yield message block | ||
| ``` | ||
| ### claude() Generator — 核心签名 (claude.ts:800-810) | ||
| --- | ||
| export async function* claude(options: Options): AsyncGenerator<StreamEvent, Message> Options = { getToolPermissionContext: () => Promise<ToolPermissionContext> model: string toolChoice?: BetaToolChoiceTool | BetaToolChoiceAuto isNonInteractiveSession: boolean extraToolSchemas?: BetaToolUnion[] maxOutputTokensOverride?: number fallbackModel?: string onStreamingFallback?: () => void querySource: QuerySource // e.g. 'repl_main_thread' agents: AgentDefinition[] allowedAgentTypes?: string[] hasAppendSystemPrompt: boolean enablePromptCaching?: boolean skipCacheWrite?: boolean temperatureOverride?: number effortValue?: EffortValue mcpTools: Tools hasPendingMcpServers?: boolean outputFormat?: BetaJSONOutputFormat fastMode?: boolean advisorModel?: string thinkingConfig: ThinkingConfig webSearchEnabled?: boolean } // Stream event loop (L1100-1400): // content_block_start → initialize // content_block_delta → accumulate // CONNECTOR_TEXT: delta.connector_text += ... // content_block_stop → finalize + yield message block | ||
| ## Progress Types | ||
| ### Retry 策略详解 (withRetry.ts:150-400) | ||
| ```typescript | ||
| ToolCallProgress<P extends ToolProgressData> = { | ||
| toolName: string | ||
| toolUseID: string | ||
| data: P // tool-specific progress | ||
| } | ||
| export async function* withRetry<T>( getClient, operation, options ): AsyncGenerator<SystemAPIErrorMessage, T> ═══ 重试参数 ═══ DEFAULT_MAX_RETRIES = 10 ═══ 429/529 处理 ═══ 1\. Extract retry-after header (L245) 2\. Short retry (<SHORT_RETRY_THRESHOLD_MS): → wait + retry with fast-mode ON (L278) 3\. Long retry: → fast-mode cooldown, retry at standard speed ═══ Fast-Mode Fallback (L308-330) ═══ "fast mode not enabled" error → disable + retry First 529 (non-premium) → fall back immediately ═══ Persistent Retry (UNATTENDED_RETRY feature) ═══ env: CLAUDE_CODE_UNATTENDED_RETRY Max backoff: 5 minutes Reset cap: 6 hours Heartbeat: SystemAPIErrorMessage every 30s (L77-85) ═══ Socket Errors (ECONNRESET, EPIPE) ═══ → disableKeepAlive() connection pooling (L237) → Retry with fresh connection ═══ Foreground 529 Sources (L32-42) ═══ FOREGROUND_529_RETRY_SOURCES = { 'repl_main_thread', 'repl_main_thread:outputStyle:*', 'sdk', 'agent:*', 'compact', 'hook_agent', 'hook_prompt', 'verification_agent', 'side_question', 'auto_mode', 'bash_classifier' } // Non-foreground (summaries, titles, classifiers) // → bail immediately, 不排队等 capacity | ||
| ToolProgressData = | ||
| | bash_progress // shell output streaming | ||
| | powershell_progress // PowerShell output | ||
| | mcp_progress // MCP tool progress | ||
| | sleep_progress // sleep countdown | ||
| ``` | ||
| ### Error Classification (errors.ts) | ||
| --- | ||
| classifyAPIError() → 分类: 'auth' — 401, 403, OAuth revoked 'overloaded' — 529 (capacity) 'rate_limit' — 429 (rate limit) 'context_window' — context_length_exceeded, max_tokens overflow 'model_not_found' — model unavailable 'connection_error' — network failure 'validation_error' — request validation 'other' — uncategorized // API Logging (logging.ts): logAPIQuery() — input (model, messagesLength, betas, permissionMode) logAPIError() — error (status, type, duration, attempt, gateway) logAPISuccessAndDuration() — success (tokens, cost, cache stats) | ||
| ## Retry Strategy (`withRetry.ts:150-400`) | ||
| ### Backpressure & Connection | ||
| ```typescript | ||
| export async function* withRetry<T>( | ||
| getClient, operation, options | ||
| ): AsyncGenerator<SystemAPIErrorMessage, T> | ||
| ═══ Generator Backpressure ═══ implicit: caller controls consumption via for-await 无显式 buffer management — async generator 自身就是 backpressure ═══ Connection ═══ Default timeout: 600 seconds (client.ts L122) Connection pooling: keep-alive by default → ECONNRESET fallback: disable pooling + retry ═══ Ephemeral Progress Types ═══ NEVER persisted to transcript: bash_progress, powershell_progress, mcp_progress, sleep_progress (PROACTIVE|KAIROS) → isEphemeralToolProgress() filter at write time | ||
| ═══ Retry Parameters ═══ | ||
| DEFAULT_MAX_RETRIES = 10 | ||
| I | ||
| ═══ 429/529 Handling ═══ | ||
| 1. Extract retry-after header (L245) | ||
| 2. Short retry (< SHORT_RETRY_THRESHOLD_MS): | ||
| → wait + retry with fast-mode ON (L278) | ||
| 3. Long retry: | ||
| → fast-mode cooldown, retry at standard speed | ||
| ## Comparison | ||
| ═══ Fast-Mode Fallback (L308-330) ═══ | ||
| "fast mode not enabled" error → disable + retry | ||
| First 529 (non-premium) → fall back immediately | ||
| Claude Code | ||
| ═══ Persistent Retry (UNATTENDED_RETRY feature) ═══ | ||
| env: CLAUDE_CODE_UNATTENDED_RETRY | ||
| Max backoff: 5 minutes | ||
| Reset cap: 6 hours | ||
| Heartbeat: SystemAPIErrorMessage every 30s (L77-85) | ||
| * **Full async generator pipeline** : API stream → message aggregator → tool executor → query engine → UI | ||
| * **Tool progress** : onProgress callback with structured data (bytes, lines, percent) | ||
| * **Backpressure** : for-await-of 自然控制节奏 | ||
| * **Concurrent tool streaming** : 多个 tool 同时 stream progress | ||
| * **Progress message types** : separate from result messages | ||
| * **Live-updating UI** : Ink terminal components render in-place | ||
| ═══ Socket Errors (ECONNRESET, EPIPE) ═══ | ||
| → disableKeepAlive() connection pooling (L237) | ||
| → Retry with fresh connection | ||
| Mitsein | ||
| ═══ Foreground 529 Sources (L32-42) ═══ | ||
| FOREGROUND_529_RETRY_SOURCES = { | ||
| 'repl_main_thread', 'repl_main_thread:outputStyle:*', | ||
| 'sdk', 'agent:*', 'compact', | ||
| 'hook_agent', 'hook_prompt', | ||
| 'verification_agent', 'side_question', | ||
| 'auto_mode', 'bash_classifier' | ||
| } | ||
| // Non-foreground (summaries, titles, classifiers) | ||
| // → bail immediately, don't queue for capacity | ||
| ``` | ||
| * LLM streaming: SSE to frontend (基础实现) | ||
| * Tool 执行: 等完成才返回结果 | ||
| * 无 tool progress callback | ||
| * 前端只显示 "Agent 运行中..." | ||
| * 无 backpressure — 如果前端慢,后端不会 pause | ||
| --- | ||
| II | ||
| ## Error Classification (`errors.ts`) | ||
| ## How to Implement | ||
| ```typescript | ||
| classifyAPIError() → categories: | ||
| 'auth' — 401, 403, OAuth revoked | ||
| 'overloaded' — 529 (capacity) | ||
| 'rate_limit' — 429 (rate limit) | ||
| 'context_window' — context_length_exceeded, max_tokens overflow | ||
| 'model_not_found' — model unavailable | ||
| 'connection_error' — network failure | ||
| 'validation_error' — request validation | ||
| 'other' — uncategorized | ||
| #### Step 1: Async generator protocol for orchestrator | ||
| // API Logging (logging.ts): | ||
| logAPIQuery() — input (model, messagesLength, betas, permissionMode) | ||
| logAPIError() — error (status, type, duration, attempt, gateway) | ||
| logAPISuccessAndDuration() — success (tokens, cost, cache stats) | ||
| ``` | ||
| from typing import AsyncGenerator @dataclass class StreamEvent: type: str # "text_delta" | "tool_start" | "tool_progress" | "tool_result" | "done" data: dict async def run_agent_stream( config: AgentConfig, thread_id: str, user_message: str ) -> AsyncGenerator[StreamEvent, None]: """Main loop as async generator — every yield = one event to frontend.""" messages = await load_messages(thread_id) messages.append(user_message) async for chunk in llm.stream(messages=messages): if chunk.type == "text_delta": yield StreamEvent(type="text_delta", data={"text": chunk.text}) elif chunk.type == "tool_use": yield StreamEvent(type="tool_start", data={ "tool": chunk.tool_name, "input": chunk.input }) # Execute tool with progress async for progress in execute_tool_stream(chunk.tool, chunk.input): yield progress yield StreamEvent(type="done", data={}) | ||
| --- | ||
| #### Step 2: Tool execution as async generator | ||
| ## Backpressure & Connection | ||
| async def execute_tool_stream( tool: BaseTool, input: BaseModel ) -> AsyncGenerator[StreamEvent, None]: """Wrap tool execution to yield progress events.""" progress_queue: asyncio.Queue[dict] = asyncio.Queue() def on_progress(data: dict): progress_queue.put_nowait(data) # Run tool in background, collecting progress task = asyncio.create_task(tool.execute(input, on_progress=on_progress)) while not task.done(): try: progress = await asyncio.wait_for(progress_queue.get(), timeout=0.5) yield StreamEvent(type="tool_progress", data=progress) except asyncio.TimeoutError: continue # No progress yet, keep waiting result = await task yield StreamEvent(type="tool_result", data={ "tool": tool.name, "result": result.data, }) | ||
| ```typescript | ||
| ═══ Generator Backpressure ═══ | ||
| Implicit: caller controls consumption via for-await | ||
| No explicit buffer management — async generator IS the backpressure | ||
| #### Step 3: SSE endpoint with backpressure | ||
| ═══ Connection ═══ | ||
| Default timeout: 600 seconds (client.ts L122) | ||
| Connection pooling: keep-alive by default | ||
| → ECONNRESET fallback: disable pooling + retry | ||
| # api/routes/stream.py (FastAPI) from fastapi.responses import StreamingResponse @router.post("/agents/{config_id}/stream") async def stream_agent(config_id: str, body: StreamRequest): config = await resolve_config(config_id) async def event_generator(): async for event in run_agent_stream(config, body.thread_id, body.message): # SSE format yield f"data: {json.dumps(event.__dict__)}\n\n" # Natural backpressure: if client is slow reading, # TCP buffer fills → write blocks → generator pauses return StreamingResponse( event_generator(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}, ) | ||
| ═══ Ephemeral Progress Types ═══ | ||
| NEVER persisted to transcript: | ||
| bash_progress, powershell_progress, | ||
| mcp_progress, sleep_progress (PROACTIVE|KAIROS) | ||
| → isEphemeralToolProgress() filter at write time | ||
| ``` | ||
| #### Step 4: Structured progress for each tool | ||
| --- | ||
| # Each tool defines its own progress structure class PptGenerateTool: async def execute(self, input, context, on_progress=None): for i, slide in enumerate(input.slides): await self._render_slide(slide) if on_progress: on_progress({ "slides_done": i + 1, "slides_total": len(input.slides), "current_slide_title": slide.title, }) class ImageGenerateTool: async def execute(self, input, context, on_progress=None): for i, prompt in enumerate(input.prompts): url = await self._generate(prompt) if on_progress: on_progress({ "images_done": i + 1, "images_total": len(input.prompts), "last_image_url": url, }) class WebSearchTool: async def execute(self, input, context, on_progress=None): results = await self._search(input.query) if on_progress: on_progress({"results_count": len(results)}) return ToolResult(data=results) | ||
| ## Feature Flags & Env Vars | ||
| III | ||
| ``` | ||
| // Build-time | ||
| CONNECTOR_TEXT — text block summarization | ||
| ## How to Verify | ||
| // GrowthBook | ||
| tengu_fgts (false) — fine-grained tool streaming | ||
| * **test_stream_yields_text_delta** — LLM text 输出产生 text_delta events | ||
| * **test_stream_yields_tool_progress** — PPT 生成 12 slides → 12 个 tool_progress events | ||
| * **test_stream_order** — tool_start 在 tool_progress 之前,tool_result 在最后 | ||
| * **test_concurrent_tool_progress** — 两个 safe tools 同时执行,progress events 交错但各自有序 | ||
| * **test_sse_format** — 每个 event 以 "data: " 开头,"\n\n" 结尾 | ||
| * **test_backpressure** — 模拟慢 client,验证 generator 暂停而非 buffer 爆掉 | ||
| IV | ||
| ## User Perception | ||
| Before | ||
| 小红书 Agent: 正在处理... | ||
| [空白等待 25 秒] | ||
| [用户不知道 agent 在做什么] | ||
| [想取消但不确定是否还在工作] | ||
| 小红书 Agent: 完成了,这是结果。 | ||
| After | ||
| 小红书 Agent: 开始制作... | ||
| [🔍 搜索热点... 找到 15 条结果 ✓] | ||
| [✏️ 撰写文案... (draft v1)] | ||
| [🖼️ 生成配图... 2/4 张] | ||
| [🖼️ 生成配图... 4/4 张 ✓] | ||
| [📐 排版中...] | ||
| 小红书 Agent: 全部完成 ✓ | ||
| **核心体感差异:** 从"黑盒等待"变成"透明进度"。用户实时看到每一步在做什么、完成了多少。这大幅降低焦虑感,也让用户能在合适时机取消(比如看到搜索结果不满意就直接重来)。 | ||
| async generator SSE progress backpressure streaming | ||
| // Env vars | ||
| CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING | ||
| CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK — disable non-streaming fallback | ||
| ``` |
+155
-87
@@ -1,131 +0,199 @@ | ||
| [← Back to Index](index.html) | ||
| # 10. Session Management & Persistence | ||
| # 10\. Session Management & Persistence Medium | ||
| Transcript recording + execution metadata + session resume. | ||
| Transcript recording + execution metadata + session resume | ||
| ## Session Storage | ||
| R | ||
| ``` | ||
| // Path pattern | ||
| <project-dir>/<sessionId>.jsonl | ||
| ## Feature Reference [→ Full Env Var Index](00-feature-discovery.html#flags-env) | ||
| // Subagent transcripts | ||
| <project-dir>/<sessionId>/subagents/agent-<agentId>.jsonl | ||
| or | ||
| <project-dir>/<sessionId>/subagents/<subdir>/agent-<agentId>.jsonl | ||
| ### Session Storage | ||
| // Saved message types | ||
| user, assistant, attachment, system | ||
| // Path pattern <project-dir>/<sessionId>.jsonl // Subagent transcripts <project-dir>/<sessionId>/subagents/agent-<agentId>.jsonl or <project-dir>/<sessionId>/subagents/<subdir>/agent-<agentId>.jsonl // Saved message types user, assistant, attachment, system // Excluded (ephemeral) bash_progress, powershell_progress, mcp_progress, sleep_progress | ||
| // Excluded (ephemeral — never persisted) | ||
| bash_progress, powershell_progress, mcp_progress, sleep_progress | ||
| ``` | ||
| ### Feature Flags & Env Vars | ||
| ## Feature Flags & Env Vars | ||
| // Build-time BG_SESSIONS — background session management COMMIT_ATTRIBUTION — commit attribution tracking // Env vars CLAUDE_CODE_SKIP_PROMPT_HISTORY — skip prompt history CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS — emit state events CLAUDE_CODE_INCLUDE_PARTIAL_MESSAGES — telemetry partials CLAUDE_CODE_TERMINAL_RECORDING — terminal recording | ||
| ``` | ||
| // Build-time | ||
| BG_SESSIONS — background session management | ||
| COMMIT_ATTRIBUTION — commit attribution tracking | ||
| ### Key Functions (src/utils/sessionStorage.ts) | ||
| // Env vars | ||
| CLAUDE_CODE_SKIP_PROMPT_HISTORY — skip prompt history | ||
| CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS — emit state events | ||
| CLAUDE_CODE_INCLUDE_PARTIAL_MESSAGES — telemetry partials | ||
| CLAUDE_CODE_TERMINAL_RECORDING — terminal recording | ||
| ``` | ||
| getTranscriptPath() — resolve session file path saveMessage() — append message to JSONL loadTranscriptFile() — load full transcript isTranscriptMessage() — filter ephemeral types isChainParticipant() — check agent chain membership getSessionStorageInfo() — session metadata | ||
| ## Key Files | ||
| ### Key Files | ||
| ``` | ||
| src/utils/sessionStorage.ts — transcript persistence (JSONL) | ||
| src/utils/sessionRestore.ts — session resume logic (L55-110) | ||
| src/utils/conversationRecovery.ts — crash recovery strategy | ||
| src/bootstrap/state.ts — session ID + bootstrap state (L30-120) | ||
| ``` | ||
| src/utils/sessionStorage.ts — transcript persistence (JSONL) src/utils/sessionRestore.ts — session resume logic (L55-110) src/utils/conversationRecovery.ts — crash recovery strategy src/bootstrap/state.ts — session ID + bootstrap state (L30-120) | ||
| ## Key Functions (`src/utils/sessionStorage.ts`) | ||
| ### Evidence Boundary | ||
| | Function | Purpose | | ||
| |---|---| | ||
| | `getTranscriptPath()` | resolve session file path | | ||
| | `saveMessage()` | append message to JSONL | | ||
| | `loadTranscriptFile()` | load full transcript | | ||
| | `isTranscriptMessage()` | filter ephemeral types | | ||
| | `isChainParticipant()` | check agent chain membership | | ||
| | `getSessionStorageInfo()` | session metadata | | ||
| src/utils/sessionStorage.ts:202-261 直接证明 transcript 与 subagent path;src/utils/sessionStorage.ts:327-360 证明 remote-agent metadata sidecar;src/utils/sessionRestore.ts:99-170 证明 resume 不只是读消息,还会恢复 file history、todos 和 collapsed state。 | ||
| --- | ||
| ### Transcript 格式 & 路径 | ||
| ## Transcript Format & Path Resolution | ||
| // 格式: JSONL (每行一个完整 Entry) // Entry = message + metadata ═══ Path Resolution ═══ getTranscriptPath(): projectDir/{sessionId}.jsonl getAgentTranscriptPath(agentId): projectDir/{sessionId}/subagents/agent-{agentId}.jsonl or projectDir/{sessionId}/subagents/{subdir}/agent-{agentId}.jsonl getRemoteAgentMetadataPath(taskId): projectDir/{sessionId}/remote-agents/remote-agent-{taskId}.meta.json ═══ Size Limits ═══ MAX_TOMBSTONE_REWRITE_BYTES = 50MB // 防 OOM in rewrite MAX_TRANSCRIPT_READ_BYTES = 50MB // bail on too-large ═══ 过滤规则 ═══ isTranscriptMessage() — type guard for user/assistant/attachment/system Progress messages NOT persisted (isChainParticipant filters) ═══ Ephemeral Progress Types (NEVER persisted) ═══ EPHEMERAL_PROGRESS_TYPES = { 'bash_progress', 'powershell_progress', 'mcp_progress', 'sleep_progress' } → isEphemeralToolProgress() detects + skips persist | ||
| Format: JSONL (one complete Entry per line). Entry = message + metadata. | ||
| ### Session Resume 完整流程 (sessionRestore.ts:55-110) | ||
| ``` | ||
| // Path Resolution | ||
| getTranscriptPath(): | ||
| projectDir/{sessionId}.jsonl | ||
| restoreSessionStateFromLog(): 1\. Load transcript (loadTranscriptFile): \- Read JSONL \- Filter: unresolved tool uses, orphaned thinking, whitespace messages \- Return Message[] 2\. Restore file history (L62): fileHistoryRestoreStateFromLog() → reconstruct FileHistory from log snapshots 3\. Restore attribution (L68, COMMIT_ATTRIBUTION): attributionRestoreStateFromLog() → git commit tracking 4\. Restore context-collapse (L75, CONTEXT_COLLAPSE): restoreFromEntries() → rebuild collapsed view state 5\. Restore todos (L87): extractTodosFromTranscript() → scan for last TodoWrite tool_use → parse todos from tool input 6\. Validation: checkResumeConsistency() → sessionId match, no orphaned messages Parent UUID chain validation → detect forks | ||
| getAgentTranscriptPath(agentId): | ||
| projectDir/{sessionId}/subagents/agent-{agentId}.jsonl | ||
| or projectDir/{sessionId}/subagents/{subdir}/agent-{agentId}.jsonl | ||
| ### Bootstrap State 完整定义 (state.ts:30-120) | ||
| getRemoteAgentMetadataPath(taskId): | ||
| projectDir/{sessionId}/remote-agents/remote-agent-{taskId}.meta.json | ||
| State = { originalCwd: string // stable project root projectRoot: string // distinct from originalCwd sessionId: SessionId // UUID, set once at start cwd: string // current working directory // Cost tracking totalCostUSD: number totalAPIDuration: number // Model state mainLoopModelOverride?: ModelSetting modelUsage: { [modelName]: ModelUsage } // Permission/feature state toolPermissionContext: ToolPermissionContext isAutoModeAvailable: boolean // Settings flagSettingsPath?: string allowedSettingSources: SettingSource[] sessionCounter: AttributedCounter | null } // 关键函数: getSessionId() — immutable, set once getOriginalCwd() — stable project identity switchSession(newId) — for --resume flag setMainLoopModelOverride(model) — mid-session model change getLastApiCompletionTimestamp() — idle detection getTotalInputTokens() — cumulative usage addToTurnToolDuration() — per-turn metrics | ||
| // Size Limits | ||
| MAX_TOMBSTONE_REWRITE_BYTES = 50MB // prevent OOM in rewrite | ||
| MAX_TRANSCRIPT_READ_BYTES = 50MB // bail on too-large | ||
| ### Crash Recovery (conversationRecovery.ts) | ||
| // Filter rules | ||
| isTranscriptMessage() — type guard for user/assistant/attachment/system | ||
| Progress messages NOT persisted (isChainParticipant filters) | ||
| ConversationRecoveryStrategy = 'full' | 'partial' | 'none' // Controlled by: CLAUDE_CODE_CONVERSATION_RECOVERY env Recovery flow: 1\. Detect incomplete last message (tool result missing, thinking unfinished) 2\. Load transcript up to last complete pair 3\. Offer resume with context // Legacy Progress Bridge (sessionStorage.ts L114-130): // Old transcripts: { type:'progress', uuid, parentUuid } // isLegacyProgressEntry() detects them // Chain rebuilt during load, skipping legacy progress | ||
| // Ephemeral Progress Types (NEVER persisted) | ||
| EPHEMERAL_PROGRESS_TYPES = { | ||
| 'bash_progress', 'powershell_progress', | ||
| 'mcp_progress', 'sleep_progress' | ||
| } | ||
| → isEphemeralToolProgress() detects + skips persist | ||
| ``` | ||
| ### Subagent Metadata (sessionStorage.ts:235-310) | ||
| ## Session Resume Flow (`sessionRestore.ts:55-110`) | ||
| AgentMetadata = { agentType: string // 'explore', 'research' worktreePath?: string // isolation info description?: string // original task from AgentTool } writeAgentMetadata(agentId, metadata) → .meta.json sidecar readAgentMetadata(agentId) → load on resume RemoteAgentMetadata = { taskId: string remoteTaskType: string sessionId: string // CCR session ID title, command, spawnedAt isLongRunning?, isUltraplan?, isRemoteReview? remoteTaskMetadata?: { [key]: unknown } } | ||
| ``` | ||
| restoreSessionStateFromLog(): | ||
| I | ||
| 1. Load transcript (loadTranscriptFile): | ||
| - Read JSONL | ||
| - Filter: unresolved tool uses, orphaned thinking, | ||
| whitespace messages | ||
| - Return Message[] | ||
| ## Comparison | ||
| 2. Restore file history (L62): | ||
| fileHistoryRestoreStateFromLog() | ||
| → reconstruct FileHistory from log snapshots | ||
| Claude Code | ||
| 3. Restore attribution (L68, COMMIT_ATTRIBUTION): | ||
| attributionRestoreStateFromLog() | ||
| → git commit tracking | ||
| * **Full transcript recording** : messages + tool calls + token usage + permission decisions → JSON file | ||
| * **Session resume** : load transcript → rebuild state → continue conversation | ||
| * **Tool execution metadata** : duration, token cost, permission source per call | ||
| * **Session title** : LLM auto-generated, cached | ||
| * **Agent metadata** : parent session, agent type, invocation kind tracked | ||
| * **Context re-validation on resume** : system prompt may have changed → warn | ||
| 4. Restore context-collapse (L75, CONTEXT_COLLAPSE): | ||
| restoreFromEntries() | ||
| → rebuild collapsed view state | ||
| Mitsein | ||
| 5. Restore todos (L87): | ||
| extractTodosFromTranscript() | ||
| → scan for last TodoWrite tool_use | ||
| → parse todos from tool input | ||
| * **thread + message 表** : messages 已持久化 ✓ | ||
| * **Thread titles** : LLM 生成或 set_title tool 设置 ✓ | ||
| * **spawned_from_thread_id** : 追踪 spawn 来源 ✓ | ||
| * 缺少: tool execution metadata (duration, token, permission) | ||
| * 缺少: resume 时的 context re-validation | ||
| * 缺少: 进化机制所需的 analytics 数据 | ||
| 6. Validation: | ||
| checkResumeConsistency() | ||
| → sessionId match, no orphaned messages | ||
| Parent UUID chain validation → detect forks | ||
| ``` | ||
| II | ||
| ## Bootstrap State (`state.ts:30-120`) | ||
| ## How to Implement | ||
| ```typescript | ||
| State = { | ||
| originalCwd: string // stable project root | ||
| projectRoot: string // distinct from originalCwd | ||
| sessionId: SessionId // UUID, set once at start | ||
| cwd: string // current working directory | ||
| #### Step 1: Tool execution metadata table | ||
| // Cost tracking | ||
| totalCostUSD: number | ||
| totalAPIDuration: number | ||
| \-- New table: track every tool call CREATE TABLE tool_execution ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), thread_id TEXT REFERENCES thread(id), message_id TEXT REFERENCES message(id), agent_config_id TEXT REFERENCES agent_config(id), tool_name TEXT NOT NULL, input_summary TEXT, -- truncated for storage status TEXT NOT NULL, -- 'success' | 'error' | 'denied' | 'cancelled' duration_ms INTEGER, input_tokens INTEGER, output_tokens INTEGER, permission_decision TEXT, -- 'allow' | 'deny' | 'ask' permission_source TEXT, -- 'config' | 'hook' | 'user' | 'auto' error_message TEXT, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_tool_exec_agent ON tool_execution(agent_config_id); CREATE INDEX idx_tool_exec_thread ON tool_execution(thread_id); | ||
| // Model state | ||
| mainLoopModelOverride?: ModelSetting | ||
| modelUsage: { [modelName]: ModelUsage } | ||
| #### Step 2: Record metadata in orchestrator | ||
| // Permission/feature state | ||
| toolPermissionContext: ToolPermissionContext | ||
| isAutoModeAvailable: boolean | ||
| # orchestrator.py — after tool execution async def _record_tool_execution( self, thread_id, message_id, agent_config_id, tool, input, result, permission, duration_ms, usage ): await self.db.execute( """INSERT INTO tool_execution (thread_id, message_id, agent_config_id, tool_name, input_summary, status, duration_ms, input_tokens, output_tokens, permission_decision, permission_source, error_message) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)""", thread_id, message_id, agent_config_id, tool.name, truncate(str(input), 500), "success" if not result.error else "error", duration_ms, usage.input_tokens if usage else None, usage.output_tokens if usage else None, permission.behavior, permission.source, result.error if hasattr(result, 'error') else None, ) | ||
| // Settings | ||
| flagSettingsPath?: string | ||
| allowedSettingSources: SettingSource[] | ||
| sessionCounter: AttributedCounter | null | ||
| } | ||
| #### Step 3: Resume with context re-validation | ||
| // Key functions: | ||
| getSessionId() — immutable, set once | ||
| getOriginalCwd() — stable project identity | ||
| switchSession(newId) — for --resume flag | ||
| setMainLoopModelOverride(model) — mid-session model change | ||
| getLastApiCompletionTimestamp() — idle detection | ||
| getTotalInputTokens() — cumulative usage | ||
| addToTurnToolDuration() — per-turn metrics | ||
| ``` | ||
| async def resume_thread(thread_id: str, agent_config_id: str): thread = await thread_repo.get(thread_id) config = await config_resolver.resolve(agent_config_id, thread.user_id) messages = await message_repo.list(thread_id) # Re-validate context warnings = [] # Check if agent config changed since last message last_msg_time = messages[-1].created_at if messages else None if last_msg_time and config.updated_at > last_msg_time: warnings.append("Agent 配置在上次对话后有更新") # Check if tools are still available current_tools = {t.name for t in tool_assembler.assemble(config)} used_tools = {m.tool_name for m in messages if hasattr(m, 'tool_name')} missing = used_tools - current_tools if missing: warnings.append(f"以下 tools 已不可用: {missing}") # Re-attach relevant memories (may have changed) memories = await recall_relevant_memories(thread.user_id, thread.title) return ResumeResult( messages=messages, memories=memories, warnings=warnings, ) | ||
| ## Crash Recovery (`conversationRecovery.ts`) | ||
| #### Step 4: Analytics queries for evolution | ||
| ``` | ||
| ConversationRecoveryStrategy = 'full' | 'partial' | 'none' | ||
| // Controlled by: CLAUDE_CODE_CONVERSATION_RECOVERY env | ||
| # Used by memory-organizer daemon for evolution signals async def get_agent_analytics(agent_config_id: str, days: int = 30) -> dict: """Aggregate tool usage patterns for evolution detection.""" stats = await db.fetch(""" SELECT tool_name, COUNT(*) as call_count, COUNT(*) FILTER (WHERE status = 'denied') as deny_count, COUNT(*) FILTER (WHERE status = 'error') as error_count, AVG(duration_ms) as avg_duration_ms, SUM(input_tokens + output_tokens) as total_tokens FROM tool_execution WHERE agent_config_id = $1 AND created_at > now() - interval '%s days' GROUP BY tool_name ORDER BY deny_count DESC """, agent_config_id, days) return { "tool_stats": stats, "evolution_signals": [ {"tool": s["tool_name"], "signal": "frequently_denied", "suggestion": f"Remove {s['tool_name']} from config"} for s in stats if s["deny_count"] > 3 ] + [ {"tool": s["tool_name"], "signal": "high_error_rate", "suggestion": f"Investigate {s['tool_name']} reliability"} for s in stats if s["error_count"] / max(s["call_count"], 1) > 0.3 ], } | ||
| Recovery flow: | ||
| 1. Detect incomplete last message | ||
| (tool result missing, thinking unfinished) | ||
| 2. Load transcript up to last complete pair | ||
| 3. Offer resume with context | ||
| III | ||
| // Legacy Progress Bridge (sessionStorage.ts L114-130): | ||
| // Old transcripts: { type:'progress', uuid, parentUuid } | ||
| // isLegacyProgressEntry() detects them | ||
| // Chain rebuilt during load, skipping legacy progress | ||
| ``` | ||
| ## How to Verify | ||
| ## Subagent Metadata (`sessionStorage.ts:235-310`) | ||
| * **test_metadata_recorded** — tool 执行后 tool_execution 表有记录,含 duration + tokens | ||
| * **test_denied_recorded** — tool 被 deny,status='denied' + permission_source 正确 | ||
| * **test_resume_warnings** — config 更新后 resume → warnings 包含变更提示 | ||
| * **test_resume_missing_tools** — tool 被移除后 resume → warnings 列出 missing tools | ||
| * **test_resume_re_attaches_memory** — resume 后 memories 是最新的(非 thread 创建时的快照) | ||
| * **test_analytics_deny_signal** — 同一 tool deny 4 次 → evolution signal "frequently_denied" | ||
| * **test_analytics_error_rate** — error rate >30% → evolution signal "high_error_rate" | ||
| ```typescript | ||
| AgentMetadata = { | ||
| agentType: string // 'explore', 'research' | ||
| worktreePath?: string // isolation info | ||
| description?: string // original task from AgentTool | ||
| } | ||
| writeAgentMetadata(agentId, metadata) → .meta.json sidecar | ||
| readAgentMetadata(agentId) → load on resume | ||
| IV | ||
| ## User Perception | ||
| Before | ||
| 用户点开 PPT Agent → "周报0324" thread | ||
| [上次做到一半的 PPT thread] | ||
| PPT Agent: [继续对话,但不知道 PPT 模板已更新] | ||
| [生成的 PPT 用了旧模板 🤦] | ||
| After | ||
| 用户点开 PPT Agent → "周报0324" thread | ||
| PPT Agent: ⚠️ 自上次对话后,PPT 模板有更新。需要用新模板重新生成吗? | ||
| 用户: 是 | ||
| PPT Agent: 好的,用新模板重新生成... | ||
| **核心体感差异:**(1) Thread resume 时自动检测环境变化,避免基于过时 context 行动;(2) Tool 执行数据自动积累,为 Agent 进化提供数据基础;(3) 用户无需手动管理 — 所有 metadata 自动记录。 | ||
| thread tool_execution resume analytics evolution | ||
| RemoteAgentMetadata = { | ||
| taskId: string | ||
| remoteTaskType: string | ||
| sessionId: string // CCR session ID | ||
| title, command, spawnedAt | ||
| isLongRunning?, isUltraplan?, isRemoteReview? | ||
| remoteTaskMetadata?: { [key]: unknown } | ||
| } | ||
| ``` |
+163
-174
@@ -1,211 +0,200 @@ | ||
| [← Back to Index](index.html) | ||
| # 11. Agent Communication | ||
| # 11\. Agent Communication Critical | ||
| Sync blocking / async notification / backgrounding / resume — complete communication mechanism between main agent and sub-agents. | ||
| Sync blocking / Async notification / backgrounding / resume — 主 agent 与 sub-agent 的完整通信机制 | ||
| ## Architecture Overview | ||
| R | ||
| ``` | ||
| ┌─────────────────────────────────────────────────────────────┐ | ||
| │ Main Agent (query loop — query.ts) │ | ||
| │ │ | ||
| │ Each iteration: │ | ||
| │ 1. LLM response → parse tool_use blocks │ | ||
| │ 2. Execute tools (including AgentTool) │ | ||
| │ 3. Check commandQueue for pending notifications ──┐ │ | ||
| │ 4. Convert notifications → AttachmentMessage │ │ | ||
| │ 5. Inject into next LLM turn as user content │ │ | ||
| │ │ │ | ||
| │ ┌──────────────────┐ ┌─────────────────────┐ │ │ | ||
| │ │ Sync SubAgent │ │ Async SubAgent │ │ │ | ||
| │ │ (blocking) │ │ (fire & forget) │ │ │ | ||
| │ │ │ │ │ │ │ | ||
| │ │ runAgent() ──┐ │ │ runAgent() ──┐ │ │ │ | ||
| │ │ yield msgs │ │ │ yield msgs │ │ │ │ | ||
| │ │ yield msgs │ │ │ yield msgs │ │ │ │ | ||
| │ │ done ────────┘ │ │ done ────────┘ │ │ │ | ||
| │ │ │ │ │ │ │ | ||
| │ │ return result │ │ enqueueAgent ──────┼─────┘ │ | ||
| │ │ directly ──→ │ │ Notification() │ │ | ||
| │ └────────┬─────────┘ └─────────────────────┘ │ | ||
| │ │ │ | ||
| │ tool_result goes directly into messages │ | ||
| └─────────────────────────────────────────────────────────────┘ | ||
| ``` | ||
| ## Feature Reference [→ Full Flag Index](00-feature-discovery.html#flags-build) | ||
| ## Notification XML Format | ||
| ### Architecture Overview | ||
| ```xml | ||
| <task-notification> | ||
| <task-id>abc123</task-id> | ||
| <tool-use-id>tu_xyz</tool-use-id> | ||
| <output-file>/path/to/task-output.json</output-file> | ||
| <status>completed | failed | killed</status> | ||
| <summary>Agent "Generate PPT" completed</summary> | ||
| <result>PPT generated with 12 slides...</result> | ||
| <usage> | ||
| <total_tokens>8234</total_tokens> | ||
| <tool_uses>7</tool_uses> | ||
| <duration_ms>23400</duration_ms> | ||
| </usage> | ||
| <worktree> | ||
| <worktreePath>/repo/.claude/worktrees/agent-abc</worktreePath> | ||
| <worktreeBranch>agent-abc</worktreeBranch> | ||
| </worktree> | ||
| </task-notification> | ||
| ``` | ||
| ┌─────────────────────────────────────────────────────────────┐ │ Main Agent (query loop — query.ts) │ │ │ │ 每轮 iteration: │ │ 1. LLM response → parse tool_use blocks │ │ 2. Execute tools (including AgentTool) │ │ 3. Check commandQueue for pending notifications ──┐ │ │ 4. Convert notifications → AttachmentMessage │ │ │ 5. Inject into next LLM turn as user content │ │ │ │ │ │ ┌──────────────────┐ ┌─────────────────────┐ │ │ │ │ Sync SubAgent │ │ Async SubAgent │ │ │ │ │ (blocking) │ │ (fire & forget) │ │ │ │ │ │ │ │ │ │ │ │ runAgent() ──┐ │ │ runAgent() ──┐ │ │ │ │ │ yield msgs │ │ │ yield msgs │ │ │ │ │ │ yield msgs │ │ │ yield msgs │ │ │ │ │ │ done ────────┘ │ │ done ────────┘ │ │ │ │ │ │ │ │ │ │ │ │ return result │ │ enqueueAgent ──────┼─────┘ │ │ │ directly ──→ │ │ Notification() │ │ │ └────────┬─────────┘ └─────────────────────┘ │ │ │ │ │ tool_result 直接 │ │ 进入本轮 messages │ └─────────────────────────────────────────────────────────────┘ | ||
| ## Feature Flags & Env Vars | ||
| ### Notification XML Format | ||
| | Flag | Description | | ||
| |---|---| | ||
| | `FORK_SUBAGENT` | Cache-safe fork: children share prompt prefix | | ||
| | `COORDINATOR_MODE` | Workers only receive task-notification, coordinator synthesizes | | ||
| | `BG_SESSIONS` | Background session management | | ||
| | `tengu_auto_background_agents` (false) | Auto-background sync agent after 120s | | ||
| | `tengu_amber_stoat` (true) | Built-in agent availability | | ||
| | `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS` | Block run_in_background entirely | | ||
| <task-notification> <task-id>abc123</task-id> <tool-use-id>tu_xyz</tool-use-id> <output-file>/path/to/task-output.json</output-file> <status>completed | failed | killed</status> <summary>Agent "Generate PPT" completed</summary> <result>PPT generated with 12 slides...</result> <usage> <total_tokens>8234</total_tokens> <tool_uses>7</tool_uses> <duration_ms>23400</duration_ms> </usage> <worktree> <worktreePath>/repo/.claude/worktrees/agent-abc</worktreePath> <worktreeBranch>agent-abc</worktreeBranch> </worktree> </task-notification> | ||
| ## Key Files | ||
| ### Feature Flags & Env Vars | ||
| ``` | ||
| src/tools/AgentTool/AgentTool.tsx — sync path (765-1200), async spawn (686-752) | ||
| src/tools/AgentTool/agentToolUtils.ts — runAsyncAgentLifecycle (508-686) | ||
| src/tools/AgentTool/resumeAgent.ts — resume background agent (42-265) | ||
| src/tools/AgentTool/runAgent.ts — core agent execution (async generator) | ||
| src/tasks/LocalAgentTask/LocalAgentTask.tsx — enqueueAgentNotification (197-262) | ||
| registerAsyncAgent (466-515) | ||
| complete/fail/kill (281-456) | ||
| src/utils/messageQueueManager.ts — commandQueue singleton (142-149) | ||
| src/utils/attachments.ts — getQueuedCommandAttachments (1046-1083) | ||
| src/query.ts — queue drain + injection (1560-1643) | ||
| src/tools/SendMessageTool/SendMessageTool.ts — inter-agent messaging | ||
| ``` | ||
| `FORK_SUBAGENT`| Cache-safe fork: children share prompt prefix | ||
| ---|--- | ||
| `COORDINATOR_MODE`| Workers only receive task-notification, coordinator synthesizes | ||
| `BG_SESSIONS`| Background session management | ||
| `tengu_auto_background_agents` (false)| Auto-background sync agent after 120s | ||
| `tengu_amber_stoat` (true)| Built-in agent availability | ||
| `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS`| Block run_in_background entirely | ||
| ### Key Files | ||
| --- | ||
| src/tools/AgentTool/AgentTool.tsx — sync path (765-1200), async spawn (686-752) src/tools/AgentTool/agentToolUtils.ts — runAsyncAgentLifecycle (508-686) src/tools/AgentTool/resumeAgent.ts — resume background agent (42-265) src/tools/AgentTool/runAgent.ts — core agent execution (async generator) src/tasks/LocalAgentTask/LocalAgentTask.tsx — enqueueAgentNotification (197-262) registerAsyncAgent (466-515) complete/fail/kill (281-456) src/utils/messageQueueManager.ts — commandQueue singleton (142-149) src/utils/attachments.ts — getQueuedCommandAttachments (1046-1083) src/query.ts — queue drain + injection (1560-1643) src/tools/SendMessageTool/SendMessageTool.ts — inter-agent messaging | ||
| ## Notification Dedup Guard (`enqueueAgentNotification` L197-259) | ||
| ### Notification 双重防护 (enqueueAgentNotification L197-259) | ||
| ```javascript | ||
| enqueueAgentNotification({ | ||
| taskId, description, status, setAppState, | ||
| finalMessage, usage?, toolUseId?, | ||
| worktreePath?, worktreeBranch? | ||
| }): | ||
| enqueueAgentNotification({ taskId, description, status, setAppState, finalMessage, usage?, toolUseId?, worktreePath?, worktreeBranch? }): 1\. Atomic check — notified flag (prevent duplicates): updateTaskState(taskId, task => { if (task.notified) return task // 已通知过,skip shouldEnqueue = true return { ...task, notified: true } }) 2\. XML construction — 完整的 task-notification 格式 3\. Enqueue with 'later' priority: enqueuePendingNotification({ value: message, mode: 'task-notification' }) // 'later' = 优先级最低 → 用户输入永远先处理 | ||
| 1. Atomic check — notified flag (prevent duplicates): | ||
| updateTaskState(taskId, task => { | ||
| if (task.notified) return task // already notified, skip | ||
| shouldEnqueue = true | ||
| return { ...task, notified: true } | ||
| }) | ||
| ### Foreground → Background 自动转换 | ||
| 2. XML construction — complete task-notification format | ||
| registerAgentForeground({ agentId, description, prompt, selectedAgent, setAppState, autoBackgroundMs, toolUseId }): isBackgrounded: false (initially) // Creates promise for background transition: backgroundSignal = new Promise(resolve => { backgroundSignalResolvers.set(agentId, resolve) }) // Auto-background timer (tengu_auto_background_agents): if (autoBackgroundMs > 0): setTimeout(() => { task.isBackgrounded = true resolveBackgroundSignal() }, autoBackgroundMs) // default: 120s return { taskId, backgroundSignal, cancelAutoBackground } // 用例: // - 用户不操作 120s → agent 自动转到后台 // - 用户手动 background → cancelAutoBackground() + immediate resolve // - 前台 task 完成 → 正常结束,不触发 auto-background | ||
| 3. Enqueue with 'later' priority: | ||
| enqueuePendingNotification({ value: message, mode: 'task-notification' }) | ||
| // 'later' = lowest priority → user input always processed first | ||
| ``` | ||
| ### runAsyncAgentLifecycle — 8 步完整生命周期 | ||
| ## Foreground → Background Transition | ||
| // agentToolUtils.ts:508-650 runAsyncAgentLifecycle({ taskId, abortController, makeStream, metadata, description, toolUseContext, rootSetAppState, enableSummarization, getWorktreeResult }): Step 1: Stream messages from query() loop for await (const message of makeStream(onCacheSafeParams)): agentMessages.push(message) Step 2: Append to UI if task retained if (task.retain) → push to task.messages Step 3: Update progress tracker per message updateProgressFromMessage(tracker, message) updateAsyncAgentProgress(taskId, progress, rootSetAppState) Step 4: Emit SDK progress events emitTaskProgress(tracker, taskId, toolUseId, ...) Step 5: Finalize result agentResult = finalizeAgentTool(agentMessages, taskId, metadata) Step 6: Mark completed BEFORE classify/worktree completeAsyncAgent(agentResult, rootSetAppState) // ⚠️ status = completed 不等 network ops (don't gate on IO) Step 7: Enhance message with handoff warning finalMessage = handoffWarning + extracts... Step 8: Get worktree metadata + enqueue notification { worktreePath, worktreeBranch } = await getWorktreeResult() enqueueAgentNotification({ ...all fields... }) | ||
| ```javascript | ||
| registerAgentForeground({ | ||
| agentId, description, prompt, selectedAgent, | ||
| setAppState, autoBackgroundMs, toolUseId | ||
| }): | ||
| ### Queue Drain — 在 query loop 中注入通知 | ||
| isBackgrounded: false (initially) | ||
| // query.ts:1560-1643 // Print loop 和 agent loop 分别 drain queue: // Main thread: cmd = dequeue(cmd => cmd.agentId === undefined) // → 只处理没有 agentId 的 commands (全局通知) // Agent thread: cmd = dequeue(cmd => cmd.agentId === myAgentId) // → 只处理属于自己的 commands // 注入方式: // - Idle (model waiting): useQueueProcessor wakes loop // - Busy (model running): queued_command attachments // → getQueuedCommandAttachments() (attachments.ts:1046) | ||
| // Creates promise for background transition: | ||
| backgroundSignal = new Promise(resolve => { | ||
| backgroundSignalResolvers.set(agentId, resolve) | ||
| }) | ||
| I | ||
| // Auto-background timer (tengu_auto_background_agents): | ||
| if (autoBackgroundMs > 0): | ||
| setTimeout(() => { | ||
| task.isBackgrounded = true | ||
| resolveBackgroundSignal() | ||
| }, autoBackgroundMs) // default: 120s | ||
| ## Comparison | ||
| return { taskId, backgroundSignal, cancelAutoBackground } | ||
| ``` | ||
| Claude Code | ||
| ## `runAsyncAgentLifecycle` — 8-Step Lifecycle (`agentToolUtils.ts:508-650`) | ||
| * **Sync path** — AgentTool blocks parent, `runAgent()` async generator 逐条 yield 消息,parent 用 while loop 消费,完成后直接 return result | ||
| * **Async path** — `void runAsyncAgentLifecycle()` fire-and-forget,立即 return `{ status: 'async_launched' }`,完成后 `enqueueAgentNotification()` 注入 XML | ||
| * **commandQueue** — module-level singleton,notification 以 `priority: 'later'` 入队,下一轮 query iteration 消费 | ||
| * **Queue → Parent injection** — `query.ts:1560` 过滤 queue → `getAttachmentMessages()` 转为 AttachmentMessage → 注入 toolResults[] → 作为 **user content** 发给 LLM | ||
| * **Backgrounding** — sync 执行中 `Promise.race([nextMsg, backgroundSignal])`,UI 按钮或 120s auto timer 触发切换为 async | ||
| * **Resume** — `resumeAgentBackground()` 从磁盘加载 transcript(.jsonl) + 追加新消息 + 重跑 lifecycle | ||
| * **防重复** — `task.notified` 原子 flag,已通知就跳过 | ||
| * **3 种终态** — completed / failed / killed,各自 enqueue 不同 status 的 notification | ||
| ```javascript | ||
| runAsyncAgentLifecycle({ | ||
| taskId, abortController, makeStream, metadata, | ||
| description, toolUseContext, rootSetAppState, | ||
| enableSummarization, getWorktreeResult | ||
| }): | ||
| Mitsein | ||
| Step 1: Stream messages from query() loop | ||
| for await (const message of makeStream(onCacheSafeParams)): | ||
| agentMessages.push(message) | ||
| * **spawn.py** — foreground (同步 block) + background (asyncio.Task 无回调) | ||
| * Background 完成后**无通知机制** — 父 agent 不知道子 agent 完成了 | ||
| * 无 commandQueue 等价物 — 单进程 queue 不适用 server-side | ||
| * 无 notification injection — 父 agent 的下一轮 iteration 看不到子 agent 结果 | ||
| * 无 sync→async 动态切换 | ||
| * 无 resume — thread 虽然存 message 但没有 "加载 + 追加 + 重跑" 机制 | ||
| * kill_subagent 是 registry 级别操作,不产生 notification | ||
| * v4 设计中已定义 spawn_agent 回调需求(Phase 1),但实现路径未明确 | ||
| Step 2: Append to UI if task retained | ||
| if (task.retain) → push to task.messages | ||
| Mechanism | Claude Code | Mitsein | ||
| ---|---|--- | ||
| Sync spawn (blocking)| ✓ async generator| ✓ foreground | ||
| Async spawn (non-blocking)| ✓ void + detached| partial (no callback) | ||
| Completion notification| ✓ XML in queue| ✗ | ||
| Queue → parent injection| ✓ AttachmentMessage| ✗ | ||
| Sync → async transition| ✓ Promise.race| ✗ | ||
| Resume completed agent| ✓ load transcript + append| ✗ | ||
| Duplicate notification guard| ✓ atomic notified flag| ✗ | ||
| 3 terminal states| ✓ completed/failed/killed| partial (kill only) | ||
| II | ||
| Step 3: Update progress tracker per message | ||
| updateProgressFromMessage(tracker, message) | ||
| updateAsyncAgentProgress(taskId, progress, rootSetAppState) | ||
| ## How to Implement | ||
| Step 4: Emit SDK progress events | ||
| emitTaskProgress(tracker, taskId, toolUseId, ...) | ||
| #### Step 1: Notification message 写入 DB | ||
| Step 5: Finalize result | ||
| agentResult = finalizeAgentTool(agentMessages, taskId, metadata) | ||
| CC 用 module-level commandQueue(单进程),Mitsein 是 multi-tenant server,用 message 表替代。 | ||
| Step 6: Mark completed BEFORE classify/worktree | ||
| completeAsyncAgent(agentResult, rootSetAppState) | ||
| // ⚠️ status = completed does NOT wait on network ops | ||
| # CC 的 enqueueAgentNotification → Mitsein 等价物 async def notify_parent( parent_thread_id: str, task_id: str, status: str, # "completed" | "failed" | "killed" summary: str, result: str | None, usage: dict | None, tool_use_id: str | None = None, ): """Write notification directly to parent thread's message table.""" content = { "type": "task_notification", "task_id": task_id, "status": status, "summary": summary, "result": result, "usage": usage, "tool_use_id": tool_use_id, } await message_repo.create( thread_id=parent_thread_id, role="system", # CC injects as user content; content=content, # Mitsein 用 system role + structured JSON idempotency_key=f"notify:{task_id}", # 防重复 (替代 CC 的 notified flag) ) # CC: commandQueue → query loop drain → AttachmentMessage # Mitsein: message 表 → orchestrator 下一轮 SELECT → 直接在 messages 里 | ||
| Step 7: Enhance message with handoff warning | ||
| finalMessage = handoffWarning + extracts... | ||
| #### Step 2: Orchestrator 消费 notification | ||
| Step 8: Get worktree metadata + enqueue notification | ||
| { worktreePath, worktreeBranch } = await getWorktreeResult() | ||
| enqueueAgentNotification({ ...all fields... }) | ||
| ``` | ||
| CC 在 `query.ts:1560` 用 `getCommandsByMaxPriority` 过滤 queue。Mitsein 在 orchestrator 的 run loop 里直接查 message 表。 | ||
| ## Queue Drain — Injection in Query Loop (`query.ts:1560-1643`) | ||
| # orchestrator.py — run loop async def run(self, agent_config, thread_id, initial_message): while True: messages = await self._load_messages(thread_id) # CC equivalent of query.ts:1560-1578 queue drain # Mitsein: notifications 已经在 messages 里了(Step 1 写入的) # 无需额外 drain — message 表就是 queue response = await self.llm.query(messages=messages) # LLM 看到 task_notification message → 知道子 agent 完成了 # Process tool calls... for tool_call in response.tool_calls: if tool_call.name == "spawn_agent": await self._spawn_async(tool_call, thread_id) # ... async def _spawn_async(self, tool_call, parent_thread_id): config_id = tool_call.input["config_id"] context = tool_call.input["context"] task_id = str(uuid4()) # 1. Create child thread child_thread = await thread_repo.create( agent_config_id=config_id, spawned_from=parent_thread_id, ) # 2. Start async task (CC: void runAsyncAgentLifecycle) task = asyncio.create_task( self._run_child_and_notify( task_id, config_id, child_thread.id, parent_thread_id, context, ) ) self._task_refs[task_id] = task # 存引用防 GC! # 3. 立即返回 (CC: return { status: 'async_launched' }) return {"status": "async_launched", "task_id": task_id} async def _run_child_and_notify(self, task_id, config_id, thread_id, parent_thread_id, context): try: result = await self._run_agent(config_id, thread_id, context) await notify_parent( parent_thread_id, task_id, "completed", summary=result.summary, result=result.final_text, usage=result.usage, ) except asyncio.CancelledError: await notify_parent(parent_thread_id, task_id, "killed", "Agent was stopped", None, None) except Exception as e: await notify_parent(parent_thread_id, task_id, "failed", str(e), None, None) finally: self._task_refs.pop(task_id, None) | ||
| ```javascript | ||
| // Main thread: | ||
| cmd = dequeue(cmd => cmd.agentId === undefined) | ||
| // → only processes commands without agentId (global notifications) | ||
| #### Step 3: Sync → Async 动态切换 | ||
| // Agent thread: | ||
| cmd = dequeue(cmd => cmd.agentId === myAgentId) | ||
| // → only processes commands belonging to this agent | ||
| CC 用 `Promise.race`,Python 用 `asyncio.wait(FIRST_COMPLETED)`。 | ||
| // Injection method: | ||
| // - Idle (model waiting): useQueueProcessor wakes loop | ||
| // - Busy (model running): queued_command attachments | ||
| // → getQueuedCommandAttachments() (attachments.ts:1046) | ||
| ``` | ||
| # CC: Promise.race([nextMsg, backgroundSignal]) # Python equivalent: async def run_with_backgrounding(self, agent_iterator, background_event, parent_thread_id, task_id): """Run sync agent with option to switch to async mid-execution.""" messages = [] while True: next_msg = asyncio.create_task(agent_iterator.__anext__()) bg_wait = asyncio.create_task(background_event.wait()) done, pending = await asyncio.wait( [next_msg, bg_wait], return_when=asyncio.FIRST_COMPLETED, ) # Cancel the loser for p in pending: p.cancel() if bg_wait in done: # Backgrounded! Transition to async. # CC: void runWithAgentContext(...) — detach remaining iteration task = asyncio.create_task( self._continue_in_background( agent_iterator, messages, parent_thread_id, task_id, ) ) self._task_refs[task_id] = task return {"status": "async_launched", "task_id": task_id} if next_msg in done: try: msg = next_msg.result() messages.append(msg) except StopAsyncIteration: break # Agent done return {"status": "completed", "messages": messages} | ||
| ## Mechanism Summary | ||
| #### Step 4: Resume 已完成的 agent | ||
| CC 的 `resumeAgentBackground` 从磁盘加载 transcript + 追加新消息 + 重跑。Mitsein 用 DB message 表更简单。 | ||
| # CC: load .jsonl transcript → filter → append → re-run # Mitsein: SELECT messages → append → re-run async def resume_agent(self, thread_id: str, new_message: str, parent_thread_id: str): """Resume a previously completed agent with a new message.""" task_id = str(uuid4()) # 1. Load existing messages (CC: getAgentTranscript) existing = await message_repo.list(thread_id) # 2. Append new user message (CC: createUserMessage) await message_repo.create( thread_id=thread_id, role="user", content=new_message, ) # 3. Get agent config from thread thread = await thread_repo.get(thread_id) config = await config_resolver.resolve(thread.agent_config_id) # 4. Re-run as async (CC: void runAsyncAgentLifecycle) task = asyncio.create_task( self._run_child_and_notify( task_id, config.id, thread_id, parent_thread_id, new_message, ) ) self._task_refs[task_id] = task return {"status": "async_launched", "task_id": task_id} # CC 的 "状态 = transcript 文件" 设计: # agent 没有持久化的运行时状态 # Resume = 加载消息历史 + 追加新消息 + 重新跑 # Mitsein 等价: # 状态 = message 表中的 thread messages # Resume = SELECT + INSERT + re-run orchestrator | ||
| #### Step 5: 防重复通知 (idempotency) | ||
| CC 用 atomic `task.notified` flag。Mitsein 用 DB unique constraint。 | ||
| # CC pattern (single process, in-memory): # updateTaskState(taskId, task => { # if (task.notified) return task; // 已通知,跳过 # shouldEnqueue = true; # return { ...task, notified: true }; # }) # Mitsein pattern (multi-tenant server, DB): await message_repo.create( thread_id=parent_thread_id, role="system", content=content, idempotency_key=f"notify:{task_id}", # UNIQUE constraint ) # INSERT ... ON CONFLICT (idempotency_key) DO NOTHING # 重复调用静默忽略,不会产生两条相同 notification | ||
| III | ||
| ## How to Verify | ||
| ### Unit Tests | ||
| * **test_sync_agent_returns_directly** — sync spawn 返回 {status: "completed", messages: [...]},不写 notification message | ||
| * **test_async_agent_writes_notification** — async spawn 完成后,parent thread 出现 task_notification message | ||
| * **test_async_returns_immediately** — async spawn 返回 {status: "async_launched"} 耗时 <100ms | ||
| * **test_notification_idempotency** — 同一 task_id 调两次 notify_parent,只产生一条 message | ||
| * **test_failed_notification** — agent 抛异常,notification 的 status="failed" + error message | ||
| * **test_killed_notification** — cancel task,notification 的 status="killed" | ||
| * **test_backgrounding_transition** — sync 执行中 set background_event → 返回 async_launched + 后台继续 | ||
| * **test_resume_appends_message** — resume 后 thread messages = 原有 + 新 user message + agent response | ||
| * **test_resume_notification** — resume 完成后 parent thread 收到新的 notification | ||
| * **test_task_ref_stored** — create_task 后 task_refs 有引用,防止 GC | ||
| ### Integration Tests | ||
| * **test_e2e_async_spawn_and_notify** — 周报 Agent spawn PPT Agent → PPT 完成 → notification 出现在周报 thread → 周报 Agent 的下一轮 LLM call 看到 notification → 继续后续步骤 | ||
| * **test_e2e_sync_to_async** — PPT Agent 正在 sync 生成 → 用户点 "background" → 立即返回 → PPT 在后台继续 → 完成后 notification | ||
| * **test_e2e_resume_and_modify** — PPT 完成 → 用户说 "改第三页" → resume → PPT Agent 看到完整历史 + 新指令 → 修改 → 新 notification | ||
| * **test_concurrent_spawns_all_notify** — 同时 spawn 3 个 capability agents → 3 个 notification 最终全部到达 parent thread | ||
| ### Smoke Test: 完整通信链路 | ||
| 1\. Companion 收到 "帮我做周报" 2\. Companion LLM → tool_use: spawn_agent("weekly_report", "做本周周报") → orchestrator._spawn_async() → asyncio.create_task() → return {"status": "async_launched", "task_id": "wr-001"} 3\. 周报 Agent 在后台运行 → 收集信息 → 整理内容 → tool_use: spawn_agent("ppt", "做周报PPT, 内容:...") → 又一个 asyncio.create_task() (嵌套 spawn) → return {"status": "async_launched", "task_id": "ppt-001"} 4\. PPT Agent 完成 → notify_parent(parent=周报thread, task_id="ppt-001", status="completed") → 周报 thread 出现 task_notification message 5\. 周报 Agent 下一轮 iteration 看到 PPT notification → tool_use: email_send → 发送周报 → notify_parent(parent=Companion thread, task_id="wr-001", status="completed") 6\. Companion 下一轮看到周报 notification → "周报已完成 ✓ PPT已生成 → [点击查看]" 7\. 用户: "PPT 第三页改一下" → resume_agent(ppt_thread_id, "第三页改一下", companion_thread_id) → PPT Agent 加载历史 + 新指令 → 修改 → notify | ||
| IV | ||
| ## User Perception | ||
| Before (Current) | ||
| 用户: 帮我做周报 | ||
| 周报 Agent: 好的,整理信息中... | ||
| 周报 Agent: 现在生成 PPT... | ||
| [sync block 30 秒] | ||
| 周报 Agent: PPT 生成完毕,发送邮件... | ||
| [整个过程 60 秒串行] | ||
| [中途想和 Companion 聊别的?不行,blocked] | ||
| 用户: PPT 第三页改一下 | ||
| [找不到之前的 PPT Agent session] | ||
| [只能重新生成整个 PPT] | ||
| After (With Communication) | ||
| 用户: 帮我做周报 | ||
| Companion: 好的,启动周报 Agent... | ||
| [async spawn → 不 block] | ||
| 用户: 对了帮我查一下明天天气 | ||
| Companion: 明天晴,24°C | ||
| [周报 Agent 在后台: 整理 → spawn PPT → 等 notification] | ||
| [PPT 完成 → notification → 周报继续 → 发邮件 → notification] | ||
| Companion: ✓ 周报已完成并发送 → 查看 PPT | ||
| 用户: PPT 第三页改一下 | ||
| [resume PPT Agent: 加载完整历史 + "改第三页"] | ||
| PPT Agent: 已修改第三页 ✓ | ||
| [增量修改,不重新生成] | ||
| **核心体感差异:** | ||
| * **Non-blocking** — Companion 不被任何子 agent block,可以继续聊天 | ||
| * **Nested spawn** — 周报 Agent 可以 spawn PPT Agent,PPT 完成后自动通知周报继续 | ||
| * **Resume** — 用户点击已完成的 PPT Agent,可以直接继续修改,不丢失上下文 | ||
| * **Transparent progress** — 每步完成都有 notification,用户知道进度 | ||
| enqueueAgentNotification commandQueue sync/async backgrounding resume Phase 1 | ||
| | Mechanism | Claude Code | | ||
| |---|---| | ||
| | Sync spawn (blocking) | async generator | | ||
| | Async spawn (non-blocking) | `void` + detached | | ||
| | Completion notification | XML in commandQueue | | ||
| | Queue → parent injection | `AttachmentMessage` | | ||
| | Sync → async transition | `Promise.race` | | ||
| | Resume completed agent | load transcript + append | | ||
| | Duplicate notification guard | atomic `notified` flag | | ||
| | Terminal states | `completed` / `failed` / `killed` | |
+371
-100
@@ -1,137 +0,408 @@ | ||
| [← Back to Index](index.html) | ||
| # 12. Memory Management Data Flow | ||
| # 12\. Memory 管理全图 Critical | ||
| Write path (3 channels) + Read path (static + dynamic) + full lifecycle. | ||
| Write Path (3 通道) + Read Path (静态 + 动态) + Lifecycle 完整数据流 | ||
| ## Architecture Overview | ||
| R | ||
| ``` | ||
| ┌─────────────────────── WRITE PATH ───────────────────────┐ | ||
| │ │ | ||
| │ Channel 1: Main Agent direct write │ | ||
| │ User: "remember I prefer minimal style" │ | ||
| │ → Agent calls Write/Edit (unrestricted for memdir) │ | ||
| │ → extraction agent SKIPS this range (mutual exclusion)│ | ||
| │ │ | ||
| │ Channel 2: Extraction Agent auto-extract │ | ||
| │ Query loop end → stopHooks │ | ||
| │ → cursor tracking (new messages only) │ | ||
| │ → forked agent (restricted tools, max 5 turns) │ | ||
| │ → writes .md files + updates MEMORY.md │ | ||
| │ │ | ||
| │ Channel 3: Auto-Dream background consolidation │ | ||
| │ ≥24h + ≥5 sessions → consolidation lock │ | ||
| │ → forked agent reads transcripts │ | ||
| │ → orient → gather → consolidate → prune │ | ||
| │ → rewrites/merges memory files │ | ||
| │ │ | ||
| └──────────────────────────────────────────────────────────┘ | ||
| ## Feature Reference [→ Full GrowthBook Index](00-feature-discovery.html#flags-gb) | ||
| ┌─────────────────────── READ PATH ────────────────────────┐ | ||
| │ │ | ||
| │ Static: System Prompt (session-level) │ | ||
| │ loadMemoryPrompt() → read MEMORY.md (sync) │ | ||
| │ → truncate 200 lines / 25KB │ | ||
| │ → inject into system prompt, cached by CWD │ | ||
| │ │ | ||
| │ Dynamic: Per-Turn Attachment (turn-level) │ | ||
| │ startRelevantMemoryPrefetch() (parallel with model) │ | ||
| │ → scanMemoryFiles (200 files, frontmatter only) │ | ||
| │ → selectRelevantMemories (Sonnet, top 5) │ | ||
| │ → readMemoriesForSurfacing (4KB each, 60KB session) │ | ||
| │ → inject as <system-reminder> attachment │ | ||
| │ │ | ||
| └──────────────────────────────────────────────────────────┘ | ||
| ### Architecture Overview | ||
| ┌─────────────────────── STORAGE ──────────────────────────┐ | ||
| │ │ | ||
| │ ~/.claude/projects/<sanitized-git-root>/memory/ │ | ||
| │ ├── MEMORY.md ← index (200 lines cap) │ | ||
| │ ├── user_role.md ← topic file (frontmatter) │ | ||
| │ ├── feedback_testing.md │ | ||
| │ ├── project_deadline.md │ | ||
| │ └── logs/YYYY/MM/DD.md ← Kairos daily logs │ | ||
| │ │ | ||
| └──────────────────────────────────────────────────────────┘ | ||
| ``` | ||
| ┌─────────────────────── WRITE PATH ───────────────────────┐ │ │ │ 通道 1: Main Agent 直接写 │ │ User: "记住我喜欢简洁风格" │ │ → Agent calls Write/Edit (unrestricted for memdir) │ │ → extraction agent SKIPS this range (mutual exclusion)│ │ │ │ 通道 2: Extraction Agent 自动提取 │ │ Query loop end → stopHooks │ │ → cursor tracking (只处理新消息) │ │ → forked agent (restricted tools, max 5 turns) │ │ → writes .md files + updates MEMORY.md │ │ │ │ 通道 3: Auto-Dream 后台整理 │ │ ≥24h + ≥5 sessions → consolidation lock │ │ → forked agent reads transcripts │ │ → orient → gather → consolidate → prune │ │ → rewrites/merges memory files │ │ │ └──────────────────────────────────────────────────────────┘ ┌─────────────────────── READ PATH ────────────────────────┐ │ │ │ 静态: System Prompt (session 级) │ │ loadMemoryPrompt() → read MEMORY.md (sync) │ │ → truncate 200 lines / 25KB │ │ → inject into system prompt, cached by CWD │ │ │ │ 动态: Per-Turn Attachment (turn 级) │ │ startRelevantMemoryPrefetch() (parallel with model) │ │ → scanMemoryFiles (200 files, frontmatter only) │ │ → selectRelevantMemories (Sonnet, top 5) │ │ → readMemoriesForSurfacing (4KB each, 60KB session) │ │ → inject as <system-reminder> attachment │ │ │ └──────────────────────────────────────────────────────────┘ ┌─────────────────────── STORAGE ──────────────────────────┐ │ │ │ ~/.claude/projects/<sanitized-git-root>/memory/ │ │ ├── MEMORY.md ← index (200 lines cap) │ │ ├── user_role.md ← topic file (frontmatter) │ │ ├── feedback_testing.md │ │ ├── project_deadline.md │ │ └── logs/YYYY/MM/DD.md ← Kairos daily logs │ │ │ └──────────────────────────────────────────────────────────┘ | ||
| ## Constants & Thresholds | ||
| ### Constants & Thresholds | ||
| ```javascript | ||
| // MEMORY.md (index) | ||
| MAX_ENTRYPOINT_LINES = 200 // line truncation first | ||
| MAX_ENTRYPOINT_BYTES = 25_000 // then byte truncation | ||
| // MEMORY.md (index) MAX_ENTRYPOINT_LINES = 200 // line truncation first MAX_ENTRYPOINT_BYTES = 25_000 // then byte truncation // Memory scan MAX_MEMORY_FILES = 200 // max files scanned (newest first) FRONTMATTER_MAX_LINES = 30 // lines read for frontmatter parse // Relevant memories (per-turn selection) max_results = 5 // Sonnet selects top 5 max_tokens = 256 // Sonnet output budget MAX_MEMORY_BYTES = 4_096 // per-file truncation MAX_SESSION_BYTES = 60_000 // session-level injection cap // Extraction agent maxTurns = 5 // hard cap per extraction run // Auto-Dream minHours = 24 // hours since last consolidation minSessions = 5 // sessions touched since last SESSION_SCAN_INTERVAL = 600_000 // 10 min scan throttle (ms) // Extraction throttle tengu_bramble_lintel = 1 // run every N eligible turns (default) | ||
| // Memory scan | ||
| MAX_MEMORY_FILES = 200 // max files scanned (newest first) | ||
| FRONTMATTER_MAX_LINES = 30 // lines read for frontmatter parse | ||
| ### Feature Flags & GrowthBook | ||
| // Relevant memories (per-turn selection) | ||
| max_results = 5 // Sonnet selects top 5 | ||
| max_tokens = 256 // Sonnet output budget | ||
| MAX_MEMORY_BYTES = 4_096 // per-file truncation | ||
| MAX_SESSION_BYTES = 60_000 // session-level injection cap | ||
| `EXTRACT_MEMORIES`| Gates extraction at query loop end | ||
| ---|--- | ||
| `TEAMMEM`| Team memory (shared dirs + combined prompt) | ||
| `KAIROS_DREAM`| Daily-log prompt (replaces MEMORY.md index) | ||
| `AWAY_SUMMARY`| 5-min blur → recap summary | ||
| `tengu_onyx_plover` (null)| Auto-Dream config: {minHours:24, minSessions:5} | ||
| `tengu_bramble_lintel` (null→1)| Extraction throttle: every N eligible turns | ||
| `tengu_passport_quail` (false)| Master gate for extraction execution | ||
| `tengu_coral_fern` (false)| "Searching past context" grep instructions | ||
| `tengu_session_memory` (false)| Session memory (conversation notes between turns) | ||
| `tengu_moth_copse` (false)| Skip MEMORY.md index, topic files only | ||
| ### Key Files | ||
| // Extraction agent | ||
| maxTurns = 5 // hard cap per extraction run | ||
| // WRITE PATH src/services/extractMemories/extractMemories.ts — extraction lifecycle (296-587) src/services/extractMemories/prompts.ts — extraction prompt (50-154) src/services/autoDream/autoDream.ts — consolidation (122-319) src/services/autoDream/consolidationLock.ts — file-based lock src/services/autoDream/consolidationPrompt.ts — 4-phase prompt src/services/autoDream/config.ts — gates + GB defaults // READ PATH src/memdir/memdir.ts — loadMemoryPrompt (419), buildMemoryLines (199) src/memdir/findRelevantMemories.ts — Sonnet selection (39-75) src/memdir/memoryScan.ts — file scanning (35-77) src/memdir/memoryAge.ts — staleness (6-53) src/memdir/paths.ts — dir resolution + security (223-235) src/memdir/memoryTypes.ts — 4 types: user/feedback/project/reference src/utils/attachments.ts — injection as <system-reminder> (2196-2332) src/constants/prompts.ts — systemPromptSection('memory') (495) // LIFECYCLE src/query/stopHooks.ts — extraction + dream trigger (141-155) src/context.ts — getUserContext() → MEMORY.md | ||
| // Auto-Dream | ||
| minHours = 24 // hours since last consolidation | ||
| minSessions = 5 // sessions touched since last | ||
| SESSION_SCAN_INTERVAL = 600_000 // 10 min scan throttle (ms) | ||
| ### Write 通道互斥机制 | ||
| // Extraction throttle | ||
| tengu_bramble_lintel = 1 // run every N eligible turns (default) | ||
| ``` | ||
| ═══ 3 条写入通道 ═══ 通道 1: Main Agent 直接写 (FileWrite/FileEdit) → 用户说 "记住这个" → Agent 直接写 memory → isAutoMemPath() check, 无特殊限制 通道 2: Extraction Agent 自动提取 (post-sampling hook) → Turn ends + no more tool calls → handleStopHooks → runForkedAgent() → cache-safe fork → 工具限制: Read/Grep/Glob + memory-only Write/Edit 通道 3: Auto-Dream 后台合并 (background process) → 24h + 5 sessions gate → tryAcquireConsolidationLock() → 4-Phase consolidation prompt ═══ 互斥规则 ═══ hasMemoryWritesSince(cursor): 扫描 assistant messages 中 Write/Edit tool_use if target path is auto-memory dir → SKIP extraction (主 agent 已写, 不重复提取) 同一时刻: \- Main Agent 写 → Extraction 跳过 ✓ \- Extraction 写 → Main Agent 可继续写 ✓ (不冲突) \- Dream + Extraction → consolidationLock 互斥 \- Dream + Dream → file-based lock 互斥 | ||
| ## Feature Flags & GrowthBook | ||
| ### Read Path — Static + Dynamic 两层注入 | ||
| | Flag | Description | | ||
| |---|---| | ||
| | `EXTRACT_MEMORIES` | Gates extraction at query loop end | | ||
| | `TEAMMEM` | Team memory (shared dirs + combined prompt) | | ||
| | `KAIROS_DREAM` | Daily-log prompt (replaces MEMORY.md index) | | ||
| | `AWAY_SUMMARY` | 5-min blur → recap summary | | ||
| | `tengu_onyx_plover` (null) | Auto-Dream config: `{minHours:24, minSessions:5}` | | ||
| | `tengu_bramble_lintel` (null→1) | Extraction throttle: every N eligible turns | | ||
| | `tengu_passport_quail` (false) | Master gate for extraction execution | | ||
| | `tengu_coral_fern` (false) | "Searching past context" grep instructions | | ||
| | `tengu_session_memory` (false) | Session memory (conversation notes between turns) | | ||
| | `tengu_moth_copse` (false) | Skip MEMORY.md index, topic files only | | ||
| ═══ Static Layer (每次 API call 都带) ═══ MEMORY.md → truncateEntrypointContent(): line cap: 200 lines → byte cap: 25KB 截断在最后一个换行符边界 注入位置: system prompt section ('memory') ═══ Dynamic Layer (per-turn, on-demand) ═══ findRelevantMemories(): 1\. scanMemoryFiles() → MAX_MEMORY_FILES=200, newest first 2\. sideQuery(Sonnet): \- System: SELECT_MEMORIES_SYSTEM_PROMPT \- Input: manifest + conversation context \- Output: top 5 file paths 3\. Dedupe: alreadySurfaced set (avoid re-inject) 4\. Per-file: MAX_MEMORY_BYTES = 4,096 5\. Session cap: MAX_SESSION_BYTES = 60,000 6\. Telemetry: logMemoryRecallShape() ═══ 注入方式 ═══ MEMORY.md → system prompt (always loaded) Topic files → user context messages (per-turn) Staleness → memoryFreshnessNote() if age > 1 day → 注入为 <system-reminder> attachment (attachments.ts:2196) | ||
| ## Key Files | ||
| ### Extraction Prompt 结构 (prompts.ts:80-154) | ||
| ``` | ||
| // WRITE PATH | ||
| src/services/extractMemories/extractMemories.ts — extraction lifecycle (296-587) | ||
| src/services/extractMemories/prompts.ts — extraction prompt (50-154) | ||
| src/services/autoDream/autoDream.ts — consolidation (122-319) | ||
| src/services/autoDream/consolidationLock.ts — file-based lock | ||
| src/services/autoDream/consolidationPrompt.ts — 4-phase prompt | ||
| src/services/autoDream/config.ts — gates + GB defaults | ||
| buildExtractAutoOnlyPrompt(): 1\. 列出 existing memories (避免重复) 2\. 4-type taxonomy: user/feedback/project/reference 3\. 强调 "limited turn budget" (maxTurns=5) 4\. 策略: "All reads in turn 1, all writes in turn 2" buildExtractCombinedPrompt(): 同上 + team scope 支持 → team memory: shared conventions, project facts // Extraction agent 状态隔离: initExtractMemories() — closure-scoped → 每个实例独立的 cursor tracking → Test-friendly (不依赖全局状态) | ||
| // READ PATH | ||
| src/memdir/memdir.ts — loadMemoryPrompt (419), buildMemoryLines (199) | ||
| src/memdir/findRelevantMemories.ts — Sonnet selection (39-75) | ||
| src/memdir/memoryScan.ts — file scanning (35-77) | ||
| src/memdir/memoryAge.ts — staleness (6-53) | ||
| src/memdir/paths.ts — dir resolution + security (223-235) | ||
| src/memdir/memoryTypes.ts — 4 types: user/feedback/project/reference | ||
| src/utils/attachments.ts — injection as <system-reminder> (2196-2332) | ||
| src/constants/prompts.ts — systemPromptSection('memory') (495) | ||
| ### Auto-Dream Gate Chain (cheapest → most expensive) | ||
| // LIFECYCLE | ||
| src/query/stopHooks.ts — extraction + dream trigger (141-155) | ||
| src/context.ts — getUserContext() → MEMORY.md | ||
| ``` | ||
| Gate 1: Time (几乎 0 成本) readLastConsolidatedAt() → hoursSince > 24? Feature: tengu_onyx_plover Gate 2: Session Count (file stat) listSessionsTouchedSince() → count > 5? Scan throttled: SESSION_SCAN_INTERVAL = 10min Gate 3: Lock (file lock) tryAcquireConsolidationLock() → File-based mutex, 同时只有一个 dream ═══ 全部通过 → 执行 4 Phase ═══ Phase 1 Orient: ls + read MEMORY.md + skim topics Phase 2 Gather: read logs/, grep transcripts Phase 3 Consolidate: merge, convert dates, delete contradictions Phase 4 Prune: MEMORY.md < 200 lines, one-line entries | ||
| --- | ||
| I | ||
| ## Write Channel 1: Main Agent Direct Write | ||
| ## Write Path: 3 通道详解 | ||
| ``` | ||
| User: "remember I prefer minimal style" | ||
| │ | ||
| ├─ System prompt contains memory save instructions | ||
| │ (buildMemoryLines → "save immediately if user asks") | ||
| │ | ||
| ├─ Agent calls Write("~/.claude/.../memory/feedback_style.md", content) | ||
| │ → isAutoMemPath() check → ALLOW (unrestricted for main agent) | ||
| │ → File written with frontmatter: | ||
| │ --- | ||
| │ name: feedback_style | ||
| │ description: user prefers minimal style | ||
| │ type: feedback | ||
| │ --- | ||
| │ User preference: minimal style, concise, direct | ||
| │ | ||
| ├─ Agent calls Edit("~/.claude/.../memory/MEMORY.md", ...) | ||
| │ → Add pointer: "- [Style](feedback_style.md) — minimal style preference" | ||
| │ | ||
| └─ Next extraction agent checks hasMemoryWritesSince() | ||
| → main agent already wrote → SKIP extraction for this range | ||
| ``` | ||
| ### 通道 1: Main Agent 直接写 | ||
| ## Write Channel 2: Extraction Agent Auto-Extract | ||
| User: "记住我喜欢简洁风格" │ ├─ System prompt 包含 memory save instructions │ (buildMemoryLines → "save immediately if user asks") │ ├─ Agent calls Write("~/.claude/.../memory/feedback_style.md", content) │ → isAutoMemPath() check → ALLOW (unrestricted for main agent) │ → File written with frontmatter: │ --- │ name: feedback_style │ description: 用户偏好简洁风格 │ type: feedback │ --- │ 用户偏好: 简洁风格, 少废话, 直接给结论 │ ├─ Agent calls Edit("~/.claude/.../memory/MEMORY.md", ...) │ → Add pointer: "- [Style](feedback_style.md) — 简洁风格偏好" │ └─ 下一轮 extraction agent 检查 hasMemoryWritesSince() → 发现 main agent 已经写了 → SKIP extraction for this range | ||
| ``` | ||
| Query loop end → handleStopHooks() | ||
| │ | ||
| ├─ Guard checks: | ||
| │ feature('EXTRACT_MEMORIES')? → yes | ||
| │ toolUseContext.agentId? → no (main thread only) | ||
| │ isExtractModeActive()? → yes | ||
| │ | ||
| ├─ executeExtractMemories(context, appendSystemMessage) | ||
| │ │ | ||
| │ ├─ Throttle: turnsSinceLastExtraction < tengu_bramble_lintel? | ||
| │ │ → if yes: skip, increment counter, return | ||
| │ │ → if no: reset counter, proceed | ||
| │ │ | ||
| │ ├─ Mutual exclusion: hasMemoryWritesSince(messages, cursor)? | ||
| │ │ → if yes: skip, advance cursor, return | ||
| │ │ | ||
| │ ├─ Scan existing memories: | ||
| │ │ scanMemoryFiles(memoryDir) → 200 files max, frontmatter only | ||
| │ │ formatMemoryManifest(memories) → "[type] file (date): desc" | ||
| │ │ | ||
| │ ├─ Build extraction prompt: | ||
| │ │ buildExtractAutoOnlyPrompt(newMessageCount, manifest, skipIndex) | ||
| │ │ → "Analyze last N messages..." | ||
| │ │ → "Write frontmatter files, update MEMORY.md" | ||
| │ │ → "Do NOT interleave reads/writes across turns" | ||
| │ │ | ||
| │ ├─ Run forked agent: | ||
| │ │ runForkedAgent({ | ||
| │ │ promptMessages: [extraction prompt] | ||
| │ │ cacheSafeParams: from parent (prompt cache sharing!) | ||
| │ │ canUseTool: createAutoMemCanUseTool(memoryDir) | ||
| │ │ querySource: 'extract_memories' | ||
| │ │ skipTranscript: true // don't pollute main transcript | ||
| │ │ maxTurns: 5 // hard cap | ||
| │ │ }) | ||
| │ │ | ||
| │ │ Forked agent RESTRICTED tool access: | ||
| │ │ ✓ Read, Grep, Glob — unrestricted | ||
| │ │ ✓ Bash — read-only only (ls/cat/stat) | ||
| │ │ ✓ Edit, Write — ONLY inside memory dir | ||
| │ │ ✗ MCP, Agent, write-Bash — DENIED | ||
| │ │ | ||
| │ ├─ On success: | ||
| │ │ Advance cursor: lastMemoryMessageUuid = latest message | ||
| │ │ appendSystemMessage("memories saved: [paths]") | ||
| │ │ | ||
| │ └─ On failure: | ||
| │ cursor NOT advanced → retry on next eligible turn | ||
| ``` | ||
| ### 通道 2: Extraction Agent 自动提取 | ||
| ## Write Channel 3: Auto-Dream Background Consolidation | ||
| Query loop end → handleStopHooks() │ ├─ Guard checks: │ feature('EXTRACT_MEMORIES')? → yes │ toolUseContext.agentId? → no (main thread only) │ isExtractModeActive()? → yes │ ├─ executeExtractMemories(context, appendSystemMessage) │ │ │ ├─ Throttle: turnsSinceLastExtraction < tengu_bramble_lintel? │ │ → if yes: skip, increment counter, return │ │ → if no: reset counter, proceed │ │ │ ├─ Mutual exclusion: hasMemoryWritesSince(messages, cursor)? │ │ → if yes: skip, advance cursor, return │ │ │ ├─ Scan existing memories: │ │ scanMemoryFiles(memoryDir) → 200 files max, frontmatter only │ │ formatMemoryManifest(memories) → "[type] file (date): desc" │ │ │ ├─ Build extraction prompt: │ │ buildExtractAutoOnlyPrompt(newMessageCount, manifest, skipIndex) │ │ → "Analyze last N messages..." │ │ → "Write frontmatter files, update MEMORY.md" │ │ → "Do NOT interleave reads/writes across turns" │ │ │ ├─ Run forked agent: │ │ runForkedAgent({ │ │ promptMessages: [extraction prompt] │ │ cacheSafeParams: from parent (prompt cache sharing!) │ │ canUseTool: createAutoMemCanUseTool(memoryDir) │ │ querySource: 'extract_memories' │ │ skipTranscript: true // don't pollute main transcript │ │ maxTurns: 5 // hard cap │ │ }) │ │ │ │ Forked agent has RESTRICTED tool access: │ │ ✓ Read, Grep, Glob — unrestricted │ │ ✓ Bash — read-only only (ls/cat/stat) │ │ ✓ Edit, Write — ONLY inside memory dir │ │ ✗ MCP, Agent, write-Bash — DENIED │ │ │ ├─ On success: │ │ Advance cursor: lastMemoryMessageUuid = latest message │ │ appendSystemMessage("memories saved: [paths]") │ │ │ └─ On failure: │ cursor NOT advanced → retry on next eligible turn | ||
| ``` | ||
| Query loop end → handleStopHooks() → executeAutoDream() | ||
| │ | ||
| ├─ Gate 1 (cheapest first): isAutoDreamEnabled()? | ||
| │ → settings.json + tengu_onyx_plover check | ||
| │ | ||
| ├─ Gate 2: Time gate | ||
| │ (Date.now() - lastConsolidatedAt) / 3_600_000 ≥ minHours (24)? | ||
| │ → if no: return (most common exit) | ||
| │ | ||
| ├─ Gate 3: Scan throttle | ||
| │ Last scan < SESSION_SCAN_INTERVAL (10 min)? | ||
| │ → if yes: return (prevents repeated scans) | ||
| │ | ||
| ├─ Gate 4: Session gate | ||
| │ listSessionsTouchedSince(lastAt).length ≥ minSessions (5)? | ||
| │ → Count distinct sessions with mtime > lastConsolidatedAt | ||
| │ → Exclude current session | ||
| │ → if no: return | ||
| │ | ||
| ├─ Gate 5: Lock | ||
| │ tryAcquireConsolidationLock() | ||
| │ → File lock: mtime-based + PID body | ||
| │ → Stale after 60 minutes | ||
| │ → if locked: return | ||
| │ | ||
| ├─ Run forked agent with consolidation prompt: | ||
| │ Phase 1 Orient: read MEMORY.md, understand structure | ||
| │ Phase 2 Gather: read recent session transcripts | ||
| │ Phase 3 Consolidate: merge/update topic files | ||
| │ Phase 4 Prune: remove outdated, merge duplicates | ||
| │ | ||
| │ Progress tracking: | ||
| │ DreamTask state: starting → updating → completed/failed/killed | ||
| │ UI: "dreaming" status pill | ||
| │ Watches for Edit/Write calls → filesTouched list | ||
| │ | ||
| ├─ On success: | ||
| │ Lock mtime updated → next eligible in 24h | ||
| │ | ||
| └─ On failure: | ||
| rollbackConsolidationLock(priorMtime) → pretend it didn't run | ||
| ``` | ||
| ### 通道 3: Auto-Dream 后台整理 | ||
| ## Read Path — Static Layer (System Prompt, session-level) | ||
| Query loop end → handleStopHooks() → executeAutoDream() │ ├─ Gate 1 (cheapest first): isAutoDreamEnabled()? │ → settings.json + tengu_onyx_plover check │ ├─ Gate 2: Time gate │ (Date.now() - lastConsolidatedAt) / 3_600_000 ≥ minHours (24)? │ → if no: return (most common exit) │ ├─ Gate 3: Scan throttle │ Last scan < SESSION_SCAN_INTERVAL (10 min)? │ → if yes: return (prevents repeated scans) │ ├─ Gate 4: Session gate │ listSessionsTouchedSince(lastAt).length ≥ minSessions (5)? │ → Count distinct sessions with mtime > lastConsolidatedAt │ → Exclude current session │ → if no: return │ ├─ Gate 5: Lock │ tryAcquireConsolidationLock() │ → File lock: mtime-based + PID body │ → Stale after 60 minutes │ → if locked: return │ ├─ Run forked agent: │ consolidation prompt: │ Stage 1: Orient — read MEMORY.md, understand structure │ Stage 2: Gather — read recent session transcripts │ Stage 3: Consolidate — merge/update topic files │ Stage 4: Prune — remove outdated, merge duplicates │ │ Progress tracking: │ DreamTask state: starting → updating → completed/failed/killed │ UI: "dreaming" status pill │ Watches for Edit/Write calls → filesTouched list │ ├─ On success: │ Lock mtime updated → next eligible in 24h │ └─ On failure: rollbackConsolidationLock(priorMtime) → pretend it didn't run | ||
| ``` | ||
| Session start / prompt rebuild | ||
| │ | ||
| ├─ buildEffectiveSystemPrompt() | ||
| │ → systemPromptSection('memory', () => loadMemoryPrompt()) | ||
| │ | ||
| ├─ loadMemoryPrompt() decision tree: | ||
| │ │ | ||
| │ ├─ isAutoMemoryEnabled()? → no → return (no memory section) | ||
| │ │ ├─ CLAUDE_CODE_DISABLE_AUTO_MEMORY=1? → disabled | ||
| │ │ ├─ --bare mode? → disabled | ||
| │ │ ├─ CCR without REMOTE_MEMORY_DIR? → disabled | ||
| │ │ └─ settings.json autoMemoryEnabled=false? → disabled | ||
| │ │ | ||
| │ ├─ KAIROS mode active? | ||
| │ │ → buildAssistantDailyLogPrompt() | ||
| │ │ → Append-only daily log: logs/YYYY/MM/YYYY-MM-DD.md | ||
| │ │ → Nightly /dream distills to topic files | ||
| │ │ | ||
| │ ├─ TEAMMEM enabled? | ||
| │ │ → teamMemPrompts.buildCombinedMemoryPrompt() | ||
| │ │ → Both auto + team dirs, each with own MEMORY.md | ||
| │ │ | ||
| │ └─ Default (auto-memory only): | ||
| │ → buildMemoryLines('auto memory', autoDir) | ||
| │ → Read MEMORY.md synchronously (fs.readFileSync) | ||
| │ → truncateEntrypointContent(): | ||
| │ Line cap first (200 lines) | ||
| │ Then byte cap (25KB) | ||
| │ Append WARNING if truncated | ||
| │ → Inject into system prompt lines | ||
| │ | ||
| └─ Result cached by CWD (systemPromptSection cache) | ||
| → Same project = same memory prompt until CWD changes | ||
| ``` | ||
| II | ||
| ## Read Path — Dynamic Layer (Per-Turn Attachment) | ||
| ## Read Path: 静态 + 动态双层注入 | ||
| ``` | ||
| User message received | ||
| │ | ||
| ├─ startRelevantMemoryPrefetch() | ||
| │ → Spawns promise (runs PARALLEL with model streaming) | ||
| │ | ||
| ├─ Step 1: Scan | ||
| │ scanMemoryFiles(memoryDir, signal): | ||
| │ readdir(recursive) → filter *.md (exclude MEMORY.md) | ||
| │ Read frontmatter (first 30 lines each) + mtime | ||
| │ Sort newest-first → cap at 200 files | ||
| │ Return: MemoryHeader[] = { filename, description, type, mtimeMs } | ||
| │ | ||
| ├─ Step 2: Filter | ||
| │ Remove already-surfaced paths (shown in prior turns) | ||
| │ Check session budget (60KB cap) → stop if exhausted | ||
| │ | ||
| ├─ Step 3: Select (LLM side-query) | ||
| │ selectRelevantMemories(): | ||
| │ Model: getDefaultSonnetModel() (Claude 3.5 Sonnet) | ||
| │ System: "Select up to 5 most relevant memories..." | ||
| │ Input: query + manifest + recently-used tools list | ||
| │ Rules: | ||
| │ - Be selective, only include clearly useful memories | ||
| │ - Skip tool reference docs for recently-used tools | ||
| │ - DO keep warnings/gotchas about those tools | ||
| │ Output: JSON { selected_memories: ["file1.md", "file2.md"] } | ||
| │ Budget: max_tokens=256 | ||
| │ | ||
| ├─ Step 4: Read & Format | ||
| │ readMemoriesForSurfacing(selected): | ||
| │ Read file (200 lines, 4KB byte cap each) | ||
| │ Add header: "Memory (saved X days ago): path:" | ||
| │ Add staleness warning if >1 day old: | ||
| │ memoryFreshnessText() → caveat in <system-reminder> tags | ||
| │ | ||
| ├─ Step 5: Inject | ||
| │ getRelevantMemoryAttachments() → Attachment[] | ||
| │ → AttachmentMessage (user role, <system-reminder> block) | ||
| │ → Injected into toolResults[] | ||
| │ → Model sees memories as part of conversation context | ||
| │ | ||
| └─ Step 6: Track | ||
| collectSurfacedMemories(): | ||
| paths: Set of surfaced filenames (for dedup) | ||
| totalBytes: cumulative injection size (for 60KB cap) | ||
| ``` | ||
| ### Layer 1: 静态注入 — System Prompt (session 级) | ||
| ## Session Lifecycle Timeline | ||
| Session start / prompt rebuild │ ├─ buildEffectiveSystemPrompt() │ → systemPromptSection('memory', () => loadMemoryPrompt()) │ ├─ loadMemoryPrompt() decision tree: │ │ │ ├─ isAutoMemoryEnabled()? → no → return (no memory section) │ │ ├─ CLAUDE_CODE_DISABLE_AUTO_MEMORY=1? → disabled │ │ ├─ --bare mode? → disabled │ │ ├─ CCR without REMOTE_MEMORY_DIR? → disabled │ │ └─ settings.json autoMemoryEnabled=false? → disabled │ │ │ ├─ KAIROS mode active? │ │ → buildAssistantDailyLogPrompt() │ │ → Append-only daily log: logs/YYYY/MM/YYYY-MM-DD.md │ │ → Nightly /dream distills to topic files │ │ │ ├─ TEAMMEM enabled? │ │ → teamMemPrompts.buildCombinedMemoryPrompt() │ │ → Both auto + team dirs, each with own MEMORY.md │ │ │ └─ Default (auto-memory only): │ → buildMemoryLines('auto memory', autoDir) │ → Read MEMORY.md synchronously (fs.readFileSync) │ → truncateEntrypointContent(): │ Line cap first (200 lines) │ Then byte cap (25KB) │ Append WARNING if truncated │ → Inject into system prompt lines │ └─ Result cached by CWD (systemPromptSection cache) → Same project = same memory prompt until CWD changes | ||
| ``` | ||
| SESSION START | ||
| │ | ||
| ├─ loadMemoryPrompt() → read MEMORY.md (sync) | ||
| │ → Inject into system prompt (cached by CWD) | ||
| │ | ||
| │ ┌──── TURN 1 ─────┐ | ||
| │ │ │ | ||
| ├─ User msg ───┤ │ | ||
| │ │ prefetch ──┐ │ | ||
| │ │ (Sonnet) │ │ | ||
| │ Streaming ─┤ │ │ | ||
| │ │ ◄──────────┘ │ | ||
| │ │ inject memories │ | ||
| │ Tool calls ┤ │ | ||
| │ Response ──┤ │ | ||
| │ │ │ | ||
| │ stopHooks ─┤ │ | ||
| │ extractMemories (fire & forget) | ||
| │ autoDream (gate check) │ | ||
| │ └──────────────────┘ | ||
| │ | ||
| │ ┌──── TURN 2 ─────┐ | ||
| │ │ │ | ||
| ├─ User msg ───┤ │ | ||
| │ │ prefetch ──┐ │ ← extraction from Turn 1 | ||
| │ │ (Sonnet) │ │ may still be running | ||
| │ Streaming ─┤ │ │ (parallel, non-blocking) | ||
| │ │ ◄──────────┘ │ | ||
| │ │ inject (dedup) │ | ||
| │ └──────────────────┘ | ||
| │ | ||
| SESSION END | ||
| │ | ||
| └─ drainPendingExtraction() | ||
| → await in-flight extraction promises | ||
| → soft timeout: 60s (doesn't block exit) | ||
| ``` | ||
| ### Layer 2: 动态注入 — Per-Turn Attachment (turn 级) | ||
| ## Write Channel Mutual Exclusion | ||
| User message received │ ├─ startRelevantMemoryPrefetch() │ → Spawns promise (runs PARALLEL with model streaming) │ ├─ Step 1: Scan │ scanMemoryFiles(memoryDir, signal): │ readdir(recursive) → filter *.md (exclude MEMORY.md) │ Read frontmatter (first 30 lines each) + mtime │ Sort newest-first → cap at 200 files │ Return: MemoryHeader[] = { filename, description, type, mtimeMs } │ ├─ Step 2: Filter │ Remove already-surfaced paths (shown in prior turns) │ Check session budget (60KB cap) → stop if exhausted │ ├─ Step 3: Select (LLM side-query) │ selectRelevantMemories(): │ Model: getDefaultSonnetModel() (Claude 3.5 Sonnet) │ System: "Select up to 5 most relevant memories..." │ Input: query + manifest + recently-used tools list │ Rules: │ - Be selective, only include clearly useful memories │ - Skip tool reference docs for recently-used tools │ - DO keep warnings/gotchas about those tools │ Output: JSON { selected_memories: ["file1.md", "file2.md"] } │ Budget: max_tokens=256 │ ├─ Step 4: Read & Format │ readMemoriesForSurfacing(selected): │ Read file (200 lines, 4KB byte cap each) │ Add header: "Memory (saved X days ago): path:" │ Add staleness warning if >1 day old: │ memoryFreshnessText() → caveat in <system-reminder> tags │ ├─ Step 5: Inject │ getRelevantMemoryAttachments() → Attachment[] │ → AttachmentMessage (user role, <system-reminder> block) │ → Injected into toolResults[] │ → Model sees memories as part of conversation context │ └─ Step 6: Track collectSurfacedMemories(): paths: Set of surfaced filenames (for dedup) totalBytes: cumulative injection size (for 60KB cap) | ||
| | Scenario | Main writes | Extraction runs | Auto-Dream runs | | ||
| |---|---|---|---| | ||
| | Normal turn | ✗ | ✓ (if throttle allows) | ✓ (if all gates pass) | | ||
| | User says "remember" | ✓ | SKIP (`hasMemoryWrites`) | ✓ (independent) | | ||
| | Dream consolidating | ✓ | ✓ | LOCK (one at a time) | | ||
| | Extraction running | ✓ | `inProgress` flag | ✓ (independent) | | ||
| III | ||
| ## Extraction Prompt Structure (`prompts.ts:80-154`) | ||
| ## Lifecycle: 时序图 | ||
| ```javascript | ||
| buildExtractAutoOnlyPrompt(): | ||
| 1. List existing memories (avoid duplicates) | ||
| 2. 4-type taxonomy: user/feedback/project/reference | ||
| 3. Emphasize "limited turn budget" (maxTurns=5) | ||
| 4. Strategy: "All reads in turn 1, all writes in turn 2" | ||
| ### Session Timeline | ||
| buildExtractCombinedPrompt(): | ||
| Same as above + team scope support | ||
| → team memory: shared conventions, project facts | ||
| SESSION START │ ├─ loadMemoryPrompt() → read MEMORY.md (sync) │ → Inject into system prompt (cached by CWD) │ │ ┌──── TURN 1 ─────┐ │ │ │ ├─ User message ─────┤ │ │ │ prefetch ──┐ │ │ │ (Sonnet) │ │ │ Model streaming ─┤ │ │ │ │ ◄──────────┘ │ │ │ inject memories │ │ Tool calls ──────┤ │ │ Model response ──┤ │ │ │ │ │ stopHooks ───────┤ │ │ extractMemories│ (fire & forget) │ │ autoDream │ (gate check) │ │ └──────────────────┘ │ │ ┌──── TURN 2 ─────┐ │ │ │ ├─ User message ─────┤ │ │ │ prefetch ──┐ │ │ │ (Sonnet) │ │ ← extraction from Turn 1 │ Model streaming ─┤ │ │ may still be running │ │ ◄──────────┘ │ (parallel, non-blocking) │ │ inject memories │ │ │ (dedup vs T1) │ │ ... │ │ │ └──────────────────┘ │ │ ... (N turns) ... │ SESSION END │ └─ drainPendingExtraction() → await in-flight extraction promises → soft timeout: 60s (doesn't block exit) | ||
| ### Mutual Exclusion Matrix | ||
| Scenario | Main writes | Extraction runs | Auto-Dream runs | ||
| ---|---|---|--- | ||
| Normal turn| ✗| ✓ (if throttle allows)| ✓ (if all gates pass) | ||
| User says "remember"| ✓| SKIP (hasMemoryWrites)| ✓ (independent) | ||
| Dream consolidating| ✓| ✓| LOCK (one at a time) | ||
| Extraction running| ✓| inProgress flag| ✓ (independent) | ||
| IV | ||
| ## Mitsein Mapping | ||
| Claude Code | ||
| * **Storage:** filesystem (~/.claude/projects/<repo>/memory/*.md) | ||
| * **Index:** MEMORY.md (always loaded, 200 lines cap) | ||
| * **Write 3 通道:** main agent + extraction + auto-dream | ||
| * **Read 2 层:** static (system prompt) + dynamic (Sonnet per-turn) | ||
| * **Mutual exclusion:** cursor + hasMemoryWritesSince + lock | ||
| * **Throttle:** configurable via GrowthBook (tengu_bramble_lintel) | ||
| * **Budget:** 4KB/file, 5 files/turn, 60KB/session | ||
| * **Staleness:** memoryAge → <system-reminder> caveat if >1 day | ||
| * **Consolidation:** Auto-Dream with 4-stage pipeline | ||
| * **Single-user:** 一台机器一个 memory 目录 | ||
| Mitsein Equivalent | ||
| * **Storage:** agent_memory 表 (user_id scope, multi-tenant) | ||
| * **Index:** 不需要 MEMORY.md — DB 查询即 index | ||
| * **Write:** SESSION_END hook (单通道) → 需要加 extraction | ||
| * **Read:** recall 全量或按 category → 需要加 relevance | ||
| * **Mutual exclusion:** DB transaction + idempotency_key | ||
| * **Throttle:** agent_config 或全局 settings 配置 | ||
| * **Budget:** 需要定义 per-turn + per-session caps | ||
| * **Staleness:** updated_at 字段 → 注入 caveat | ||
| * **Consolidation:** memory-organizer daemon (daily cron) | ||
| * **Multi-tenant:** user_id scope + 跨 agent/channel 共享 | ||
| **CC → Mitsein 的关键适配:** | ||
| * **Filesystem → DB** — CC 的 scanMemoryFiles 变成 SELECT, Write 变成 INSERT/UPDATE, MEMORY.md 不需要(DB query = index) | ||
| * **Sonnet side-query** — 直接复用,改用 DB 查询构建 manifest 而非 readdir + frontmatter parse | ||
| * **Extraction agent** — 复用 forked agent pattern,canUseTool 改为只允许写 agent_memory 表 | ||
| * **Auto-Dream** — 复用 memory-organizer daemon,加 consolidation prompt | ||
| * **Session budget** — CC 的 60KB cap 直接适用,用 Redis counter 或 per-thread metadata 追踪 | ||
| * **Mutual exclusion** — CC 用 cursor + hasMemoryWrites, Mitsein 用 DB transaction isolation | ||
| MemoryCard extractMemories Auto-Dream findRelevantMemories MEMORY.md Sonnet side-query cursor tracking mutual exclusion | ||
| // Extraction agent state isolation: | ||
| initExtractMemories() — closure-scoped | ||
| → Each instance has independent cursor tracking | ||
| → Test-friendly (no global state dependency) | ||
| ``` |
+307
-37
@@ -1,65 +0,335 @@ | ||
| [← Back to Index](index.html) | ||
| # 13. Main Query Loop | ||
| # 13\. Main Query Loop Critical | ||
| The central `while(true)` loop that connects all modules — `query.ts`. | ||
| while(true) — 把所有模块串起来的骨架 | ||
| ## Architecture Overview | ||
| R | ||
| ``` | ||
| ask() [QueryEngine.ts:1182] | ||
| └─ QueryEngine.constructor() + submitMessage() [QueryEngine.ts:209] | ||
| └─ processUserInput() → record to transcript | ||
| └─ query() [query.ts:155] — async generator, yields messages | ||
| └─ queryLoop() [query.ts:175+] — THE MAIN LOOP | ||
| while (true) { | ||
| Phase 1: Context Assembly + Compaction | ||
| Phase 2: API Streaming (model call) | ||
| Phase 3: Post-Stream Recovery (6 paths) | ||
| Phase 4: Tool Execution | ||
| Phase 5: Attachment Collection | ||
| Phase 6: State Reassembly → LOOP BACK | ||
| } | ||
| ``` | ||
| ## Feature Reference | ||
| ## Feature Gates (inline tree-shaking) | ||
| ### 架构全景 | ||
| | Flag | Phase | | ||
| |---|---| | ||
| | `HISTORY_SNIP` | Bulk history trimming (Phase 1) | | ||
| | `CONTEXT_COLLAPSE` | Projection-based summarization (Phase 1 + 3) | | ||
| | `REACTIVE_COMPACT` | Recovery from 413 without proactive compact (Phase 3) | | ||
| | `CHICAGO_MCP` | Computer use cleanup on abort | | ||
| | `TOKEN_BUDGET` | Per-turn token limits (Phase 3) | | ||
| | `BG_SESSIONS` | Task summary generation (Phase 5) | | ||
| ask() [QueryEngine.ts:1186] └─ QueryEngine.constructor() + submitMessage() [QueryEngine.ts:209] └─ processUserInput() → record to transcript └─ query() [query.ts:155] — async generator, yields messages └─ queryLoop() [query.ts:175+] — THE MAIN LOOP while (true) { Phase 1: Context Assembly + Compaction Phase 2: API Streaming (model call) Phase 3: Post-Stream Recovery (6 paths) Phase 4: Tool Execution Phase 5: Attachment Collection Phase 6: State Reassembly → LOOP BACK } | ||
| ## Key Files | ||
| ### Feature Gates (内联 tree-shaking) | ||
| ``` | ||
| src/query.ts — THE MAIN LOOP (175-1750, ~1750 lines) | ||
| src/QueryEngine.ts — orchestrator class (176-1290) | ||
| src/context.ts — system/user context assembly (45-95) | ||
| src/query/stopHooks.ts — extraction + dream trigger (141-155) | ||
| src/query/tokenBudget.ts — budget tracking | ||
| src/main.tsx — CLI entry point wiring | ||
| ``` | ||
| `HISTORY_SNIP`| Bulk history trimming (Phase 1) | ||
| ---|--- | ||
| `CONTEXT_COLLAPSE`| Projection-based summarization (Phase 1 + 3) | ||
| `REACTIVE_COMPACT`| Recovery from 413 without proactive compact (Phase 3) | ||
| `CHICAGO_MCP`| Computer use cleanup on abort | ||
| `TOKEN_BUDGET`| Per-turn token limits (Phase 3) | ||
| `BG_SESSIONS`| Task summary generation (Phase 5) | ||
| ### Key Files | ||
| --- | ||
| src/query.ts — THE MAIN LOOP (175-1750, ~1750 lines) src/QueryEngine.ts — orchestrator class (176-1290) src/context.ts — system/user context assembly (45-95) src/query/stopHooks.ts — extraction + dream trigger (141-155) src/query/tokenBudget.ts — budget tracking src/main.tsx — CLI entry point wiring | ||
| ## Phase 1: Context Assembly (L300-450) | ||
| ### Phase 1: Context Assembly (L300-450) | ||
| ``` | ||
| Execution order (has dependencies — order matters): | ||
| ═══ 执行顺序 (顺序有依赖!) ═══ 1.1 Query Tracking (L310-330) → Increment depth, new chainId per fork 1.2 Message Boundary Snapping (L342) → getMessagesAfterCompactBoundary(messages) → 只取最后一次 compact 之后的 messages 1.3 Tool Result Budget (L350-365) ⚠️ 在 microcompact 之前! → applyToolResultBudget() → 因为 cached MC 对 content replacement 不可见 1.4 Snip Compaction (L365-375, HISTORY_SNIP) → snipCompactIfNeeded() — bulk trim → Yields boundary message if snip fires 1.5 Microcompact (L375-385) ← ALWAYS runs → deps.microcompact(messages, toolUseContext, querySource) → Cached: defer boundary until API response → Non-cached: emit immediately 1.6 Context Collapse (L389-400, CONTEXT_COLLAPSE) → contextCollapse.applyCollapsesIfNeeded() → Runs BEFORE autocompact (if under-threshold, skip compact) 1.7 Autocompact Decision (L395-450) → Token threshold check → Circuit breaker: MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3 → Post-compact messages yielded → Track taskBudgetRemaining | ||
| 1.1 Query Tracking (L310-330) | ||
| → Increment depth, new chainId per fork | ||
| ### Phase 2: API Streaming (L500-950) | ||
| 1.2 Message Boundary Snapping (L342) | ||
| → getMessagesAfterCompactBoundary(messages) | ||
| → Only use messages after the last compact boundary | ||
| ═══ Pre-API Setup (L500-630) ═══ yield 'stream_request_start' marker Model selection: permissionMode-aware escalation (200k+ tokens) StreamingToolExecutor creation Blocking limit check ═══ Main API Call (L630-680) ═══ for await (const message of deps.callModel({ messages: prependUserContext(messagesForQuery, userContext), // ↑ inject claude.md, currentDate systemPrompt: appendSystemContext(systemPrompt, systemContext), // ↑ append git status, cache breaker tools, model, thinkingConfig, fastMode, effortValue, ... })) ═══ Streaming Message Handler (L690-810) ═══ Fallback Model Recovery: if streamingFallbackOccured → tombstone orphaned messages → clear state, fresh StreamingToolExecutor Backfill Observable Inputs: tool.backfillObservableInput(inputCopy) → only clone if backfill ADDS fields (not overwrites) Withhold Recoverable Errors: contextCollapse.isWithheldPromptTooLong() reactiveCompact.isWithheldPromptTooLong() reactiveCompact.isWithheldMediaSizeError() isWithheldMaxOutputTokens() → 不立即 yield, 走 recovery path Collect Tool Blocks: → Extract tool_use blocks → push to assistantMessages → Feed to StreamingToolExecutor (并行执行开始!) | ||
| 1.3 Tool Result Budget (L350-365) ⚠️ BEFORE microcompact! | ||
| → applyToolResultBudget() | ||
| → Reason: cached MC can't see content replacements | ||
| ### Phase 3: Post-Stream Recovery — 6 条路径 (L950-1350) | ||
| 1.4 Snip Compaction (L365-375, HISTORY_SNIP) | ||
| → snipCompactIfNeeded() — bulk trim | ||
| → Yields boundary message if snip fires | ||
| 只在 needsFollowUp=false (无 tool_use) 时触发: Path 1: User Abort (L960-1010) abortController.signal.aborted? → Drain remaining StreamingToolExecutor results → Yield synthetic tool_result for aborted tools → Return { reason: 'aborted_streaming' } Path 2: Collapse Drain (L1060-1090, CONTEXT_COLLAPSE) 413 + staged collapses? → contextCollapse.recoverFromOverflow() → Commit staged collapses, continue Path 3: Reactive Compact (L1100-1150, REACTIVE_COMPACT) 413 + collapse failed? → reactiveCompact.tryReactiveCompact() → Full summary + strip-retry for media errors Path 4: Max Output Tokens Recovery (L1165-1265) Phase A: Escalation — retry at 64K (single escalation) Phase B: Multi-turn recovery (max 3 attempts) → Inject: "Output token limit hit. Resume directly..." → Continue with recovery message Path 5: Stop Hooks (L1265-1345) handleStopHooks() → extraction, dream, custom hooks → Can inject blocking errors → continue with errors Path 6: Token Budget (L1305-1340, TOKEN_BUDGET) decision.action === 'continue' → inject nudge diminishingReturns → early exit { reason: 'completed' } | ||
| 1.5 Microcompact (L375-385) ← ALWAYS runs | ||
| → deps.microcompact(messages, toolUseContext, querySource) | ||
| → Cached: defer boundary until API response | ||
| → Non-cached: emit immediately | ||
| ### Phase 4: Tool Execution (L1350-1550) | ||
| 1.6 Context Collapse (L389-400, CONTEXT_COLLAPSE) | ||
| → contextCollapse.applyCollapsesIfNeeded() | ||
| → Runs BEFORE autocompact (if under-threshold, skip compact) | ||
| 只在 needsFollowUp=true (有 tool_use blocks) 时执行: 4.1 Tool Executor Selection (L1360-1380) StreamingToolExecutor exists? YES → getRemainingResults() (已经在 Phase 2 并行执行!) NO → runTools() (sequential, blocks on each) 4.2 Tool Result Collection (L1380-1420) for each result: → yield result message → normalize to API format → track hook_stopped_continuation 4.3 Tool Use Summary (L1425-1500) ⚡ 异步 non-blocking: → generateToolUseSummary() fires DURING tool execution → Passed as pendingToolUseSummary to next turn → Awaited AFTER next API response (overlap latency!) 4.4 Abort During Tools (L1515-1545) → Drain remaining executor → Return { reason: 'aborted_tools' } | ||
| 1.7 Autocompact Decision (L395-450) | ||
| → Token threshold check | ||
| → Circuit breaker: MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3 | ||
| → Post-compact messages yielded | ||
| → Track taskBudgetRemaining | ||
| ``` | ||
| ### Phase 5: Attachment Collection (L1560-1670) | ||
| ## Phase 2: API Streaming (L500-950) | ||
| 三种 parallel prefetch 在这里 harvest: 5.1 Queued Commands (L1570-1620) getCommandsByMaxPriority(): priority: 'later' if SLEEP_TOOL ran, else 'next' → Filter by slash command status + agentId → Convert to attachment messages → Remove consumed from queue 5.2 Memory Prefetch (L1625-1650) pendingMemoryPrefetch.settledAt !== null? → Filter duplicates against readFileState → Add to toolResults 5.3 Skill Discovery Prefetch (L1655-1670) skillPrefetch.collectSkillDiscoveryPrefetch() → Discovery 跟 API stream 并行 → Harvest results, add as attachments | ||
| ``` | ||
| Pre-API Setup (L500-630): | ||
| yield 'stream_request_start' marker | ||
| Model selection: permissionMode-aware escalation (200k+ tokens) | ||
| StreamingToolExecutor creation | ||
| Blocking limit check | ||
| ### Phase 6: Continue — 状态重组 (L1700-1750) | ||
| Main API Call (L630-680): | ||
| for await (const message of deps.callModel({ | ||
| messages: prependUserContext(messagesForQuery, userContext), | ||
| // ↑ inject claude.md, currentDate | ||
| systemPrompt: appendSystemContext(systemPrompt, systemContext), | ||
| // ↑ append git status, cache breaker | ||
| tools, model, thinkingConfig, fastMode, effortValue, ... | ||
| })) | ||
| 6.1 maxTurns Check (L1710-1720) nextTurnCount > maxTurns → yield attachment + return 6.2 Task Summary (L1665-1695, BG_SESSIONS) taskSummaryModule.maybeGenerateTaskSummary() → for 'claude ps' display 6.3 THE CONTINUATION SITE (L1730-1750): const next: State = { messages: [...messagesForQuery, ...assistantMessages, ...toolResults], toolUseContext: toolUseContextWithQueryTracking, autoCompactTracking: tracking, turnCount: nextTurnCount, maxOutputTokensRecoveryCount: 0, // reset hasAttemptedReactiveCompact: false, // reset pendingToolUseSummary: nextPendingToolUseSummary, maxOutputTokensOverride: undefined, // reset stopHookActive, transition: { reason: 'next_turn' }, } state = next // ← LOOP BACK TO PHASE 1 | ||
| Streaming Message Handler (L690-810): | ||
| Fallback Model Recovery: | ||
| if streamingFallbackOccured → tombstone orphaned messages | ||
| → clear state, fresh StreamingToolExecutor | ||
| ### QueryEngine — Orchestrator Class (QueryEngine.ts:176-1170) | ||
| Backfill Observable Inputs: | ||
| tool.backfillObservableInput(inputCopy) | ||
| → only clone if backfill ADDS fields (not overwrites) | ||
| class QueryEngine { private mutableMessages: Message[] // persists across turns private totalUsage: NonNullableUsage // cumulative API billing private abortController: AbortController private readFileState: FileStateCache // cache for read-file tool private discoveredSkillNames: Set<string> // turn-scoped constructor(config: QueryEngineConfig) async *submitMessage(prompt, options?) // per-turn entry } ═══ submitMessage() Flow ═══ 1\. System Prompt Assembly (L250-300): [customSystemPrompt OR default] \+ [memoryMechanicsPrompt?] \+ [appendSystemPrompt?] → MultiLineSystemPrompt 2\. User Input Processing (L305-420): processUserInput({ input, mode:'prompt' }) → Parse slash commands → Record to transcript BEFORE query (resumability!) 3\. System Init Message (L490-510): buildSystemInitMessage() → tools list + status 4\. Early Return (L520-590): !shouldQuery (local commands like /version) → yield outputs, return early 5\. Query Loop (L600-1170): for await (const message of query({...})) → Switch on message type: assistant → push + yield user → push + yield stream_event → track usage attachment → handle structured output system (compact_boundary) → handle snip 6\. Result Emission (L1100-1170): yield { type:'result', usage, cost, session_id } | ||
| Withhold Recoverable Errors: | ||
| contextCollapse.isWithheldPromptTooLong() | ||
| reactiveCompact.isWithheldPromptTooLong() | ||
| reactiveCompact.isWithheldMediaSizeError() | ||
| isWithheldMaxOutputTokens() | ||
| → don't yield immediately, take recovery path | ||
| ### Context Assembly — Prompt 构建 (context.ts) | ||
| Collect Tool Blocks: | ||
| → Extract tool_use blocks → push to assistantMessages | ||
| → Feed to StreamingToolExecutor (parallel execution starts!) | ||
| ``` | ||
| ═══ System Context (L45-75) — memoized ═══ getSystemContext(): gitStatus = await getGitStatus() // → git commands run in PARALLEL (branch, status, recent commits) injection = getSystemPromptInjection() (ant-only, cache break) return { gitStatus, cacheBreaker } ═══ User Context (L80-95) — memoized ═══ getUserContext(): claudeMd = getClaudeMds(await getMemoryFiles()) // → directory walk for claude.md files return { claudeMd, currentDate } // Disabled if CLAUDE_CODE_DISABLE_CLAUDE_MDS or --bare ═══ API Call 时拼装 (query.ts:630) ═══ messages: prependUserContext(messagesForQuery, userContext) // ↑ inject { claude.md, currentDate } systemPrompt: appendSystemContext(systemPrompt, systemContext) // ↑ append git status + cache breaker | ||
| ## Phase 3: Post-Stream Recovery — 6 Paths (L950-1350) | ||
| ### Main Loop 决策树 (Summary) | ||
| Only triggered when `needsFollowUp=false` (no `tool_use`): | ||
| WHILE TRUE: ├─ Context Assembly (snip → MC → autocompact) ├─ API Call → STREAMING │ ├─ Tool blocks → collect + feed StreamingToolExecutor │ └─ Fallback error → retry with fallback model │ ├─ needsFollowUp? (tool_use blocks?) │ ├─ NO: │ │ ├─ 413 + collapse? → Recover + Continue │ │ ├─ 413 + reactive? → Compact + Continue │ │ ├─ max_tokens hit? → Escalate + Continue │ │ ├─ Stop hooks errors? → Inject + Continue │ │ ├─ Token budget? → Nudge + Continue │ │ └─ Clean completion → RETURN ✓ │ │ │ └─ YES: │ ├─ Execute tools (stream ∥ or batched ─) │ ├─ Abort during tools? → RETURN │ ├─ Harvest: commands + memory + skills │ └─ maxTurns exceeded? → RETURN │ └─ state = { messages + assistant + tools, turnCount++ } → Jump to Phase 1 | ||
| ``` | ||
| Path 1: User Abort (L960-1010) | ||
| abortController.signal.aborted? | ||
| → Drain remaining StreamingToolExecutor results | ||
| → Yield synthetic tool_result for aborted tools | ||
| → Return { reason: 'aborted_streaming' } | ||
| query.ts QueryEngine.ts context.ts main-loop orchestration | ||
| Path 2: Collapse Drain (L1060-1090, CONTEXT_COLLAPSE) | ||
| 413 + staged collapses? | ||
| → contextCollapse.recoverFromOverflow() | ||
| → Commit staged collapses, continue | ||
| Path 3: Reactive Compact (L1100-1150, REACTIVE_COMPACT) | ||
| 413 + collapse failed? | ||
| → reactiveCompact.tryReactiveCompact() | ||
| → Full summary + strip-retry for media errors | ||
| Path 4: Max Output Tokens Recovery (L1165-1265) | ||
| Phase A: Escalation — retry at 64K (single escalation) | ||
| Phase B: Multi-turn recovery (max 3 attempts) | ||
| → Inject: "Output token limit hit. Resume directly..." | ||
| → Continue with recovery message | ||
| Path 5: Stop Hooks (L1265-1345) | ||
| handleStopHooks() → extraction, dream, custom hooks | ||
| → Can inject blocking errors → continue with errors | ||
| Path 6: Token Budget (L1305-1340, TOKEN_BUDGET) | ||
| decision.action === 'continue' → inject nudge | ||
| diminishingReturns → early exit { reason: 'completed' } | ||
| ``` | ||
| ## Phase 4: Tool Execution (L1350-1550) | ||
| Only when `needsFollowUp=true` (tool_use blocks present): | ||
| ``` | ||
| 4.1 Tool Executor Selection (L1360-1380) | ||
| StreamingToolExecutor exists? | ||
| YES → getRemainingResults() (already executing in parallel from Phase 2!) | ||
| NO → runTools() (sequential, blocks on each) | ||
| 4.2 Tool Result Collection (L1380-1420) | ||
| for each result: | ||
| → yield result message | ||
| → normalize to API format | ||
| → track hook_stopped_continuation | ||
| 4.3 Tool Use Summary (L1425-1500) | ||
| ⚡ async non-blocking: | ||
| → generateToolUseSummary() fires DURING tool execution | ||
| → Passed as pendingToolUseSummary to next turn | ||
| → Awaited AFTER next API response (overlap latency!) | ||
| 4.4 Abort During Tools (L1515-1545) | ||
| → Drain remaining executor | ||
| → Return { reason: 'aborted_tools' } | ||
| ``` | ||
| ## Phase 5: Attachment Collection (L1560-1670) | ||
| Three parallel prefetches are harvested here: | ||
| ``` | ||
| 5.1 Queued Commands (L1570-1620) | ||
| getCommandsByMaxPriority(): | ||
| priority: 'later' if SLEEP_TOOL ran, else 'next' | ||
| → Filter by slash command status + agentId | ||
| → Convert to attachment messages | ||
| → Remove consumed from queue | ||
| 5.2 Memory Prefetch (L1625-1650) | ||
| pendingMemoryPrefetch.settledAt !== null? | ||
| → Filter duplicates against readFileState | ||
| → Add to toolResults | ||
| 5.3 Skill Discovery Prefetch (L1655-1670) | ||
| skillPrefetch.collectSkillDiscoveryPrefetch() | ||
| → Discovery runs parallel with API stream | ||
| → Harvest results, add as attachments | ||
| ``` | ||
| ## Phase 6: State Reassembly (L1700-1750) | ||
| ```javascript | ||
| 6.1 maxTurns Check (L1710-1720) | ||
| nextTurnCount > maxTurns → yield attachment + return | ||
| 6.2 Task Summary (L1665-1695, BG_SESSIONS) | ||
| taskSummaryModule.maybeGenerateTaskSummary() | ||
| → for 'claude ps' display | ||
| 6.3 THE CONTINUATION SITE (L1730-1750): | ||
| const next: State = { | ||
| messages: [...messagesForQuery, | ||
| ...assistantMessages, | ||
| ...toolResults], | ||
| toolUseContext: toolUseContextWithQueryTracking, | ||
| autoCompactTracking: tracking, | ||
| turnCount: nextTurnCount, | ||
| maxOutputTokensRecoveryCount: 0, // reset | ||
| hasAttemptedReactiveCompact: false, // reset | ||
| pendingToolUseSummary: nextPendingToolUseSummary, | ||
| maxOutputTokensOverride: undefined, // reset | ||
| stopHookActive, | ||
| transition: { reason: 'next_turn' }, | ||
| } | ||
| state = next // ← LOOP BACK TO PHASE 1 | ||
| ``` | ||
| ## QueryEngine — Orchestrator Class (`QueryEngine.ts:176-1170`) | ||
| ```typescript | ||
| class QueryEngine { | ||
| private mutableMessages: Message[] // persists across turns | ||
| private totalUsage: NonNullableUsage // cumulative API billing | ||
| private abortController: AbortController | ||
| private readFileState: FileStateCache // cache for read-file tool | ||
| private discoveredSkillNames: Set<string> // turn-scoped | ||
| constructor(config: QueryEngineConfig) | ||
| async *submitMessage(prompt, options?) // per-turn entry | ||
| } | ||
| ``` | ||
| `submitMessage()` flow: | ||
| ``` | ||
| 1. System Prompt Assembly (L250-300): | ||
| [customSystemPrompt OR default] | ||
| + [memoryMechanicsPrompt?] | ||
| + [appendSystemPrompt?] | ||
| → MultiLineSystemPrompt | ||
| 2. User Input Processing (L305-420): | ||
| processUserInput({ input, mode:'prompt' }) | ||
| → Parse slash commands | ||
| → Record to transcript BEFORE query (resumability!) | ||
| 3. System Init Message (L490-510): | ||
| buildSystemInitMessage() → tools list + status | ||
| 4. Early Return (L520-590): | ||
| !shouldQuery (local commands like /version) | ||
| → yield outputs, return early | ||
| 5. Query Loop (L600-1170): | ||
| for await (const message of query({...})) | ||
| → Switch on message type: | ||
| assistant → push + yield | ||
| user → push + yield | ||
| stream_event → track usage | ||
| attachment → handle structured output | ||
| system (compact_boundary) → handle snip | ||
| 6. Result Emission (L1100-1170): | ||
| yield { type:'result', usage, cost, session_id } | ||
| ``` | ||
| ## Context Assembly (`context.ts`) | ||
| ```javascript | ||
| // System Context (L45-75) — memoized | ||
| getSystemContext(): | ||
| gitStatus = await getGitStatus() | ||
| // → git commands run in PARALLEL (branch, status, recent commits) | ||
| injection = getSystemPromptInjection() (ant-only, cache break) | ||
| return { gitStatus, cacheBreaker } | ||
| // User Context (L80-95) — memoized | ||
| getUserContext(): | ||
| claudeMd = getClaudeMds(await getMemoryFiles()) | ||
| // → directory walk for claude.md files | ||
| return { claudeMd, currentDate } | ||
| // Disabled if CLAUDE_CODE_DISABLE_CLAUDE_MDS or --bare | ||
| // API call assembly (query.ts:630): | ||
| messages: prependUserContext(messagesForQuery, userContext) | ||
| // ↑ inject { claude.md, currentDate } | ||
| systemPrompt: appendSystemContext(systemPrompt, systemContext) | ||
| // ↑ append git status + cache breaker | ||
| ``` | ||
| ## Decision Tree Summary | ||
| ``` | ||
| WHILE TRUE: | ||
| ├─ Context Assembly (snip → MC → autocompact) | ||
| ├─ API Call → STREAMING | ||
| │ ├─ Tool blocks → collect + feed StreamingToolExecutor | ||
| │ └─ Fallback error → retry with fallback model | ||
| │ | ||
| ├─ needsFollowUp? (tool_use blocks?) | ||
| │ ├─ NO: | ||
| │ │ ├─ 413 + collapse? → Recover + Continue | ||
| │ │ ├─ 413 + reactive? → Compact + Continue | ||
| │ │ ├─ max_tokens hit? → Escalate + Continue | ||
| │ │ ├─ Stop hooks errors? → Inject + Continue | ||
| │ │ ├─ Token budget? → Nudge + Continue | ||
| │ │ └─ Clean completion → RETURN ✓ | ||
| │ │ | ||
| │ └─ YES: | ||
| │ ├─ Execute tools (stream ∥ or batched ─) | ||
| │ ├─ Abort during tools? → RETURN | ||
| │ ├─ Harvest: commands + memory + skills | ||
| │ └─ maxTurns exceeded? → RETURN | ||
| │ | ||
| └─ state = { messages + assistant + tools, turnCount++ } | ||
| → Jump to Phase 1 | ||
| ``` |
+178
-22
@@ -1,43 +0,199 @@ | ||
| [← Back to Index](index.html) | ||
| # 14. Bridge System | ||
| # 14\. Bridge System High | ||
| IDE Extension ↔ CLI REPL bidirectional real-time communication layer. | ||
| IDE Extension ↔ CLI REPL 的双向实时通信层 | ||
| ## Architecture Overview | ||
| R | ||
| ``` | ||
| ┌─────────────┐ Environments API ┌─────────────────┐ | ||
| │ VS Code / │ ◄─── long-poll ────► │ Anthropic │ | ||
| │ JetBrains │ │ Backend │ | ||
| │ Extension │ │ (work dispatch) │ | ||
| └──────┬───────┘ └────────┬────────┘ | ||
| │ │ | ||
| │ WebSocket (SDK ingress) │ poll for work | ||
| │ │ | ||
| ┌──────▼───────┐ ┌────────▼────────┐ | ||
| │ User's │ │ Bridge Main │ | ||
| │ Browser │ │ (bridgeMain.ts) │ | ||
| │ (Web UI) │ │ ├─ register env │ | ||
| └──────────────┘ │ ├─ poll work │ | ||
| │ └─ spawn child │ | ||
| └────────┬────────┘ | ||
| │ stdin/stdout JSONL | ||
| ┌────────▼────────┐ | ||
| │ Child CLI │ | ||
| │ (--sdk-url) │ | ||
| │ REPL session │ | ||
| └─────────────────┘ | ||
| ``` | ||
| ## Feature Reference | ||
| ## Key Files (~30 files, ~2000 LOC) | ||
| ### 架构全景 | ||
| ``` | ||
| src/bridge/bridgeMain.ts — main loop: register → poll → spawn | ||
| src/bridge/bridgeApi.ts — HTTP API client (8 endpoints) | ||
| src/bridge/bridgeConfig.ts — OAuth token + base URL resolution | ||
| src/bridge/bridgeEnabled.ts — feature gate + diagnostics | ||
| src/bridge/bridgeMessaging.ts — message dedup + routing | ||
| src/bridge/replBridge.ts — IDE ↔ REPL connection (L79+) | ||
| src/bridge/replBridgeHandle.ts — global singleton handle | ||
| src/bridge/replBridgeTransport.ts — WebSocket transport | ||
| src/bridge/sessionRunner.ts — child process spawning | ||
| src/bridge/inboundMessages.ts — message normalization | ||
| src/bridge/inboundAttachments.ts — attachment handling | ||
| src/bridge/capacityWake.ts — sleep/wake signaling | ||
| src/bridge/trustedDevice.ts — device trust (90-day token) | ||
| src/bridge/types.ts — all type definitions | ||
| src/bridge/jwtUtils.ts — JWT parsing | ||
| src/bridge/workSecret.ts — session secret decode | ||
| ``` | ||
| ┌─────────────┐ Environments API ┌─────────────────┐ │ VS Code / │ ◄─── long-poll ────► │ Anthropic │ │ JetBrains │ │ Backend │ │ Extension │ │ (work dispatch) │ └──────┬───────┘ └────────┬────────┘ │ │ │ WebSocket (SDK ingress) │ poll for work │ │ ┌──────▼───────┐ ┌────────▼────────┐ │ User's │ │ Bridge Main │ │ Browser │ │ (bridgeMain.ts) │ │ (Web UI) │ │ ├─ register env │ └──────────────┘ │ ├─ poll work │ │ └─ spawn child │ └────────┬────────┘ │ stdin/stdout JSONL ┌────────▼────────┐ │ Child CLI │ │ (--sdk-url) │ │ REPL session │ └─────────────────┘ | ||
| --- | ||
| ### Key Files (30 files, ~2000 LOC) | ||
| ## BridgeConfig (`types.ts:90-130`) | ||
| src/bridge/bridgeMain.ts — main loop: register → poll → spawn src/bridge/bridgeApi.ts — HTTP API client (8 endpoints) src/bridge/bridgeConfig.ts — OAuth token + base URL resolution src/bridge/bridgeEnabled.ts — feature gate + diagnostics src/bridge/bridgeMessaging.ts — message dedup + routing src/bridge/replBridge.ts — IDE ↔ REPL connection (L79+) src/bridge/replBridgeHandle.ts — global singleton handle src/bridge/replBridgeTransport.ts — WebSocket transport src/bridge/sessionRunner.ts — child process spawning src/bridge/inboundMessages.ts — message normalization src/bridge/inboundAttachments.ts — attachment handling src/bridge/capacityWake.ts — sleep/wake signaling src/bridge/trustedDevice.ts — device trust (90-day token) src/bridge/types.ts — all type definitions src/bridge/jwtUtils.ts — JWT parsing src/bridge/workSecret.ts — session secret decode | ||
| ```typescript | ||
| type BridgeConfig = { | ||
| dir: string // working directory | ||
| machineName: string // machine identifier | ||
| branch?: string // git branch | ||
| gitRepoUrl?: string // repo URL | ||
| maxSessions: number // concurrent session limit | ||
| spawnMode: SpawnMode // 'single-session' | 'worktree' | 'same-dir' | ||
| verbose: boolean | ||
| sandbox: boolean | ||
| bridgeId: string // stable per-machine | ||
| workerType: string // environment type | ||
| environmentId?: string // from registration | ||
| reuseEnvironmentId?: string // idempotent re-register | ||
| apiBaseUrl: string | ||
| sessionIngressUrl: string | ||
| debugFile?: string | ||
| sessionTimeoutMs: number | ||
| } | ||
| ### BridgeConfig — 完整配置 (types.ts:90-130) | ||
| SpawnMode: | ||
| 'single-session' — one session at a time | ||
| 'worktree' — one git worktree per session | ||
| 'same-dir' — all sessions share directory | ||
| ``` | ||
| type BridgeConfig = { dir: string // working directory machineName: string // machine identifier branch?: string // git branch gitRepoUrl?: string // repo URL maxSessions: number // concurrent session limit spawnMode: SpawnMode // 'single-session' | 'worktree' | 'same-dir' verbose: boolean sandbox: boolean bridgeId: string // stable per-machine workerType: string // environment type environmentId?: string // from registration reuseEnvironmentId?: string // idempotent re-register apiBaseUrl: string sessionIngressUrl: string debugFile?: string sessionTimeoutMs: number } SpawnMode: 'single-session' — 一次只跑一个 session 'worktree' — 每个 session 一个 git worktree 'same-dir' — 所有 sessions 共享目录 | ||
| ## Bridge API — 8 HTTP Endpoints (`bridgeApi.ts`) | ||
| ### Bridge API — 8 个 HTTP Endpoints | ||
| ``` | ||
| POST /v1/environments/bridge | ||
| → registerBridgeEnvironment() | ||
| → Sends: machine_name, directory, branch, git_repo_url, | ||
| max_sessions, metadata.worker_type | ||
| → Returns: { environment_id, environment_secret } | ||
| → Supports idempotent re-registration (reuseEnvironmentId) | ||
| // bridgeApi.ts — createBridgeApiClient(deps) POST /v1/environments/bridge → registerBridgeEnvironment() → Sends: machine_name, directory, branch, git_repo_url, max_sessions, metadata.worker_type → Returns: { environment_id, environment_secret } → Supports idempotent re-registration (reuseEnvironmentId) pollForWork() — long-poll for WorkResponse | null stopWork() — stop work item (force optional) deregisterEnvironment() — graceful shutdown cleanup sendPermissionResponseEvent() — post permission decision heartbeatWork() — extends lease, returns { lease_extended, state } archiveSession() — hide from web UI reconnectSession() — resume after bridge crash // 安全机制: OAuth 401 Retry (withOAuthRetry): refresh + retry once Trusted Device: X-Trusted-Device-Token header (90-day rolling) | ||
| pollForWork() — long-poll for WorkResponse | null | ||
| stopWork() — stop work item (force optional) | ||
| deregisterEnvironment() — graceful shutdown cleanup | ||
| sendPermissionResponseEvent() — post permission decision | ||
| heartbeatWork() — extends lease, returns { lease_extended, state } | ||
| archiveSession() — hide from web UI | ||
| reconnectSession() — resume after bridge crash | ||
| ### Session Spawning (sessionRunner.ts) | ||
| // Security: | ||
| OAuth 401 Retry (withOAuthRetry): refresh + retry once | ||
| Trusted Device: X-Trusted-Device-Token header (90-day rolling) | ||
| ``` | ||
| createSessionSpawner(deps): → spawn child CLI with: \--sdk-url, --session-id, --access-token, --ingress-url Activity Tracking (L80-140): → 从 stdout JSONL 实时提取 tool_use → Ring buffers: MAX_ACTIVITIES=10, MAX_STDERR_LINES=10 → Tool verb mapping: Read→'Reading', Write→'Writing', Edit→'Editing', Bash→'Running', Grep→'Searching', WebFetch→'Fetching' → Summary: verb + target (file_path | command | url | query) Permission Requests (L180+): → can_use_tool control_request extracted from stdout → forwarded to bridge API via onPermissionRequest WorkSecret (decoded session secret): { version, session_ingress_token, api_base_url, sources, auth, claude_code_args, mcp_config, environment_variables, use_code_sessions } | ||
| ## Session Spawning (`sessionRunner.ts`) | ||
| ### Message Dedup — BoundedUUIDSet (bridgeMessaging.ts) | ||
| ```javascript | ||
| createSessionSpawner(deps): | ||
| → spawn child CLI with: | ||
| --sdk-url, --session-id, --access-token, --ingress-url | ||
| handleIngressMessage(): Two-level UUID dedup: recentPostedUUIDs — echoes (messages we sent) recentInboundUUIDs — replay (server re-delivery) → BoundedUUIDSet: circular buffer prevents echo Message routing: control_response → onPermissionResponse (permission approved) control_request → onControlRequest (set_model, can_use_tool, init) SDKMessage → UUID check → onInboundMessage (user type only) isEligibleBridgeMessage(m): true for: user, assistant, system+local_command false for: virtuals, internal messages extractTitleText(m): → Derive session title from first meaningful message → Filter: meta, toolUseResult, compact, non-human | ||
| Activity Tracking (L80-140): | ||
| → extract tool_use from stdout JSONL in real time | ||
| → Ring buffers: MAX_ACTIVITIES=10, MAX_STDERR_LINES=10 | ||
| → Tool verb mapping: | ||
| Read→'Reading', Write→'Writing', Edit→'Editing', | ||
| Bash→'Running', Grep→'Searching', WebFetch→'Fetching' | ||
| → Summary: verb + target (file_path | command | url | query) | ||
| ### REPL Bridge — IDE ↔ REPL 实时同步 | ||
| Permission Requests (L180+): | ||
| → can_use_tool control_request extracted from stdout | ||
| → forwarded to bridge API via onPermissionRequest | ||
| // replBridge.ts:79+ initBridgeCore(params: BridgeCoreParams): Flow: initialMessages → createSession API → WebSocket connect → message dedup → onInboundMessage callback State machine: ready → connected → (reconnecting loops) → failed Global singleton (replBridgeHandle.ts): getReplBridgeHandle() / setReplBridgeHandle(h) → 一个 CLI 进程只有一个 bridge instance → updateSessionBridgeId() for local peer dedup // Capacity Wake (capacityWake.ts): createCapacityWake(outerSignal): → Merges outer AbortSignal + capacity controller → wake(): abort current → poll loop re-checks immediately → Used by replBridge + bridgeMain to sleep at capacity // iOS Image Normalization (inboundMessages.ts): normalizeImageBlocks(): → iOS sends camelCase 'mediaType' (not snake_case 'media_type') → Auto-detect format from base64 magic bytes if missing | ||
| WorkSecret (decoded session secret): | ||
| { version, session_ingress_token, api_base_url, | ||
| sources, auth, claude_code_args, mcp_config, | ||
| environment_variables, use_code_sessions } | ||
| ``` | ||
| ### Device Trust (trustedDevice.ts) | ||
| ## Message Dedup — BoundedUUIDSet (`bridgeMessaging.ts`) | ||
| Gate: tengu_sessions_elevated_auth_enforcement Enrollment: POST /auth/trusted_devices during /login Server gates: account_session.created_at < 10min Token: 90-day rolling expiry Stored in macOS keychain Memoized (secureStorage read ~40ms, cached) Usage: X-Trusted-Device-Token header on every bridge API call CCR v2: enforces SecurityTier=ELEVATED server-side | ||
| ```javascript | ||
| handleIngressMessage(): | ||
| Two-level UUID dedup: | ||
| recentPostedUUIDs — echoes (messages we sent) | ||
| recentInboundUUIDs — replay (server re-delivery) | ||
| → BoundedUUIDSet: circular buffer prevents echo | ||
| bridge IDE integration WebSocket session spawning device trust | ||
| Message routing: | ||
| control_response → onPermissionResponse (permission approved) | ||
| control_request → onControlRequest (set_model, can_use_tool, init) | ||
| SDKMessage → UUID check → onInboundMessage (user type only) | ||
| isEligibleBridgeMessage(m): | ||
| true for: user, assistant, system+local_command | ||
| false for: virtuals, internal messages | ||
| extractTitleText(m): | ||
| → Derive session title from first meaningful message | ||
| → Filter: meta, toolUseResult, compact, non-human | ||
| ``` | ||
| ## REPL Bridge — IDE ↔ REPL Sync (`replBridge.ts:79+`) | ||
| ```javascript | ||
| initBridgeCore(params: BridgeCoreParams): | ||
| Flow: | ||
| initialMessages → createSession API → WebSocket connect | ||
| → message dedup → onInboundMessage callback | ||
| State machine: | ||
| ready → connected → (reconnecting loops) → failed | ||
| Global singleton (replBridgeHandle.ts): | ||
| getReplBridgeHandle() / setReplBridgeHandle(h) | ||
| → one bridge instance per CLI process | ||
| → updateSessionBridgeId() for local peer dedup | ||
| // Capacity Wake (capacityWake.ts): | ||
| createCapacityWake(outerSignal): | ||
| → Merges outer AbortSignal + capacity controller | ||
| → wake(): abort current → poll loop re-checks immediately | ||
| → Used by replBridge + bridgeMain to sleep at capacity | ||
| // iOS Image Normalization (inboundMessages.ts): | ||
| normalizeImageBlocks(): | ||
| → iOS sends camelCase 'mediaType' (not snake_case 'media_type') | ||
| → Auto-detect format from base64 magic bytes if missing | ||
| ``` | ||
| ## Device Trust (`trustedDevice.ts`) | ||
| ``` | ||
| Gate: tengu_sessions_elevated_auth_enforcement | ||
| Enrollment: | ||
| POST /auth/trusted_devices during /login | ||
| Server gates: account_session.created_at < 10min | ||
| Token: | ||
| 90-day rolling expiry | ||
| Stored in macOS keychain | ||
| Memoized (secureStorage read ~40ms, cached) | ||
| Usage: | ||
| X-Trusted-Device-Token header on every bridge API call | ||
| CCR v2: enforces SecurityTier=ELEVATED server-side | ||
| ``` |
+208
-24
@@ -1,47 +0,231 @@ | ||
| [← Back to Index](index.html) | ||
| # 15. Ink Terminal UI | ||
| # 15\. Ink Terminal UI High | ||
| Custom React Terminal Reconciler + Yoga Layout + double-buffered rendering. | ||
| 自研 React Terminal Reconciler + Yoga Layout + 双缓冲渲染 | ||
| ## Architecture Overview | ||
| R | ||
| ``` | ||
| ┌─────────────────────────────────────────────────┐ | ||
| │ React Tree │ | ||
| │ <App> │ | ||
| │ <Box flexDirection="column"> │ | ||
| │ <Text>Hello</Text> │ | ||
| │ <ScrollBox height={20}>...</ScrollBox> │ | ||
| │ <Button onClick={...}>OK</Button> │ | ||
| │ </Box> │ | ||
| │ </App> │ | ||
| └─────────────┬───────────────────────────────────┘ | ||
| │ react-reconciler | ||
| ┌─────────────▼───────────────────────────────────┐ | ||
| │ Custom DOM (DOMElement + yogaNode) │ | ||
| │ ├─ createInstance() → DOMElement │ | ||
| │ ├─ appendChild/insertBefore/removeChild │ | ||
| │ └─ setMeasureFunc() for text measurement │ | ||
| └─────────────┬───────────────────────────────────┘ | ||
| │ Yoga layout (WASM) | ||
| ┌─────────────▼───────────────────────────────────┐ | ||
| │ Screen Buffer (character grid) │ | ||
| │ ├─ frontFrame / backFrame (double buffering) │ | ||
| │ ├─ renderNodeToOutput() → rasterize tree │ | ||
| │ ├─ diff-based blit (only changed cells) │ | ||
| │ └─ ANSI escape code output │ | ||
| └─────────────┬───────────────────────────────────┘ | ||
| │ | ||
| ┌─────────────▼───────────────────────────────────┐ | ||
| │ Terminal (stdout/stdin) │ | ||
| │ ├─ Mouse: mode-1003 motion tracking │ | ||
| │ ├─ Keyboard: kitty protocol + CSI parsing │ | ||
| │ ├─ Selection: drag + word-mode + scroll │ | ||
| │ └─ Hit-test: click → deepest DOMElement │ | ||
| └─────────────────────────────────────────────────┘ | ||
| ``` | ||
| ## Feature Reference | ||
| ## Key Files (~45 files, ~5000 LOC) | ||
| ### 架构全景 | ||
| ``` | ||
| src/ink/ink.tsx — Ink class, frame loop, selection (L87+) | ||
| src/ink/reconciler.ts — React reconciler (createInstance, mutations) | ||
| src/ink/renderer.ts — screen buffer, blit optimization (L1+) | ||
| src/ink/terminal.ts — terminal capability detection | ||
| src/ink/dom.ts — DOMElement, yogaNode, style application | ||
| src/ink/hit-test.ts — click/hover dispatch, parent chain bubble | ||
| src/ink/selection.ts — text selection state machine | ||
| src/ink/components/ — 18 component files: | ||
| App.tsx — root frame loop, mouse/keyboard | ||
| Box.tsx, Text.tsx — layout containers | ||
| ScrollBox.tsx — viewport with soft-wrap | ||
| AlternateScreen.tsx — fullscreen mode | ||
| Button.tsx, Link.tsx — interactive elements | ||
| RawAnsi.tsx — pass-through ANSI codes | ||
| ErrorOverview.tsx — error display panel | ||
| ``` | ||
| ┌─────────────────────────────────────────────────┐ │ React Tree │ │ <App> │ │ <Box flexDirection="column"> │ │ <Text>Hello</Text> │ │ <ScrollBox height={20}>...</ScrollBox> │ │ <Button onClick={...}>OK</Button> │ │ </Box> │ │ </App> │ └─────────────┬───────────────────────────────────┘ │ react-reconciler ┌─────────────▼───────────────────────────────────┐ │ Custom DOM (DOMElement + yogaNode) │ │ ├─ createInstance() → DOMElement │ │ ├─ appendChild/insertBefore/removeChild │ │ └─ setMeasureFunc() for text measurement │ └─────────────┬───────────────────────────────────┘ │ Yoga layout (WASM) ┌─────────────▼───────────────────────────────────┐ │ Screen Buffer (character grid) │ │ ├─ frontFrame / backFrame (double buffering) │ │ ├─ renderNodeToOutput() → rasterize tree │ │ ├─ diff-based blit (only changed cells) │ │ └─ ANSI escape code output │ └─────────────┬───────────────────────────────────┘ │ ┌─────────────▼───────────────────────────────────┐ │ Terminal (stdout/stdin) │ │ ├─ Mouse: mode-1003 motion tracking │ │ ├─ Keyboard: kitty protocol + CSI parsing │ │ ├─ Selection: drag + word-mode + scroll │ │ └─ Hit-test: click → deepest DOMElement │ └─────────────────────────────────────────────────┘ | ||
| --- | ||
| ### Key Files (45+ files, ~5000 LOC) | ||
| ## Ink Class — Core Rendering Engine (`ink.tsx:87+`) | ||
| src/ink/ink.tsx — Ink class, frame loop, selection (L87+) src/ink/reconciler.ts — React reconciler (createInstance, mutations) src/ink/renderer.ts — screen buffer, blit optimization (L1+) src/ink/terminal.ts — terminal capability detection src/ink/dom.ts — DOMElement, yogaNode, style application src/ink/hit-test.ts — click/hover dispatch, parent chain bubble src/ink/selection.ts — text selection state machine src/ink/components/ — 18 component files: App.tsx — root frame loop, mouse/keyboard Box.tsx, Text.tsx — layout containers ScrollBox.tsx — viewport with soft-wrap AlternateScreen.tsx — fullscreen mode Button.tsx, Link.tsx — interactive elements RawAnsi.tsx — pass-through ANSI codes ErrorOverview.tsx — error display panel | ||
| ```typescript | ||
| class Ink { | ||
| // Double buffering | ||
| frontFrame: Frame // current display | ||
| backFrame: Frame // previous (for diff) | ||
| ### Evidence Boundary | ||
| // Terminal state | ||
| terminalColumns: number | ||
| terminalRows: number | ||
| isUnmounted: boolean | ||
| isPaused: boolean | ||
| prevFrameContaminated: boolean // prevents wrong blit | ||
| 这一章的结论不是来自“项目用了 Ink”这种 package-level 推断,而是来自 src/ink/ink.tsx、src/ink/reconciler.ts、src/ink/renderer.ts、src/ink/hit-test.ts、src/ink/terminal.ts 这套自定义终端 runtime。它更接近“自研 terminal React renderer”,不是薄封装。 | ||
| // Selection | ||
| selectionState: SelectionState { | ||
| anchor, focus: {col, row} | ||
| isDragging: boolean | ||
| anchorSpan: word/line bounds | ||
| scrolledOff{Above,Below}: text accumulator | ||
| virtual{Anchor,Focus}Row: pre-clamp positions | ||
| lastPressHadAlt: boolean | ||
| } | ||
| ### Ink Class — 核心渲染引擎 (ink.tsx:87+) | ||
| // Search | ||
| searchHighlightQuery: string | ||
| searchPositions: SearchPosition[] | ||
| class Ink { // Double buffering frontFrame: Frame // current display backFrame: Frame // previous (for diff) // Terminal state terminalColumns: number terminalRows: number isUnmounted: boolean isPaused: boolean prevFrameContaminated: boolean // prevents wrong blit // Selection selectionState: SelectionState { anchor, focus: {col, row} isDragging: boolean anchorSpan: word/line bounds scrolledOff{Above,Below}: text accumulator virtual{Anchor,Focus}Row: pre-clamp positions lastPressHadAlt: boolean } // Search searchHighlightQuery: string searchPositions: SearchPosition[] // Mouse hoveredNodes: Set<DOMElement> // mode-1003 motion tracking // Cursor cursorDeclaration: CursorPos // for IME input positioning } ═══ onRender() — 主渲染循环 ═══ 1\. Reconciler commits → React tree updated 2\. Yoga layout → calculate positions 3\. renderNodeToOutput() → rasterize to char grid 4\. Selection/search overlay → highlight 5\. Diff against backFrame → emit ANSI patches 6\. Swap buffers (front ↔ back) | ||
| // Mouse | ||
| hoveredNodes: Set<DOMElement> // mode-1003 motion tracking | ||
| ### React Reconciler (reconciler.ts) | ||
| // Cursor | ||
| cursorDeclaration: CursorPos // for IME input positioning | ||
| } | ||
| createReconciler() from react-reconciler: createInstance(type, props): → Creates DOMElement → Applies styles to yogaNode → Sets yogaNode.setMeasureFunc() for text appendChild/insertBefore/removeChild: → DOM tree mutations → yogaNode child operations commitTextUpdate: → Text node value changes Props diff: → Only changed keys committed → style, textStyles, event handlers, attributes Event handlers: → Stored in _eventHandlers map → setEventHandler(node, eventName, handler) Styles: → applyStyles(yogaNode, styles) for Yoga directives → flexDirection, padding, margin, width, height, etc. Debug: CLAUDE_CODE_DEBUG_REPAINTS env getLastYogaMs() for profiling | ||
| // onRender() — main render loop: | ||
| // 1. Reconciler commits → React tree updated | ||
| // 2. Yoga layout → calculate positions | ||
| // 3. renderNodeToOutput() → rasterize to char grid | ||
| // 4. Selection/search overlay → highlight | ||
| // 5. Diff against backFrame → emit ANSI patches | ||
| // 6. Swap buffers (front ↔ back) | ||
| ``` | ||
| ### Renderer — 双缓冲 + Blit 优化 (renderer.ts) | ||
| ## React Reconciler (`reconciler.ts`) | ||
| createRenderer(node, stylePool) → Renderer function: 1\. Validate Yoga dimensions (finite, non-negative) 2\. Alt-screen clamp: height === terminalRows (enforced) 3\. renderNodeToOutput(): tree → character grid 4\. Blit optimization (L73-85): if absoluteRemoved OR prevFrameContaminated: → skip blit (copy stale data, full repaint) else: → blit from prevScreen (O(unchanged) fast path) → 只输出变化的 cells → 极大减少 ANSI write charCache: grapheme clustering 缓存,跨 frame 持久化 markDirty(): Yoga invalidation for scroll recovery | ||
| ```javascript | ||
| createReconciler() from react-reconciler: | ||
| ### Terminal 能力检测 (terminal.ts) | ||
| createInstance(type, props): | ||
| → Creates DOMElement | ||
| → Applies styles to yogaNode | ||
| → Sets yogaNode.setMeasureFunc() for text | ||
| isProgressReportingAvailable(): ConEmu, Ghostty 1.2.0+, iTerm2 3.6.6+ isSynchronizedOutputSupported(): iTerm, WezTerm, Ghostty, kitty, foot, Windows Terminal, VTE 0.68+ XTVERSION probe (L97-120): → Query xterm.js identity from terminal → CS > 0 q → reply on stdin isXtermJs(): VS Code, Cursor, Windsurf (via TERM_PROGRAM or XTVERSION) Kitty keyboard protocol (L130+): → CSI >1u support detection Hyperlink support (L160+): → OSC 8 terminal capability | ||
| appendChild/insertBefore/removeChild: | ||
| → DOM tree mutations | ||
| → yogaNode child operations | ||
| ### Hit Testing & Mouse Events (hit-test.ts) | ||
| commitTextUpdate: | ||
| → Text node value changes | ||
| hitTest(node, col, row) → DOMElement | null: → Uses nodeCache from renderNodeToOutput → Reversed child traversal (later siblings on top) → Returns node even without onClick (bubbles up) dispatchClick(root, col, row, cellIsBlank): 1\. hitTest → find deepest element 2\. Bubble ClickEvent up parent chain 3\. Click-to-focus: focus closest tabIndex ancestor 4\. Stop on stopImmediatePropagation() dispatchHover(root, col, row, hovered: Set): → Collect ancestors with onMouseEnter/Leave → Diff against previous hovered Set → Fire leave/enter (non-bubbling) → Mutate hovered Set in-place | ||
| Props diff: | ||
| → Only changed keys committed | ||
| → style, textStyles, event handlers, attributes | ||
| ### Text Selection State Machine (selection.ts) | ||
| Event handlers: | ||
| → Stored in _eventHandlers map | ||
| → setEventHandler(node, eventName, handler) | ||
| SelectionState lifecycle: startSelection(s, col, row) → set anchor, focus=null updateSelection(s, col, row) → set focus on drag finishSelection(s) → isDragging=false (keep visual) clearSelection(s) → full reset Word mode (L80+): WORD_CHAR = /[\p{L}\p{N}_/.\\-+~\\\\]/u → matches iTerm2 default word chars → double-click selects word Scroll tracking: scrolledOff{Above,Below}: text accumulator scrolledOff{Above,Below}SW: parallel softWrap bitmaps → 拖选时 rows 滚出 viewport 时保留 text → 接着拖回来时恢复 Alt modifier: lastPressHadAlt → block selection mode (column select) | ||
| Styles: | ||
| → applyStyles(yogaNode, styles) for Yoga directives | ||
| → flexDirection, padding, margin, width, height, etc. | ||
| ink React reconciler Yoga layout terminal UI double buffering | ||
| Debug: | ||
| CLAUDE_CODE_DEBUG_REPAINTS env | ||
| getLastYogaMs() for profiling | ||
| ``` | ||
| ## Renderer — Double Buffering + Blit Optimization (`renderer.ts`) | ||
| ```javascript | ||
| createRenderer(node, stylePool) → Renderer function: | ||
| 1. Validate Yoga dimensions (finite, non-negative) | ||
| 2. Alt-screen clamp: height === terminalRows (enforced) | ||
| 3. renderNodeToOutput(): tree → character grid | ||
| 4. Blit optimization (L73-85): | ||
| if absoluteRemoved OR prevFrameContaminated: | ||
| → skip blit (copy stale data, full repaint) | ||
| else: | ||
| → blit from prevScreen (O(unchanged) fast path) | ||
| → only output changed cells → minimizes ANSI writes | ||
| charCache: grapheme clustering cache, persists across frames | ||
| markDirty(): Yoga invalidation for scroll recovery | ||
| ``` | ||
| ## Terminal Capability Detection (`terminal.ts`) | ||
| ```javascript | ||
| isProgressReportingAvailable(): | ||
| ConEmu, Ghostty 1.2.0+, iTerm2 3.6.6+ | ||
| isSynchronizedOutputSupported(): | ||
| iTerm, WezTerm, Ghostty, kitty, foot, | ||
| Windows Terminal, VTE 0.68+ | ||
| XTVERSION probe (L97-120): | ||
| → Query xterm.js identity from terminal | ||
| → CSI > 0 q → reply on stdin | ||
| isXtermJs(): | ||
| VS Code, Cursor, Windsurf | ||
| (via TERM_PROGRAM or XTVERSION) | ||
| Kitty keyboard protocol (L130+): | ||
| → CSI >1u support detection | ||
| Hyperlink support (L160+): | ||
| → OSC 8 terminal capability | ||
| ``` | ||
| ## Hit Testing & Mouse Events (`hit-test.ts`) | ||
| ```javascript | ||
| hitTest(node, col, row) → DOMElement | null: | ||
| → Uses nodeCache from renderNodeToOutput | ||
| → Reversed child traversal (later siblings on top) | ||
| → Returns node even without onClick (bubbles up) | ||
| dispatchClick(root, col, row, cellIsBlank): | ||
| 1. hitTest → find deepest element | ||
| 2. Bubble ClickEvent up parent chain | ||
| 3. Click-to-focus: focus closest tabIndex ancestor | ||
| 4. Stop on stopImmediatePropagation() | ||
| dispatchHover(root, col, row, hovered: Set): | ||
| → Collect ancestors with onMouseEnter/Leave | ||
| → Diff against previous hovered Set | ||
| → Fire leave/enter (non-bubbling) | ||
| → Mutate hovered Set in-place | ||
| ``` | ||
| ## Text Selection State Machine (`selection.ts`) | ||
| ```javascript | ||
| SelectionState lifecycle: | ||
| startSelection(s, col, row) → set anchor, focus=null | ||
| updateSelection(s, col, row) → set focus on drag | ||
| finishSelection(s) → isDragging=false (keep visual) | ||
| clearSelection(s) → full reset | ||
| Word mode (L80+): | ||
| WORD_CHAR = /[\p{L}\p{N}_/.\-+~\\]/u | ||
| → matches iTerm2 default word chars | ||
| → double-click selects word | ||
| Scroll tracking: | ||
| scrolledOff{Above,Below}: text accumulator | ||
| scrolledOff{Above,Below}SW: parallel softWrap bitmaps | ||
| → preserve text as rows scroll out of viewport during drag | ||
| → restore when dragging back | ||
| Alt modifier: | ||
| lastPressHadAlt → block selection mode (column select) | ||
| ``` |
+273
-26
@@ -1,51 +0,298 @@ | ||
| [← Back to Index](index.html) | ||
| # 16. Task System | ||
| # 16\. Task System High | ||
| 7 polymorphic Task types — unified lifecycle management for background jobs. | ||
| 7 种多态 Task — 后台作业的统一生命周期管理 | ||
| ## Task Type Union (`Task.ts:8-15`) | ||
| R | ||
| ```typescript | ||
| TaskType = | ||
| | 'local_bash' // Shell command (Bash/PowerShell) | ||
| | 'local_agent' // Sub-agent (foreground/background) | ||
| | 'remote_agent' // Cloud CCR session | ||
| | 'in_process_teammate' // Teammate swarm agent | ||
| | 'local_workflow' // Workflow script (WORKFLOW_SCRIPTS) | ||
| | 'monitor_mcp' // MCP monitoring (MONITOR_TOOL) | ||
| | 'dream' // Auto-consolidation | ||
| ## Feature Reference | ||
| TaskStatus = 'pending' | 'running' | 'completed' | 'failed' | 'killed' | ||
| isTerminalTaskStatus(s) = s ∈ {completed, failed, killed} | ||
| ### Task Type Union — 7 种 (Task.ts:8-15) | ||
| // Task ID generation: | ||
| generateTaskId(type) → prefix + 8 random base36 chars | ||
| b = local_bash | ||
| a = local_agent | ||
| r = remote_agent | ||
| t = in_process_teammate | ||
| w = local_workflow | ||
| m = monitor_mcp | ||
| d = dream | ||
| s = main-session (special) | ||
| // 36^8 ≈ 2.8 trillion → resist brute-force symlink attacks | ||
| ``` | ||
| TaskType = | 'local_bash' // Shell 命令 (Bash/PowerShell) | 'local_agent' // Sub-agent (foreground/background) | 'remote_agent' // Cloud CCR session | 'in_process_teammate' // Teammate swarm agent | 'local_workflow' // Workflow script (WORKFLOW_SCRIPTS) | 'monitor_mcp' // MCP monitoring (MONITOR_TOOL) | 'dream' // Auto-consolidation TaskStatus = 'pending' | 'running' | 'completed' | 'failed' | 'killed' isTerminalTaskStatus(s) = s ∈ {completed, failed, killed} // Task ID generation: generateTaskId(type) → prefix + 8 random base36 chars b = local_bash a = local_agent r = remote_agent t = in_process_teammate w = local_workflow m = monitor_mcp d = dream s = main-session (special) // 36^8 ≈ 2.8 trillion → resist brute-force symlink attacks | ||
| ## TaskStateBase — Shared by All Tasks (`Task.ts:38-50`) | ||
| ### TaskStateBase — 所有 Task 共享 (Task.ts:38-50) | ||
| ```typescript | ||
| type TaskStateBase = { | ||
| id: string // generated task ID | ||
| type: TaskType | ||
| status: TaskStatus | ||
| description: string // human-readable | ||
| toolUseId?: string // originating tool call | ||
| startTime: number // epoch ms | ||
| endTime?: number | ||
| totalPausedMs?: number | ||
| outputFile?: string // disk output path | ||
| outputOffset?: number // read cursor | ||
| notified: boolean // notification sent to parent? | ||
| } | ||
| type TaskStateBase = { id: string // generated task ID type: TaskType status: TaskStatus description: string // human-readable toolUseId?: string // originating tool call startTime: number // epoch ms endTime?: number totalPausedMs?: number outputFile: string // disk output path (non-optional) outputOffset: number // read cursor (non-optional) notified: boolean // notification sent to parent? } interface Task { name: string type: TaskType kill(taskId: string, setAppState): Promise<void> } | ||
| interface Task { | ||
| name: string | ||
| type: TaskType | ||
| kill(taskId: string, setAppState): Promise<void> | ||
| } | ||
| ``` | ||
| ### Key Files | ||
| ## Key Files | ||
| src/Task.ts — TaskType, TaskStateBase, generateTaskId src/tasks.ts — feature-gated task registry src/tasks/types.ts — TaskState union, isBackgroundTask src/tasks/stopTask.ts — universal kill protocol src/tasks/pillLabel.ts — footer UI label src/tasks/LocalShellTask/ — bash/powershell background src/tasks/LocalAgentTask/LocalAgentTask.tsx — sub-agent lifecycle src/tasks/RemoteAgentTask/RemoteAgentTask.tsx — cloud sessions (5 types) src/tasks/InProcessTeammateTask/ — teammate swarm src/tasks/DreamTask/DreamTask.ts — auto-consolidation src/tasks/LocalMainSessionTask.ts — main session background | ||
| ``` | ||
| src/Task.ts — TaskType, TaskStateBase, generateTaskId | ||
| src/tasks.ts — feature-gated task registry | ||
| src/tasks/types.ts — TaskState union, isBackgroundTask | ||
| src/tasks/stopTask.ts — universal kill protocol | ||
| src/tasks/pillLabel.ts — footer UI label | ||
| src/tasks/LocalShellTask/ — bash/powershell background | ||
| src/tasks/LocalAgentTask/LocalAgentTask.tsx — sub-agent lifecycle | ||
| src/tasks/RemoteAgentTask/RemoteAgentTask.tsx — cloud sessions (5 types) | ||
| src/tasks/InProcessTeammateTask/ — teammate swarm | ||
| src/tasks/DreamTask/DreamTask.ts — auto-consolidation | ||
| src/tasks/LocalMainSessionTask.ts — main session background | ||
| ``` | ||
| ### LocalShellTask — Shell 后台命令 | ||
| --- | ||
| type LocalShellTaskState = TaskStateBase & { type: 'local_bash' command: string // shell command result?: { code: number, interrupted: boolean } shellCommand: ShellCommand | null completionStatusSentInAttachment: boolean lastReportedTotalLines: number // progress ring buffer isBackgrounded: boolean // Ctrl+B 转后台 agentId?: AgentId // orphan cleanup kind?: 'bash' | 'monitor' // UI variant } // 'bash' kind → command as label // 'monitor' kind → description as label | ||
| ## LocalShellTask — Shell Background Commands | ||
| ### RemoteAgentTask — 5 种远程类型 | ||
| ```typescript | ||
| type LocalShellTaskState = TaskStateBase & { | ||
| type: 'local_bash' | ||
| command: string | ||
| result?: { code: number, interrupted: boolean } | ||
| shellCommand: ShellCommand | null | ||
| completionStatusSentInAttachment: boolean | ||
| lastReportedTotalLines: number // progress ring buffer | ||
| isBackgrounded: boolean // Ctrl+B to background | ||
| agentId?: AgentId // orphan cleanup | ||
| kind?: 'bash' | 'monitor' // UI variant | ||
| } | ||
| type RemoteAgentTaskState = TaskStateBase & { type: 'remote_agent' remoteTaskType: RemoteTaskType remoteTaskMetadata?: AutofixPrRemoteTaskMetadata sessionId: string // for API calls command: string title: string todoList: SDKMessage[] log: SDKMessage[] isLongRunning?: boolean // don't mark complete pollStartedAt: number // timeout defense // UltraPlan specific: isUltraplan?: boolean ultraplanPhase?: 'finding' | 'verifying' | 'synthesizing' | 'needs_input' | 'plan_ready' // UltraReview specific: isRemoteReview?: boolean reviewProgress?: { stage: string bugsFound: number bugsVerified: number bugsRefuted: number } } RemoteTaskType = | 'remote-agent' // general remote execution | 'ultraplan' // multi-phase planning | 'ultrareview' // code review | 'autofix-pr' // PR auto-fix | 'background-pr' // background PR creation // Completion checking: registerCompletionChecker(type, checker) → Custom checker polled every tick → Returns notification text or null (keep polling) // Eligibility check: checkRemoteAgentEligibility(): Validates: logged in, remote env, git repo, git remote, GitHub app, org policy → formatPreconditionError() → actionable message | ||
| // 'bash' kind → command as label | ||
| // 'monitor' kind → description as label | ||
| ``` | ||
| ### InProcessTeammateTask — Swarm Agent | ||
| ## RemoteAgentTask — 5 Remote Types | ||
| type InProcessTeammateTaskState = TaskStateBase & { type: 'in_process_teammate' identity: TeammateIdentity prompt: string model?: string selectedAgent?: AgentDefinition abortController?: AbortController currentWorkAbortController?: AbortController // Permission (独立 cycle!) awaitingPlanApproval: boolean permissionMode: PermissionMode // State error?: string result?: AgentToolResult progress?: AgentProgress messages?: Message[] // 非 mailbox, 独立存储 inProgressToolUseIDs?: Set<string> pendingUserMessages: string[] spinnerVerb?, pastTenseVerb?: string // Lifecycle isIdle: boolean shutdownRequested: boolean onIdleCallbacks?: Function[] lastReportedToolCount: number lastReportedTokenCount: number } TeammateIdentity = { agentId: string // e.g. "researcher@my-team" agentName: string teamName: string color: string // UI color planModeRequired: boolean parentSessionId: string } // UI cap: TEAMMATE_MESSAGES_UI_CAP = 50 // Full history on disk, AppState only mirrors last 50 appendCappedMessage(prev, item) → keep last N | ||
| ```typescript | ||
| type RemoteAgentTaskState = TaskStateBase & { | ||
| type: 'remote_agent' | ||
| remoteTaskType: RemoteTaskType | ||
| remoteTaskMetadata?: AutofixPrRemoteTaskMetadata | ||
| sessionId: string // for API calls | ||
| command: string | ||
| title: string | ||
| todoList: SDKMessage[] | ||
| log: SDKMessage[] | ||
| isLongRunning?: boolean // don't mark complete | ||
| pollStartedAt: number // timeout defense | ||
| ### DreamTask — Auto-Consolidation | ||
| // UltraPlan specific: | ||
| isUltraplan?: boolean | ||
| ultraplanPhase?: 'finding' | 'verifying' | 'synthesizing' | ||
| | 'needs_input' | 'plan_ready' | ||
| type DreamTaskState = TaskStateBase & { type: 'dream' phase: 'starting' | 'updating' // flip on Edit/Write detect sessionsReviewing: number filesTouched: string[] turns: DreamTurn[] // max 30 recent abortController?: AbortController priorMtime: number // lock rollback on kill } DreamTurn = { text: string toolUseCount: number } Lifecycle: registerDreamTask(setAppState, opts) → taskId addDreamTurn(taskId, turn, touchedPaths, setAppState) completeDreamTask(taskId, setAppState) failDreamTask(taskId, setAppState) Kill: DreamTask.kill() → rollback consolidationLock mtime → allows retry on next session | ||
| // UltraReview specific: | ||
| isRemoteReview?: boolean | ||
| reviewProgress?: { | ||
| stage: string | ||
| bugsFound: number | ||
| bugsVerified: number | ||
| bugsRefuted: number | ||
| } | ||
| } | ||
| ### LocalMainSessionTask — 主 Session 后台化 | ||
| RemoteTaskType = | ||
| | 'remote-agent' // general remote execution | ||
| | 'ultraplan' // multi-phase planning | ||
| | 'ultrareview' // code review | ||
| | 'autofix-pr' // PR auto-fix | ||
| | 'background-pr' // background PR creation | ||
| // Ctrl+B twice → 主 session 转后台 registerMainSessionTask(description, setAppState, agent?, abortController?): → { taskId, abortSignal } → Uses type='local_agent' + agentType='main-session' → Task ID prefix 's' (vs 'a' for normal agents) → 前台 → 后台的逆向操作 | ||
| // Completion checking: | ||
| registerCompletionChecker(type, checker) | ||
| → Custom checker polled every tick | ||
| → Returns notification text or null (keep polling) | ||
| ### Task Lifecycle & 基础设施 | ||
| // Eligibility check: | ||
| checkRemoteAgentEligibility(): | ||
| Validates: logged in, remote env, git repo, | ||
| git remote, GitHub app, org policy | ||
| → formatPreconditionError() → actionable message | ||
| ``` | ||
| ═══ Task Registry ═══ registerTask(state, setAppState) — 注册到 AppState.tasks updateTaskState(taskId, setAppState, f) — atomic update stopTask(taskId, context) → StopTaskResult → Validates running status → Calls taskImpl.kill() → Bash: suppress "exit 137" notification → Emits taskTerminatedSdk event ═══ Disk Output ═══ 每个 task → /tmp/claude-code/outputs/ initTaskOutput(taskId) appendTaskOutput(taskId, data) getTaskOutputPath(taskId) evictTaskOutput(taskId) Agent: symlink to sidechain transcript ═══ UI Footer ═══ getPillLabel(tasks) → "1 shell", "2 agents", "◇ ultraplan" pillNeedsCta(tasks) → true if ultraplan awaiting approval PANEL_GRACE_MS: hide grace period after completion ═══ Feature-Gated Tasks (tasks.ts:9-10) ═══ MonitorMcpTask = feature('MONITOR_TOOL') ? require(...) : null LocalWorkflowTask = feature('WORKFLOW_SCRIPTS') ? require(...) : null // 两个目录都是 future implementation | ||
| ## InProcessTeammateTask — Swarm Agent | ||
| ### Task 进度追踪 — LocalAgentTask 详解 | ||
| ```typescript | ||
| type InProcessTeammateTaskState = TaskStateBase & { | ||
| type: 'in_process_teammate' | ||
| identity: TeammateIdentity | ||
| prompt: string | ||
| model?: string | ||
| selectedAgent?: AgentDefinition | ||
| abortController?: AbortController | ||
| currentWorkAbortController?: AbortController | ||
| ProgressTracker = { toolUseCount: number latestInputTokens: number // cumulative in API cumulativeOutputTokens: number // sum across turns recentActivities: ToolActivity[] // max 5 } ToolActivity = { toolName: string input: Record<string, unknown> activityDescription: string isSearch: boolean isRead: boolean } AgentProgress = { toolUseCount: number tokenCount: number lastActivity: string recentActivities: ToolActivity[] summary?: string } // 更新: updateProgressFromMessage(tracker, message): → Parse assistant turn → Increment counters → Extract recent tool activities // Activity Description: createActivityDescriptionResolver(tools): → (name, input) → "Reading src/main.tsx" → "Running git status" → "Searching for handleClick" | ||
| // Permission (independent cycle!) | ||
| awaitingPlanApproval: boolean | ||
| permissionMode: PermissionMode | ||
| Task.ts polymorphic background jobs lifecycle progress tracking | ||
| // State | ||
| error?: string | ||
| result?: AgentToolResult | ||
| progress?: AgentProgress | ||
| messages?: Message[] // not mailbox, stored independently | ||
| inProgressToolUseIDs?: Set<string> | ||
| pendingUserMessages: string[] | ||
| spinnerVerb?, pastTenseVerb?: string | ||
| // Lifecycle | ||
| isIdle: boolean | ||
| shutdownRequested: boolean | ||
| onIdleCallbacks?: Function[] | ||
| lastReportedToolCount: number | ||
| lastReportedTokenCount: number | ||
| } | ||
| TeammateIdentity = { | ||
| agentId: string // e.g. "researcher@my-team" | ||
| agentName: string | ||
| teamName: string | ||
| color: string // UI color | ||
| planModeRequired: boolean | ||
| parentSessionId: string | ||
| } | ||
| // UI cap: TEAMMATE_MESSAGES_UI_CAP = 50 | ||
| // Full history on disk, AppState only mirrors last 50 | ||
| appendCappedMessage(prev, item) → keep last N | ||
| ``` | ||
| ## DreamTask — Auto-Consolidation | ||
| ```typescript | ||
| type DreamTaskState = TaskStateBase & { | ||
| type: 'dream' | ||
| phase: 'starting' | 'updating' // flips on Edit/Write detection | ||
| sessionsReviewing: number | ||
| filesTouched: string[] | ||
| turns: DreamTurn[] // max 30 recent | ||
| abortController?: AbortController | ||
| priorMtime: number // lock rollback on kill | ||
| } | ||
| DreamTurn = { | ||
| text: string | ||
| toolUseCount: number | ||
| } | ||
| Lifecycle: | ||
| registerDreamTask(setAppState, opts) → taskId | ||
| addDreamTurn(taskId, turn, touchedPaths, setAppState) | ||
| completeDreamTask(taskId, setAppState) | ||
| failDreamTask(taskId, setAppState) | ||
| Kill: | ||
| DreamTask.kill() → rollback consolidationLock mtime | ||
| → allows retry on next session | ||
| ``` | ||
| ## LocalMainSessionTask — Main Session Backgrounding | ||
| ```javascript | ||
| // Ctrl+B twice → main session goes to background | ||
| registerMainSessionTask(description, setAppState, agent?, abortController?): | ||
| → { taskId, abortSignal } | ||
| → Uses type='local_agent' + agentType='main-session' | ||
| → Task ID prefix 's' (vs 'a' for normal agents) | ||
| → Reverse operation: foreground → background | ||
| ``` | ||
| ## Task Infrastructure | ||
| ```javascript | ||
| // Task Registry | ||
| registerTask(state, setAppState) — register to AppState.tasks | ||
| updateTaskState(taskId, setAppState, f) — atomic update | ||
| stopTask(taskId, context) → StopTaskResult | ||
| → Validates running status | ||
| → Calls taskImpl.kill() | ||
| → Bash: suppress "exit 137" notification | ||
| → Emits taskTerminatedSdk event | ||
| // Disk Output | ||
| // Each task → /tmp/claude-code/outputs/ | ||
| initTaskOutput(taskId) | ||
| appendTaskOutput(taskId, data) | ||
| getTaskOutputPath(taskId) | ||
| evictTaskOutput(taskId) | ||
| Agent: symlink to sidechain transcript | ||
| // UI Footer | ||
| getPillLabel(tasks) → "1 shell", "2 agents", "◇ ultraplan" | ||
| pillNeedsCta(tasks) → true if ultraplan awaiting approval | ||
| PANEL_GRACE_MS: hide grace period after completion | ||
| // Feature-Gated Tasks (tasks.ts:9-10) | ||
| MonitorMcpTask = feature('MONITOR_TOOL') ? require(...) : null | ||
| LocalWorkflowTask = feature('WORKFLOW_SCRIPTS') ? require(...) : null | ||
| // Both directories are future implementation | ||
| ``` | ||
| ## Progress Tracking — LocalAgentTask Detail | ||
| ```typescript | ||
| ProgressTracker = { | ||
| toolUseCount: number | ||
| latestInputTokens: number // cumulative in API | ||
| cumulativeOutputTokens: number // sum across turns | ||
| recentActivities: ToolActivity[] // max 5 | ||
| } | ||
| ToolActivity = { | ||
| toolName: string | ||
| input: Record<string, unknown> | ||
| activityDescription: string | ||
| isSearch: boolean | ||
| isRead: boolean | ||
| } | ||
| AgentProgress = { | ||
| toolUseCount: number | ||
| tokenCount: number | ||
| lastActivity: string | ||
| recentActivities: ToolActivity[] | ||
| summary?: string | ||
| } | ||
| // Updates: | ||
| updateProgressFromMessage(tracker, message): | ||
| → Parse assistant turn | ||
| → Increment counters | ||
| → Extract recent tool activities | ||
| // Activity Description: | ||
| createActivityDescriptionResolver(tools): | ||
| → (name, input) → "Reading src/main.tsx" | ||
| → "Running git status" | ||
| → "Searching for handleClick" | ||
| ``` |
+192
-24
@@ -1,47 +0,215 @@ | ||
| [← Back to Index](index.html) | ||
| # 17. System Prompt Assembly | ||
| # 17\. System Prompt Assembly Critical | ||
| CLAUDE.md user context + static prompt skeleton + registry-managed dynamic sections. | ||
| CLAUDE.md user context + static prompt skeleton + registry-managed dynamic sections | ||
| ## Key Files | ||
| R | ||
| ``` | ||
| src/constants/prompts.ts — getSystemPrompt() main builder (L444-540) | ||
| src/constants/systemPromptSections.ts — section registry + cached/uncached | ||
| src/utils/claudemd.ts — CLAUDE.md loading chain (L798-950) | ||
| src/context.ts — getUserContext / getSystemContext | ||
| src/QueryEngine.ts — final assembly (L287-327) | ||
| src/main.tsx — CLI --system-prompt param (L1343-1388) | ||
| src/memdir/memdir.ts — loadMemoryPrompt() memory section | ||
| ``` | ||
| ## Feature Reference | ||
| --- | ||
| ### Key Files | ||
| ## CLAUDE.md / Rules Discovery Chain (`claudemd.ts:798-950`) | ||
| src/constants/prompts.ts — getSystemPrompt() 主构建 (L444-540) src/constants/systemPromptSections.ts — section 注册 + cached/uncached src/utils/claudemd.ts — CLAUDE.md 加载链 (L798-950) src/context.ts — getUserContext / getSystemContext src/QueryEngine.ts — 最终拼装 (L287-327) src/main.tsx — CLI --system-prompt 参数 (L1343-1388) src/memdir/memdir.ts — loadMemoryPrompt() memory section | ||
| Priority (low → high, later overrides earlier): | ||
| ### Evidence Boundary | ||
| ``` | ||
| 1. Managed /etc/claude-code/CLAUDE.md + rules | ||
| 2. User ~/.claude/CLAUDE.md + ~/.claude/rules/*.md | ||
| 3. Project CLAUDE.md / .claude/CLAUDE.md / .claude/rules/*.md | ||
| → traverse upward from CWD | ||
| 4. Local CLAUDE.local.md | ||
| 5. Additional dirs --add-dir paths (env gated) | ||
| src/utils/claudemd.ts:798-950 证明的是 `getUserContext()` 里的 CLAUDE.md / rules / local 文件发现链;src/constants/prompts.ts:444-577 证明 static prompt skeleton 与 dynamic section registry;src/context.ts:154-183 证明 user context 里真正注入的是 `claudeMd` \+ `currentDate`,而不是把所有内容都塞进 system prompt。 | ||
| Note: | ||
| - MEMORY.md / topic memories come from loadMemoryPrompt(), a separate memory path | ||
| - They appear in the system prompt's memory section, not via this scan chain | ||
| ``` | ||
| ### CLAUDE.md / Rules 发现链 (claudemd.ts:798-950) | ||
| Key functions: | ||
| 优先级 (低 → 高, 后者 override 前者): 1\. Managed /etc/claude-code/CLAUDE.md + rules 2\. User ~/.claude/CLAUDE.md + ~/.claude/rules/*.md 3\. Project CLAUDE.md / .claude/CLAUDE.md / .claude/rules/*.md → 从 CWD 向上遍历 4\. Local CLAUDE.local.md 5\. Additional dirs --add-dir 下的 CLAUDE.md (env gated) 注意: \- MEMORY.md / topic memories 来自 loadMemoryPrompt(),是另一条 memory path \- 它们会在 system prompt 的 memory section 中出现,不属于 claudemd.ts 这条扫描链 ═══ 关键函数 ═══ getMemoryFiles() (L798) — memoized, 返回 MemoryFileInfo[] processMemoryFile() (L700) — 递归处理 + @include (max depth 5) extractIncludePathsFromTokens() (L398-446) — @path, @./path, @~/path ═══ @include 机制 ═══ 支持: @path, @./relative, @~/home, @/absolute Fragment stripping: @path#heading 循环引用防护: MAX_INCLUDE_DEPTH = 5 ═══ 条件规则 (Frontmatter) ═══ parseFrontmatterPaths() (L233-254): YAML frontmatter 中的 glob patterns → 只在匹配文件时才加载该 CLAUDE.md 内容 processConditionedMdRules() (L1380-1395): → 按 target file path 过滤条件规则 | ||
| ```javascript | ||
| getMemoryFiles() (L798) — memoized, returns MemoryFileInfo[] | ||
| processMemoryFile() (L700) — recursive processing + @include (max depth 5) | ||
| extractIncludePathsFromTokens() (L398-446) — @path, @./path, @~/path | ||
| ### Static Skeleton + Dynamic Registry (prompts.ts:444-577) | ||
| // @include mechanism: | ||
| // Supports: @path, @./relative, @~/home, @/absolute | ||
| // Fragment stripping: @path#heading | ||
| // Circular reference protection: MAX_INCLUDE_DEPTH = 5 | ||
| getSystemPrompt(tools, model, additionalDirs, mcpClients): ═══ STATIC SECTIONS (cache_control: global ephemeral) ═══ [1] getSimpleIntroSection() — "You are an interactive agent..." [2] getSimpleSystemSection() — Tools 列表, permissions, tags [3] getSimpleDoingTasksSection() — Coding best practices [4] getActionsSection() — When to ask for confirmation [5] getUsingYourToolsSection() — Tool guidance (File*, Bash, Glob, Grep) [6] getSimpleToneAndStyleSection() — 语气/风格指导 [7] getOutputEfficiencySection() — Brief output guidance ═══ CACHE BOUNDARY ═══ __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ ↑ 这个 marker 上面: cache_control scope='global' ↓ 这个 marker 下面: cache_control scope='user' ═══ DYNAMIC REGISTRY (conditional, count not fixed) ═══ session_guidance memory ant_model_override // ant-only env_info_simple language output_style mcp_instructions // uncached section scratchpad frc summarize_tool_results numeric_length_anchors // ant-only token_budget // feature gated brief // KAIROS gated 结论: \- 不是固定“12 段”常量 \- 静态骨架相对稳定 \- 动态 section 数量随 feature / entrypoint 变化 | ||
| // Conditional rules (frontmatter): | ||
| parseFrontmatterPaths() (L233-254): | ||
| YAML frontmatter with glob patterns | ||
| → only load this CLAUDE.md content when matched file is active | ||
| processConditionedMdRules() (L1380-1395): | ||
| → filter conditional rules by target file path | ||
| ``` | ||
| ### Cache Boundary — 为什么分两段? | ||
| ## Static Skeleton + Dynamic Registry (`prompts.ts:444-577`) | ||
| SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__' // Anthropic API prompt caching 机制: // cache_control: { type: 'ephemeral', scope: 'global' } // → 跨用户共享 (同 model + 同 tools = cache hit) // → 所有人的 "You are an interactive agent..." 是一样的 // // cache_control: { type: 'ephemeral', scope: 'user' } // → 用户级缓存 (同一用户的 memory/env 在 session 内稳定) // 设计意图: // Static sections (1-7) → 不变, 全球缓存 → 极高 hit rate // Dynamic sections (8-16) → 每 session 变, 用户级缓存 // MCP instructions 必须 UNCACHED: // → server 可能 mid-session connect/disconnect // → delta 机制: 只在变化时重新计算 | ||
| ```javascript | ||
| getSystemPrompt(tools, model, additionalDirs, mcpClients): | ||
| ### Section 注册机制 (systemPromptSections.ts) | ||
| // STATIC SECTIONS (cache_control: global ephemeral) | ||
| [1] getSimpleIntroSection() — "You are an interactive agent..." | ||
| [2] getSimpleSystemSection() — Tools list, permissions, tags | ||
| [3] getSimpleDoingTasksSection() — Coding best practices | ||
| [4] getActionsSection() — When to ask for confirmation | ||
| [5] getUsingYourToolsSection() — Tool guidance (File*, Bash, Glob, Grep) | ||
| [6] getSimpleToneAndStyleSection() — Tone/style guidance | ||
| [7] getOutputEfficiencySection() — Brief output guidance | ||
| type SystemPromptSection = { name: string compute: () => Promise<string> cacheBreak: boolean // false=cached, true=uncached } // Cached section (大多数): systemPromptSection(name, computeFn): → memoized, 第一次计算后缓存 → cacheBreak = false // Uncached section (MCP instructions 等): DANGEROUS_uncachedSystemPromptSection(name, computeFn): → 每次调用都重新计算 → cacheBreak = true → 命名里的 DANGEROUS_ 提醒: 这会破坏 cache! // 解析: resolveSystemPromptSections(sections): → Promise.all() 并行计算所有 sections → memoization 保证相同 section 不重复计算 | ||
| // CACHE BOUNDARY | ||
| __SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ | ||
| // ↑ above: cache_control scope='global' | ||
| // ↓ below: cache_control scope='user' | ||
| ### 最终拼装 — QueryEngine (QueryEngine.ts:287-327) | ||
| // DYNAMIC REGISTRY (conditional, count varies) | ||
| session_guidance | ||
| memory | ||
| ant_model_override // ant-only | ||
| env_info_simple | ||
| language | ||
| output_style | ||
| mcp_instructions // UNCACHED section | ||
| scratchpad | ||
| frc | ||
| summarize_tool_results | ||
| numeric_length_anchors // ant-only | ||
| token_budget // feature gated | ||
| brief // KAIROS gated | ||
| ``` | ||
| submitMessage() 内: 1\. 获取 3 个组件: defaultPrompt = await getSystemPrompt(tools, model, dirs, mcp) userCtx = await getUserContext() // { claudeMd, currentDate } sysCtx = await getSystemContext() // { gitStatus, cacheBreaker } 2\. 拼装 system prompt: systemPrompt = [ customSystemPrompt ?? defaultPrompt, // --system-prompt 覆盖 memoryMechanicsPrompt?, // memory 操作指令 appendSystemPrompt?, // --append-system-prompt 追加 ] 3\. 注入 context (query.ts:630): messages = prependUserContext(messagesForQuery, userCtx) // → 在 messages 前插入 { claudeMd, currentDate } systemPrompt = appendSystemContext(systemPrompt, sysCtx) // → 在 prompt 后追加 { gitStatus, cacheBreaker } // 结果: // System prompt: // customPrompt ?? defaultSystemPrompt // + memoryMechanicsPrompt? (仅 custom prompt + auto-mem override) // + appendSystemPrompt? // User context: // [claudeMd] + [currentDate] // System context: // [gitStatus] + [cacheBreaker] | ||
| > The structure is NOT a fixed constant number of sections. Static skeleton is relatively stable; dynamic section count varies by feature flags and entrypoint. | ||
| ### Tool Descriptions 注入 | ||
| ## Cache Boundary Design | ||
| ═══ Built-in Tools ═══ 每个 tool 有: description(): Promise<string> — model-facing 短描述 prompt(): Promise<string> — 详细使用指导 → 通过 API tools 参数传递 (NOT system prompt) ═══ MCP Tools ═══ getMcpInstructions() (prompts.ts:540-565): → 格式化为 "# MCP Server Instructions" section → 每个 server 的 instructions 单独一段 → 描述 cap: MAX_MCP_DESCRIPTION_LENGTH = 2048 chars → 超长截断 + "…[truncated]" → 这是 UNCACHED section (servers 可能变) ═══ Agent 定义 ═══ agentListContext (prompts.ts:498+): → Built-in + custom agents 列表 → 注入到 session_guidance section → 或作为 message attachment (tengu_agent_list_attach) | ||
| ```javascript | ||
| SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__' | ||
| ### CLI 覆盖路径 (main.tsx:1343-1388) | ||
| // Anthropic API prompt caching: | ||
| // cache_control: { type: 'ephemeral', scope: 'global' } | ||
| // → shared across users (same model + same tools = cache hit) | ||
| // → "You are an interactive agent..." is identical for all users | ||
| // | ||
| // cache_control: { type: 'ephemeral', scope: 'user' } | ||
| // → user-level cache (same user's memory/env stable within session) | ||
| \--system-prompt "text" → 完全替换 system prompt \--system-prompt-file path.txt → 从文件读取替换 \--append-system-prompt "text" → 追加到末尾 \--append-system-prompt-file path → 从文件读取追加 // customSystemPrompt 存在时: // defaultPrompt 被完全忽略 (static sections 全部丢弃) // 只保留 custom + memoryMechanics + append // appendSystemPrompt: // 在所有默认 sections 之后追加 // 不破坏 cache boundary | ||
| // Design intent: | ||
| // Static sections (1-7) → unchanging, global cache → very high hit rate | ||
| // Dynamic sections (8+) → vary per session, user-level cache | ||
| system prompt claude.md cache boundary instructions context assembly | ||
| // MCP instructions MUST BE UNCACHED: | ||
| // → server may connect/disconnect mid-session | ||
| // → delta mechanism: only recompute when changed | ||
| ``` | ||
| ## Section Registry (`systemPromptSections.ts`) | ||
| ```typescript | ||
| type SystemPromptSection = { | ||
| name: string | ||
| compute: () => Promise<string> | ||
| cacheBreak: boolean // false=cached, true=uncached | ||
| } | ||
| // Cached section (most): | ||
| systemPromptSection(name, computeFn): | ||
| → memoized, cached after first computation | ||
| → cacheBreak = false | ||
| // Uncached section (MCP instructions etc.): | ||
| DANGEROUS_uncachedSystemPromptSection(name, computeFn): | ||
| → recomputed on every call | ||
| → cacheBreak = true | ||
| → DANGEROUS_ prefix is a warning: this breaks cache! | ||
| // Resolution: | ||
| resolveSystemPromptSections(sections): | ||
| → Promise.all() computes all sections in parallel | ||
| → memoization ensures same section not recomputed | ||
| ``` | ||
| ## Final Assembly — QueryEngine (`QueryEngine.ts:287-327`) | ||
| ```javascript | ||
| // Inside submitMessage(): | ||
| // 1. Get 3 components: | ||
| defaultPrompt = await getSystemPrompt(tools, model, dirs, mcp) | ||
| userCtx = await getUserContext() // { claudeMd, currentDate } | ||
| sysCtx = await getSystemContext() // { gitStatus, cacheBreaker } | ||
| // 2. Assemble system prompt: | ||
| systemPrompt = [ | ||
| customSystemPrompt ?? defaultPrompt, // --system-prompt override | ||
| memoryMechanicsPrompt?, // memory operation instructions | ||
| appendSystemPrompt?, // --append-system-prompt append | ||
| ] | ||
| // 3. Inject context (query.ts:630): | ||
| messages = prependUserContext(messagesForQuery, userCtx) | ||
| // → insert { claudeMd, currentDate } before messages | ||
| systemPrompt = appendSystemContext(systemPrompt, sysCtx) | ||
| // → append { gitStatus, cacheBreaker } after prompt | ||
| // Result: | ||
| // System prompt: | ||
| // customPrompt ?? defaultSystemPrompt | ||
| // + memoryMechanicsPrompt? (only with custom prompt + auto-mem override) | ||
| // + appendSystemPrompt? | ||
| // User context: | ||
| // [claudeMd] + [currentDate] | ||
| // System context: | ||
| // [gitStatus] + [cacheBreaker] | ||
| ``` | ||
| ## Tool Description Injection | ||
| ```javascript | ||
| // Built-in Tools: | ||
| // Each tool has: | ||
| // description(): Promise<string> — model-facing short description | ||
| // prompt(): Promise<string> — detailed usage guidance | ||
| // → passed via API tools parameter (NOT system prompt) | ||
| // MCP Tools: | ||
| getMcpInstructions() (prompts.ts:540-565): | ||
| → formatted as "# MCP Server Instructions" section | ||
| → each server's instructions in a separate block | ||
| → description cap: MAX_MCP_DESCRIPTION_LENGTH = 2048 chars | ||
| → over-length truncated + "…[truncated]" | ||
| → this is UNCACHED section (servers may change) | ||
| // Agent definitions: | ||
| agentListContext (prompts.ts:498+): | ||
| → built-in + custom agents list | ||
| → injected into session_guidance section | ||
| → or as message attachment (tengu_agent_list_attach) | ||
| ``` | ||
| ## CLI Override Paths (`main.tsx:1343-1388`) | ||
| ``` | ||
| --system-prompt "text" → completely replace system prompt | ||
| --system-prompt-file path.txt → read replacement from file | ||
| --append-system-prompt "text" → append to end | ||
| --append-system-prompt-file path → read append from file | ||
| // When customSystemPrompt exists: | ||
| // defaultPrompt is completely ignored (all static sections discarded) | ||
| // only custom + memoryMechanics + append are kept | ||
| // appendSystemPrompt: | ||
| // appended after all default sections | ||
| // does not break cache boundary | ||
| ``` |
+229
-26
@@ -1,51 +0,254 @@ | ||
| [← Back to Index](index.html) | ||
| # 18. MCP Server Lifecycle | ||
| # 18\. MCP Server Lifecycle High | ||
| 7 typed config scopes + plugin/Claude.ai overlays → 6 transport enums → connect/discover/reconnect/Elicitation. | ||
| 7 个 typed config scopes + plugin/Claude.ai overlays → 6 个 transport enums → 连接/发现/重连/Elicitation | ||
| ## Key Files | ||
| R | ||
| ``` | ||
| src/services/mcp/config.ts — config discovery (7 typed scopes + overlays) | ||
| src/services/mcp/types.ts — ConfigScope, Transport types | ||
| src/services/mcp/client.ts — connectToServer (L1094-1670) | ||
| fetchToolsForClient (L1743-1853) | ||
| tool conversion + caching | ||
| reconnection + cleanup | ||
| src/services/mcp/elicitationHandler.ts — elicitation register + complete | ||
| src/services/mcp/useManageMCPConnections.ts — batch connect + lifecycle | ||
| src/tools/MCPTool/MCPTool.ts — MCP tool template (buildTool) | ||
| src/state/AppStateStore.ts — mcp: {clients, tools, resources} | ||
| ``` | ||
| ## Feature Reference | ||
| --- | ||
| ### Key Files | ||
| ## Config Discovery — 7 Typed Scopes + Overlays (`config.ts`) | ||
| src/services/mcp/config.ts — config discovery (7 typed scopes + overlays) src/services/mcp/types.ts — ConfigScope, Transport types src/services/mcp/client.ts — connectToServer (L595-1670) ⚠️ fetchToolsForClient (L1743-1853) tool conversion + caching reconnection + cleanup src/services/mcp/elicitationHandler.ts — elicitation register + complete src/services/mcp/useManageMCPConnections.ts — batch connect + lifecycle src/tools/MCPTool/MCPTool.ts — MCP tool template (buildTool) src/state/AppStateStore.ts — mcp: {clients, tools, resources} | ||
| ``` | ||
| ConfigScope = 'local' | 'user' | 'project' | 'dynamic' | ||
| | 'enterprise' | 'claudeai' | 'managed' | ||
| ### Config Discovery — 7 个 typed scopes + overlays (config.ts) | ||
| Core typed scopes: | ||
| 1. managed — getManagedFilePath()/managed-mcp.json (enterprise policy) | ||
| 2. user — ~/.mcp.json (user global) | ||
| 3. project — .mcp.json in CWD (project-level) | ||
| 4. dynamic — provided at runtime (SDK/API) | ||
| 5. enterprise — policy-enforced servers (MDM) | ||
| 6. claudeai — Claude.ai proxy/integration configs | ||
| 7. local — local persisted state / toggles | ||
| ConfigScope = 'local' | 'user' | 'project' | 'dynamic' | 'enterprise' | 'claudeai' | 'managed' 核心 typed scopes: 1\. managed — getManagedFilePath()/managed-mcp.json (企业策略) 2\. user — ~/.mcp.json (用户全局) 3\. project — .mcp.json in CWD (项目级) 4\. dynamic — runtime 提供 (SDK/API) 5\. enterprise — policy-enforced servers (MDM) 6\. claudeai — Claude.ai proxy/integration configs 7\. local — local persisted state / toggles 额外 overlay sources: \- plugin MCP servers → getPluginMcpServers() \- Claude.ai eligible configs → fetchClaudeAIMcpConfigsIfEligible() 注意: plugin 不是 ConfigScope enum 成员 它是在 config assembly 时额外 merge 进最终 server set | ||
| Additional overlay sources: | ||
| - plugin MCP servers → getPluginMcpServers() | ||
| - Claude.ai eligible configs → fetchClaudeAIMcpConfigsIfEligible() | ||
| ### 6 个 Transport Enums + 1 个内部 in-process 适配 (types.ts + client.ts:1139-1315) | ||
| Note: | ||
| plugin is NOT a ConfigScope enum member. | ||
| It is extra-merged into the final server set during config assembly. | ||
| (src/services/mcp/config.ts:1148-1155) | ||
| (src/utils/plugins/mcpPluginIntegration.ts:589-623) | ||
| ``` | ||
| Transport = 'stdio' | 'sse' | 'sse-ide' | 'http' | 'ws' | 'sdk' ═══ stdio (L1300-1315) ═══ StdioClientTransport({ command, args, env, stderr }) → spawn child process → JSON-RPC over stdin/stdout → stderr captured for error display ═══ sse (L1139-1191) ═══ SSEClientTransport({ url, authProvider, timeout, fetch }) → Server-Sent Events connection → Separate EventSource fetch wrapper ═══ sse-ide ═══ IDE extension 专用 SSE 变体 → transport enum 里存在,但语义是 internal IDE bridge path ═══ http (L1195-1260) ═══ StreamableHTTPClientTransport({ url, auth, headers, proxy, timeout }) → HTTP streaming (Streamable HTTP transport) ═══ ws (L1100-1140) ═══ WebSocketTransport({ url, tlsOptions, proxy }) → WebSocket bidirectional connection ═══ sdk ═══ 指向 SDK-side server reference → 不是新网络协议,而是通过 SDK registry 解析的 transport variant ═══ internal in-process adapter (L1270-1285) ═══ Chrome MCP + Computer Use MCP → 直接在 Node.js 进程内运行 → 这是 client.ts 的内部适配路径,不是 Transport enum 新成员 | ||
| ## 6 Transport Enums + 1 Internal In-Process Adapter (`types.ts` + `client.ts:1139-1315`) | ||
| ### Evidence Boundary | ||
| ``` | ||
| Transport = 'stdio' | 'sse' | 'sse-ide' | 'http' | 'ws' | 'sdk' | ||
| src/services/mcp/types.ts:10-21 定义 typed scopes 与 transport enums;src/services/mcp/config.ts:1148-1155 证明 plugin MCP servers 是额外并行装载;src/utils/plugins/mcpPluginIntegration.ts:589-623 证明 plugin servers 最终被加上 plugin scope metadata 后再并入 config assembly。 | ||
| stdio (L1300-1315): | ||
| StdioClientTransport({ command, args, env, stderr }) | ||
| → spawn child process | ||
| → JSON-RPC over stdin/stdout | ||
| → stderr captured for error display | ||
| ### connectToServer() — 完整连接流程 (client.ts:595, NOT :1094) | ||
| sse (L1139-1191): | ||
| SSEClientTransport({ url, authProvider, timeout, fetch }) | ||
| → Server-Sent Events connection | ||
| → Separate EventSource fetch wrapper | ||
| connectToServer = memoize(async (name, serverRef, stats)): 1\. Transport Setup (L1139-1315): → 根据 transport type 创建 transport instance → 注入 auth provider, proxy, timeout wrapper 2\. Client Creation (L1330-1345): const client = new Client({ capabilities: { roots: {}, elicitation: {} } }) → ListRootsRequestSchema handler → file://[cwd] → Default elicitation handler → cancel 3\. Connection (L1346-1375): await client.connect(transport) → Timeout: 30s default (MCP_TIMEOUT env override) → Request timeout: 60s default (MCP_REQUEST_TIMEOUT_MS) 4\. Success or Failure: → { type: 'connected', client, capabilities, instructions } → { type: 'failed', error } // Memoization: // Cache key: ${name}-${jsonStringify(serverRef)} // → 防止并发连接同一 server | ||
| sse-ide: | ||
| IDE extension dedicated SSE variant | ||
| → exists in transport enum, semantic is internal IDE bridge path | ||
| ### Tool Discovery — fetchToolsForClient (client.ts:1743-1853) | ||
| http (L1195-1260): | ||
| StreamableHTTPClientTransport({ url, auth, headers, proxy, timeout }) | ||
| → HTTP streaming (Streamable HTTP transport) | ||
| fetchToolsForClient = memoizeWithLRU(async (client)): 1\. RPC 调用: client.client.request( { method: 'tools/list' }, ListToolsResultSchema ) → { tools: [{ name, description, inputSchema, annotations, _meta }] } 2\. Tool 转换 (L1768-1850): name: mcp__[serverName]__[toolName] // 命名规则 mcpInfo: { serverName, toolName } // 权限检查用 // Metadata from _meta: searchHint: _meta['anthropic/searchHint'] alwaysLoad: _meta['anthropic/alwaysLoad'] === true // Description cap: MAX_MCP_DESCRIPTION_LENGTH = 2048 → 超长截断 + "…[truncated]" // Annotations → safety declarations: readOnlyHint → isConcurrencySafe() destructiveHint → isDestructive() openWorldHint → isOpenWorld() // Schema: inputJSONSchema: 直接从 MCP server 传递 (not Zod!) 3\. LRU Cache: keyed by server name cleared on client.onclose (connection drop) | ||
| ws (L1100-1140): | ||
| WebSocketTransport({ url, tlsOptions, proxy }) | ||
| → WebSocket bidirectional connection | ||
| ### Batch Connection & Lifecycle (useManageMCPConnections.ts) | ||
| sdk: | ||
| Points to SDK-side server reference | ||
| → Not a new network protocol; transport variant resolved via SDK registry | ||
| // Batch sizes: MCP_SERVER_CONNECTION_BATCH_SIZE = 3 // stdio (local) MCP_REMOTE_SERVER_CONNECTION_BATCH_SIZE = 20 // sse/http (remote) useManageMCPConnections(dynamicConfig?, isStrictConfig?): → Orchestrates connect/disconnect/toggle of ALL servers → Listens for MCP notifications: ToolListChangedNotification → re-fetch tools ResourceListChangedNotification → re-fetch resources PromptListChangedNotification → re-fetch prompts → Reconnection with exponential backoff // AppState.mcp: { clients: MCPServerConnection[] tools: Tool[] // all MCP tools (merged) commands: Command[] // MCP-sourced commands resources: Record<string, ServerResource[]> } | ||
| Internal in-process adapter (L1270-1285): | ||
| Chrome MCP + Computer Use MCP | ||
| → run directly inside Node.js process | ||
| → internal adapter path in client.ts, NOT a new Transport enum member | ||
| ``` | ||
| ### Error Recovery & Reconnection (client.ts:1375-1530) | ||
| ## `connectToServer()` — Full Connection Flow (`client.ts:1094`) | ||
| ═══ Connection Drop Detection ═══ client.onerror handler (L1279-1369): → ECONNRESET, ETIMEDOUT, EPIPE → terminal error → consecutiveConnectionErrors 追踪 → 3 consecutive failures → close (L1228, 1357-1359) ═══ Session Expiry ═══ HTTP 404 + JSON-RPC code -32001 → close immediately (L1313-1324) SSE reconnect exhaustion → "Maximum reconnection attempts" (L1336-1343) ═══ Connection Close Cleanup (L1381-1398) ═══ 1\. 清除 memoization cache: connectToServer.cache.delete(key) 2\. 清除 fetch caches: fetchToolsForClient.cache.delete(name) fetchResourcesForClient.cache.delete(name) fetchCommandsForClient.cache.delete(name) 3\. Fire originalOnclose() hook ═══ Stdio Graceful Shutdown (L1422-1530) ═══ SIGINT → wait 100ms SIGTERM → wait 400ms (if still alive) SIGKILL → final fallback Total cleanup: ~600ms timeout | ||
| ```javascript | ||
| connectToServer = memoize(async (name, serverRef, stats)): | ||
| ### Elicitation — MCP Server 向用户提问 | ||
| 1. Transport Setup (L1139-1315): | ||
| → create transport instance based on transport type | ||
| → inject auth provider, proxy, timeout wrapper | ||
| // elicitationHandler.ts registerElicitationHandler(client, serverName, setAppState) (L62): Request Handler (L72-105): 1\. Receive ElicitRequestSchema (mode: 'form' | 'url') 2\. First try elicitation hooks (L78-86) 3\. If no hook response → queue to AppState (L93-115) 4\. Set AbortSignal listener + waiting UI state Completion (L130-150): Listens for ElicitationCompleteNotificationSchema → marks completed: true → fires hooks // 完整流程: // MCP server 需要用户输入 (e.g. OAuth 授权) // → server 发 ElicitRequest // → Claude Code 显示 UI (form 或 URL redirect) // → 用户完成 → ElicitationComplete // → server 继续执行 | ||
| 2. Client Creation (L1330-1345): | ||
| const client = new Client({ | ||
| capabilities: { roots: {}, elicitation: {} } | ||
| }) | ||
| → ListRootsRequestSchema handler → file://[cwd] | ||
| → Default elicitation handler → cancel | ||
| ### Tool Refresh — mid-session 变化 | ||
| 3. Connection (L1346-1375): | ||
| await client.connect(transport) | ||
| → Timeout: 30s default (MCP_TIMEOUT env override) | ||
| → Request timeout: 60s default (MCP_REQUEST_TIMEOUT_MS) | ||
| // Tools 可以 mid-session 变化: 1\. Server 发 ToolListChangedNotification → useManageMCPConnections 监听 → re-fetch: fetchToolsForClient(client) (LRU cache miss) → update AppState.mcp.tools 2\. refreshTools() callback (ToolUseContext.options): → query loop mid-query 调用 → 返回最新 tools 列表 → 下一轮 API call 使用新 tools 3\. System prompt mcp_instructions section: → UNCACHED (DANGEROUS_uncachedSystemPromptSection) → 每次 API call 重新计算 → 捕捉 server connect/disconnect delta // 结果: MCP server 可以在 session 中间: // - 新增 tools → 下一轮可用 // - 移除 tools → 下一轮消失 // - 修改 descriptions → 下一轮更新 | ||
| 4. Success or Failure: | ||
| → { type: 'connected', client, capabilities, instructions } | ||
| → { type: 'failed', error } | ||
| MCP transport tool discovery elicitation reconnection | ||
| // Memoization: | ||
| // Cache key: ${name}-${jsonStringify(serverRef)} | ||
| // → prevents concurrent connections to same server | ||
| ``` | ||
| ## Tool Discovery — `fetchToolsForClient` (`client.ts:1743-1853`) | ||
| ```javascript | ||
| fetchToolsForClient = memoizeWithLRU(async (client)): | ||
| 1. RPC call: | ||
| client.client.request( | ||
| { method: 'tools/list' }, | ||
| ListToolsResultSchema | ||
| ) | ||
| → { tools: [{ name, description, inputSchema, annotations, _meta }] } | ||
| 2. Tool conversion (L1768-1850): | ||
| name: mcp__[serverName]__[toolName] // naming convention | ||
| mcpInfo: { serverName, toolName } // for permission checks | ||
| // Metadata from _meta: | ||
| searchHint: _meta['anthropic/searchHint'] | ||
| alwaysLoad: _meta['anthropic/alwaysLoad'] === true | ||
| // Description cap: | ||
| MAX_MCP_DESCRIPTION_LENGTH = 2048 | ||
| → over-length truncated + "…[truncated]" | ||
| // Annotations → safety declarations: | ||
| readOnlyHint → isConcurrencySafe() | ||
| destructiveHint → isDestructive() | ||
| openWorldHint → isOpenWorld() | ||
| // Schema: | ||
| inputJSONSchema: passed directly from MCP server (not Zod!) | ||
| 3. LRU Cache: | ||
| keyed by server name | ||
| cleared on client.onclose (connection drop) | ||
| ``` | ||
| ## Batch Connection & Lifecycle (`useManageMCPConnections.ts`) | ||
| ```javascript | ||
| // Batch sizes: | ||
| MCP_SERVER_CONNECTION_BATCH_SIZE = 3 // stdio (local) | ||
| MCP_REMOTE_SERVER_CONNECTION_BATCH_SIZE = 20 // sse/http (remote) | ||
| useManageMCPConnections(dynamicConfig?, isStrictConfig?): | ||
| → Orchestrates connect/disconnect/toggle of ALL servers | ||
| → Listens for MCP notifications: | ||
| ToolListChangedNotification → re-fetch tools | ||
| ResourceListChangedNotification → re-fetch resources | ||
| PromptListChangedNotification → re-fetch prompts | ||
| → Reconnection with exponential backoff | ||
| // AppState.mcp: | ||
| { | ||
| clients: MCPServerConnection[] | ||
| tools: Tool[] // all MCP tools (merged) | ||
| commands: Command[] // MCP-sourced commands | ||
| resources: Record<string, ServerResource[]> | ||
| } | ||
| ``` | ||
| ## Error Recovery & Reconnection (`client.ts:1375-1530`) | ||
| ```javascript | ||
| // Connection Drop Detection | ||
| client.onerror handler (L1279-1369): | ||
| → ECONNRESET, ETIMEDOUT, EPIPE → terminal error | ||
| → consecutiveConnectionErrors tracking | ||
| → 3 consecutive failures → close (L1228, 1357-1359) | ||
| // Session Expiry | ||
| HTTP 404 + JSON-RPC code -32001 → close immediately (L1313-1324) | ||
| SSE reconnect exhaustion → "Maximum reconnection attempts" (L1336-1343) | ||
| // Connection Close Cleanup (L1381-1398) | ||
| 1. Clear memoization cache: | ||
| connectToServer.cache.delete(key) | ||
| 2. Clear fetch caches: | ||
| fetchToolsForClient.cache.delete(name) | ||
| fetchResourcesForClient.cache.delete(name) | ||
| fetchCommandsForClient.cache.delete(name) | ||
| 3. Fire originalOnclose() hook | ||
| // Stdio Graceful Shutdown (L1422-1530) | ||
| SIGINT → wait 100ms | ||
| SIGTERM → wait 400ms (if still alive) | ||
| SIGKILL → final fallback | ||
| Total cleanup: ~600ms timeout | ||
| ``` | ||
| ## Elicitation — MCP Server Requesting User Input (`elicitationHandler.ts`) | ||
| ```javascript | ||
| registerElicitationHandler(client, serverName, setAppState) (L62): | ||
| Request Handler (L72-105): | ||
| 1. Receive ElicitRequestSchema (mode: 'form' | 'url') | ||
| 2. First try elicitation hooks (L78-86) | ||
| 3. If no hook response → queue to AppState (L93-115) | ||
| 4. Set AbortSignal listener + waiting UI state | ||
| Completion (L130-150): | ||
| Listens for ElicitationCompleteNotificationSchema | ||
| → marks completed: true | ||
| → fires hooks | ||
| // Full flow: | ||
| // MCP server needs user input (e.g. OAuth authorization) | ||
| // → server sends ElicitRequest | ||
| // → Claude Code displays UI (form or URL redirect) | ||
| // → user completes → ElicitationComplete | ||
| // → server continues execution | ||
| ``` | ||
| ## Tool Refresh — Mid-Session Changes | ||
| ```javascript | ||
| // Tools can change mid-session: | ||
| 1. Server sends ToolListChangedNotification | ||
| → useManageMCPConnections listens | ||
| → re-fetch: fetchToolsForClient(client) (LRU cache miss) | ||
| → update AppState.mcp.tools | ||
| 2. refreshTools() callback (ToolUseContext.options): | ||
| → called mid-query in query loop | ||
| → returns latest tools list | ||
| → next API call uses new tools | ||
| 3. System prompt mcp_instructions section: | ||
| → UNCACHED (DANGEROUS_uncachedSystemPromptSection) | ||
| → recomputed on every API call | ||
| → captures server connect/disconnect delta | ||
| // Result: MCP servers can mid-session: | ||
| // - add tools → available next turn | ||
| // - remove tools → gone next turn | ||
| // - modify descriptions → updated next turn | ||
| ``` |
+359
-30
@@ -1,59 +0,388 @@ | ||
| [← Back to Index](index.html) | ||
| # 19. End-to-End Request Flow | ||
| # 19\. End-to-End Request Flow Critical | ||
| A complete trace of a real request through all modules. | ||
| 一个真实请求穿过所有模块的完整 trace | ||
| **Scenario:** "Find the bug in Button click state and fix it" | ||
| R | ||
| > **Trace note:** This uses real file paths from the repo (e.g. `src/ink/components/Button.tsx`, `src/ink/hit-test.ts`, `src/bootstrap/state.ts`). It is a **representative trace** — tool_use IDs, thinking text, and intermediate message blocks are illustrative samples reconstructed from `src/QueryEngine.ts` and streaming/tool executor behavior, not a literal recorded transcript. | ||
| ## Scenario: "搜索 Button 点击态的 bug 并修复它" | ||
| --- | ||
| ### Trace Boundary | ||
| ## Phase 0: Session Bootstrap | ||
| 这一页现在只使用当前 repo 中真实存在的文件路径,比如 src/ink/components/Button.tsx、src/ink/hit-test.ts、src/bootstrap/state.ts。但它仍然是 **representative trace** :tool_use ID、thinking 文案、部分中间 message block 是根据 src/QueryEngine.ts 与 streaming/tool executor 行为重建的说明性样本,不是一次真实录下来的 transcript 回放。 | ||
| ``` | ||
| User launches claude | ||
| ### Phase 0: Session Bootstrap | ||
| Entry Point (main.tsx): | ||
| → Parse CLI args (--model, --system-prompt, --allow-*, etc.) | ||
| → Bootstrap state (bootstrap/state.ts) | ||
| → sessionId = UUID, originalCwd, projectRoot | ||
| → Load settings (08-config-system): | ||
| → user + project + local + managed + flag → merged config | ||
| → MCP servers connect (18-mcp-lifecycle): | ||
| → Batch: 3 stdio + 20 remote concurrently | ||
| → fetchToolsForClient() → register tools | ||
| → Load CLAUDE.md (17-system-prompt): | ||
| → managed → user → project → local → team → memdir | ||
| → @include expansion (max depth 5) | ||
| → Build system prompt: | ||
| → 7 static sections (global cache) | ||
| → BOUNDARY | ||
| → 9 dynamic sections (user cache) | ||
| → Load memory (12-memory-dataflow): | ||
| → MEMORY.md → 200 lines into system prompt | ||
| → findRelevantMemories() → Sonnet side-query → top 5 topics | ||
| → Assemble tool pool (02-tool-system): | ||
| → built-in tools + MCP tools | ||
| → filterToolsByDenyRules() | ||
| → isDeferredTool() → deferred tools hidden, ToolSearchTool shown | ||
| → Render REPL (15-ink-terminal-ui): | ||
| → React reconciler → Yoga layout → terminal output | ||
| 用户启动 claude ═══ Entry Point (main.tsx) ═══ → Parse CLI args (--model, --system-prompt, --allow-*, etc.) → Bootstrap state (bootstrap/state.ts) → sessionId = UUID, originalCwd, projectRoot → Load settings (08-config-system): → user + project + local + managed + flag → merged config → MCP servers connect (18-mcp-lifecycle): → Batch: 3 stdio + 20 remote concurrently → fetchToolsForClient() → register tools → Load CLAUDE.md (17-system-prompt): → managed → user → project → local → team → memdir → @include expansion (max depth 5) → Build system prompt: → 7 static sections (global cache) → BOUNDARY → 9 dynamic sections (user cache) → Load memory (03-memory, 12-memory-dataflow): → MEMORY.md → 200 lines into system prompt → findRelevantMemories() → Sonnet side-query → top 5 topics → Assemble tool pool (02-tool-system): → built-in tools + MCP tools → filterToolsByDenyRules() → isDeferredTool() → deferred tools hidden, ToolSearchTool shown → Render REPL (15-ink-terminal-ui): → React reconciler → Yoga layout → terminal output [Session ready — waiting for user input] | ||
| [Session ready — waiting for user input] | ||
| ``` | ||
| ### Phase 1: User Input → Message Recording | ||
| ## Phase 1: User Input → Message Recording | ||
| 用户输入: "搜索 Button 点击态的 bug 并修复它" ═══ Input Processing (QueryEngine.ts:305-420) ═══ processUserInput({ input, mode: 'prompt' }) → Check slash command? NO → regular message → Create user Message object: { role: 'user', content: '搜索 Button 点击态的 bug 并修复它' } ═══ Transcript Recording (10-session) ═══ saveMessage(sessionId, message) → Append to {projectDir}/{sessionId}.jsonl → ⚠️ BEFORE query loop! (resumability guarantee) ═══ Permission Context (07-permission-model) ═══ getAppState().toolPermissionContext: mode: 'default' alwaysAllowRules: { Bash: {ruleContent: 'prefix:grep'} } alwaysDenyRules: {} [Message recorded, entering query loop] | ||
| ``` | ||
| User input: "Find the bug in Button click state and fix it" | ||
| ### Phase 2: Context Assembly (query.ts Phase 1) | ||
| Input Processing (QueryEngine.ts:305-420): | ||
| processUserInput({ input, mode: 'prompt' }) | ||
| → Check slash command? NO → regular message | ||
| → Create user Message object: | ||
| { role: 'user', content: 'Find the bug in Button click state and fix it' } | ||
| ═══ Boundary Snapping ═══ getMessagesAfterCompactBoundary(messages) → 第一轮,无 compact boundary → 使用全部 messages ═══ Tool Result Budget ═══ applyToolResultBudget() → 第一轮,无 tool results → skip ═══ Microcompact ═══ deps.microcompact() → 第一轮,token count 很低 → skip ═══ Autocompact Check ═══ deps.autocompact() → token count << threshold → skip ═══ Attachment Collection (pre-turn) ═══ Memory prefetch: pendingMemoryPrefetch 已 settled → 5 relevant memories injected as user context Skill discovery: collectSkillDiscoveryPrefetch() → 匹配的 skills 注入 (if any) ═══ Final Message Assembly ═══ messages = [ { user_context: claudeMd + currentDate }, // from getUserContext() { user: '搜索 Button 点击态的 bug 并修复它' }, { memory_attachments: [relevant_memories] }, // from findRelevantMemories() ] systemPrompt = [ ...static_sections, // intro, system, tasks, actions, tools, tone, output BOUNDARY, ...dynamic_sections, // session, memory, env, lang, style, mcp, ... gitStatus, // from getSystemContext() ] | ||
| Transcript Recording (10-session): | ||
| saveMessage(sessionId, message) | ||
| → Append to {projectDir}/{sessionId}.jsonl | ||
| → ⚠️ BEFORE query loop! (resumability guarantee) | ||
| ### Phase 3: API Call → Model Streaming (query.ts Phase 2) | ||
| Permission Context (07-permission-model): | ||
| getAppState().toolPermissionContext: | ||
| mode: 'default' | ||
| alwaysAllowRules: { Bash: {ruleContent: 'prefix:grep'} } | ||
| alwaysDenyRules: {} | ||
| ═══ Pre-API Setup ═══ yield 'stream_request_start' Model selection: 'claude-sonnet-4-20250514' (default) Create StreamingToolExecutor ═══ API Call (09-streaming) ═══ for await (const msg of claude({ messages, systemPrompt, tools, model, thinkingConfig, querySource: 'repl_main_thread' })) ═══ Model Response (streaming) ═══ Model 返回 assistant message: { content: [ { type: 'thinking', thinking: '用户要找 Button 点击态的 bug...' }, { type: 'text', text: '让我先搜索相关代码。' }, { type: 'tool_use', id: 'tu_1', name: 'Grep', input: { pattern: 'handleClick', include: '**/*.ts' } }, { type: 'tool_use', id: 'tu_2', name: 'Grep', input: { pattern: 'handleClick', include: '**/*.tsx' } }, ] } ═══ During Streaming ═══ tool_use blocks extracted → feed to StreamingToolExecutor Grep.isConcurrencySafe() → TRUE for both → ⚡ 两个 Grep 立刻开始并行执行! | ||
| [Message recorded, entering query loop] | ||
| ``` | ||
| ### Phase 4: Tool Execution — Grep ×2 并行 (query.ts Phase 4) | ||
| ## Phase 2: Context Assembly (`query.ts` Phase 1) | ||
| ═══ Concurrency Decision (02-tool-system) ═══ StreamingToolExecutor.canExecuteTool(): executing = [] (empty) Grep isConcurrencySafe = true → EXECUTE Grep isConcurrencySafe = true + all executing are safe → EXECUTE → 两个 Grep 并行执行 ═══ Permission Check per tool (07-permission-model) ═══ checkPermissionsAndCallTool(Grep, input): Stage 1: Zod validation → ✓ (pattern: string, include: string) Stage 2: validateInput → ✓ Stage 3: PreToolUse hooks → no hooks matched Stage 4: Permission decision: Grep.isReadOnly() = true → auto-allow (read-only tools) Stage 5: Execute! ═══ Hook Execution (04-hook-engine) ═══ PreToolUse event fired: → No matching hooks for 'Grep' pattern → Pass through ═══ Grep Execution ═══ Grep tu_1: found matches in src/ink/focus.ts, src/ink/hit-test.ts, ... Grep tu_2: found match in src/ink/components/Button.tsx → Results as tool_result messages ═══ Tool Result Budget (02-tool-system) ═══ maybePersistLargeToolResult(): size = 3KB → under 50KB threshold → inline (no persist) ═══ Post-Tool Hooks ═══ PostToolUse event: no matching hooks → pass through [Tool results collected: 2 Grep results, ready for Turn 2] | ||
| ``` | ||
| Boundary Snapping: | ||
| getMessagesAfterCompactBoundary(messages) | ||
| → First turn, no compact boundary → use all messages | ||
| ### Phase 5: Turn 2 — Model 分析 + Edit | ||
| Tool Result Budget: | ||
| applyToolResultBudget() | ||
| → First turn, no tool results → skip | ||
| ═══ State Reassembly (query.ts Phase 6) ═══ state = { messages: [...original, assistant_turn1, grep_result1, grep_result2], turnCount: 2, } → LOOP BACK to Phase 1 (Context Assembly) ═══ Context Assembly (Turn 2) ═══ Microcompact: token count still low → skip Autocompact: still under threshold → skip ═══ API Call (Turn 2) ═══ Model 看到 Grep 结果,返回: { content: [ { type: 'thinking', thinking: 'Button 的 keyboard path 会设置 isActive,但 click path 只调 onAction()...' }, { type: 'text', text: '找到了!点击路径没有像键盘路径那样设置 active 态。' }, { type: 'tool_use', id: 'tu_3', name: 'Read', input: { file_path: 'src/ink/components/Button.tsx', offset: 92, limit: 28 } }, ] } ═══ Read 执行 ═══ Read.isReadOnly() = true → auto-allow Read.isConcurrencySafe() = true → 读取 Button.tsx:92-119 | ||
| Microcompact: | ||
| deps.microcompact() | ||
| → First turn, token count very low → skip | ||
| ### Phase 6: Turn 3 — Edit (Permission 触发!) | ||
| Autocompact Check: | ||
| deps.autocompact() | ||
| → token count << threshold → skip | ||
| ═══ API Call (Turn 3) ═══ Model 返回: { content: [ { type: 'text', text: '问题在第42行...' }, { type: 'tool_use', id: 'tu_4', name: 'Edit', input: { file_path: 'src/ink/components/Button.tsx', old_string: 't5 = _e => {\n onAction();\n };', new_string: 't5 = _e => {\n setIsActive(true);\n onAction();\n if (activeTimer.current) {\n clearTimeout(activeTimer.current);\n }\n activeTimer.current = setTimeout(_temp, 100, setIsActive);\n };' } }, ] } ═══ Permission Check — Edit (07-permission-model) ═══ checkPermissionsAndCallTool(Edit, input): Stage 1: Zod validation → ✓ Stage 2: validateInput → ✓ (file exists, old_string found) Stage 3: PreToolUse hooks → no match Stage 4: Permission decision: Edit.isReadOnly() = FALSE Edit.isDestructive() = FALSE mode = 'default' → check allow/deny rules → No allow rule for Edit → behavior: 'ask' ═══ 🔔 PERMISSION PROMPT ═══ [Terminal UI shows]: ┌─────────────────────────────────────┐ │ Edit src/ink/components/Button.tsx │ │ │ │ - t5 = _e => { │ │ - onAction(); │ │ - }; │ │ + t5 = _e => { │ │ + setIsActive(true); │ │ + onAction(); ... │ │ │ │ [Allow] [Allow Always] [Deny] │ └─────────────────────────────────────┘ 用户点 [Allow] → permissionDecision = { behavior: 'allow', classification: 'once' } ═══ Edit 执行 ═══ Edit tool applies patch → file modified → tool_result: { data: 'Successfully edited file' } | ||
| Attachment Collection (pre-turn): | ||
| Memory prefetch: pendingMemoryPrefetch already settled | ||
| → 5 relevant memories injected as user context | ||
| Skill discovery: collectSkillDiscoveryPrefetch() | ||
| → matched skills injected (if any) | ||
| ### Phase 7: Turn 4 — Completion + Stop Hooks | ||
| Final Message Assembly: | ||
| messages = [ | ||
| { user_context: claudeMd + currentDate }, // from getUserContext() | ||
| { user: 'Find the bug in Button click state...' }, | ||
| { memory_attachments: [relevant_memories] }, // from findRelevantMemories() | ||
| ] | ||
| systemPrompt = [ | ||
| ...static_sections, // intro, system, tasks, actions, tools, tone, output | ||
| BOUNDARY, | ||
| ...dynamic_sections, // session, memory, env, lang, style, mcp, ... | ||
| gitStatus, // from getSystemContext() | ||
| ] | ||
| ``` | ||
| ═══ API Call (Turn 4) ═══ Model 返回: { content: [ { type: 'text', text: '已修复 Button 点击路径不显示 active 态的问题。现在 click 和 keyboard activation 一致。' } ] // ← 无 tool_use! → needsFollowUp = false } ═══ Post-Stream: Stop Hooks (query.ts Phase 3, Path 5) ═══ handleStopHooks(): 1\. Memory Extraction (03-memory, 12-memory-dataflow): → hasMemoryWritesSince(cursor)? NO → turns since last extract ≥ interval? → trigger! → runForkedAgent() (cache-safe fork): \- Reads conversation, existing memories \- Decides: 这次修复经历值得记为 feedback memory \- FileWrite ~/.claude/.../memory/feedback_bugfix.md: "Ink Button click path 应和 keyboard path 保持一致的 active-state feedback" 2\. Auto-Dream check (12-memory-dataflow): → hoursSince < 24 → skip gate 1 3\. Custom hooks: → No matching SessionEnd/Stop hooks ═══ Token Budget Check ═══ → No TOKEN_BUDGET feature → skip ═══ RETURN { reason: 'completed' } ═══ [Query loop exits normally] | ||
| ## Phase 3: API Call → Model Streaming (`query.ts` Phase 2) | ||
| ### Phase 8: Post-Completion | ||
| ``` | ||
| Pre-API Setup: | ||
| yield 'stream_request_start' | ||
| Model selection: 'claude-sonnet-4-20250514' (default) | ||
| Create StreamingToolExecutor | ||
| ═══ QueryEngine Result (QueryEngine.ts:1100-1170) ═══ yield { type: 'result', usage: { input: 12,450, output: 1,823, cache_read: 8,100 }, cost: $0.032, session_id: 'sess_abc123', turns: 4 } ═══ Transcript Final State (10-session) ═══ {sessionId}.jsonl 包含: line 1: { role: 'user', content: '搜索 Button 点击态...' } line 2: { role: 'assistant', content: [thinking, text, tool_use(Grep×2)] } line 3: { role: 'user', content: [tool_result(Grep×2)] } line 4: { role: 'assistant', content: [thinking, text, tool_use(Read)] } line 5: { role: 'user', content: [tool_result(Read)] } line 6: { role: 'assistant', content: [text, tool_use(Edit)] } line 7: { role: 'user', content: [tool_result(Edit)] } line 8: { role: 'assistant', content: [text] } // Progress messages NOT persisted ═══ Memory State ═══ 新增 memory: ~/.claude/.../memory/feedback_bugfix.md → "Button click path should mirror keyboard active-state behavior" MEMORY.md index: → 下次 session 自动加载 ═══ Cost Tracking ═══ totalCostUSD += 0.032 modelUsage['claude-sonnet-4'] += { input: 12450, output: 1823 } [等待下一个用户输入] | ||
| API Call (09-streaming): | ||
| for await (const msg of claude({ | ||
| messages, systemPrompt, tools, | ||
| model, thinkingConfig, querySource: 'repl_main_thread' | ||
| })) | ||
| ### 模块交叉点 — 每个 Turn 中哪些模块参与 | ||
| Model Response (streaming): | ||
| { | ||
| content: [ | ||
| { type: 'thinking', thinking: 'User wants to find the Button click state bug...' }, | ||
| { type: 'text', text: 'Let me search for relevant code.' }, | ||
| { type: 'tool_use', id: 'tu_1', name: 'Grep', | ||
| input: { pattern: 'handleClick', include: '**/*.ts' } }, | ||
| { type: 'tool_use', id: 'tu_2', name: 'Grep', | ||
| input: { pattern: 'handleClick', include: '**/*.tsx' } }, | ||
| ] | ||
| } | ||
| T1(Grep) T2(Read) T3(Edit) T4(Done) ──────── ──────── ──────── ──────── 17 System Prompt ✓ build cache cache cache 08 Config ✓ load cache cache cache 03 Memory ✓ recall ── ── extract 07 Permission Grep→auto Read→auto Edit→ASK ── 04 Hooks pre/post pre/post pre/post stop 02 Tool System Grep×2∥ Read Edit ── 09 Streaming yes yes yes yes 05 Compression check check check check 10 Session record record record record 13 Query Loop all 6 ph all 6 ph all 6 ph phase 1-3 15 Ink UI render render PROMPT! render 12 Memory Dataflow ── ── ── write ch2 Legend: ∥=parallel →=decision cache=memoized ──=not involved | ||
| During Streaming: | ||
| tool_use blocks extracted → feed to StreamingToolExecutor | ||
| Grep.isConcurrencySafe() → TRUE for both | ||
| → ⚡ Both Greps start executing in parallel immediately! | ||
| ``` | ||
| ### 如果中途出错? — Recovery Paths | ||
| ## Phase 4: Tool Execution — Grep ×2 Parallel (`query.ts` Phase 4) | ||
| ═══ 如果 Turn 2 触发 413 (Prompt Too Long) ═══ Phase 3, Path 2: contextCollapse.recoverFromOverflow() → 或 Path 3: reactiveCompact.tryReactiveCompact() → 压缩后 continue → 重新 API call ═══ 如果 API 返回 529 (Overloaded) ═══ withRetry() generator: → Extract retry-after header → Short retry: wait + fast-mode ON → Max 10 retries, foreground source 优先 ═══ 如果用户 Ctrl+C 中途取消 ═══ abortController.signal fires: → During streaming: drain + synthetic tool_results + return → During tool execution: drain executor + return → Abort cascades: parent → all children (01-agent-spawning) ═══ 如果 Edit tool 被用户 Deny ═══ permissionDecision = { behavior: 'deny' } → tool_result with is_error: true → Model sees error → 可能改用其他方式或询问用户 ═══ 如果 session crash ═══ conversationRecovery.ts: → Load transcript up to last complete pair → Offer resume with context (10-session) | ||
| ``` | ||
| Concurrency Decision (02-tool-system): | ||
| StreamingToolExecutor.canExecuteTool(): | ||
| executing = [] (empty) | ||
| Grep isConcurrencySafe = true → EXECUTE | ||
| Grep isConcurrencySafe = true + all executing are safe → EXECUTE | ||
| → Two Greps run in parallel | ||
| end-to-end request flow cross-module trace | ||
| Permission Check per tool (07-permission-model): | ||
| checkPermissionsAndCallTool(Grep, input): | ||
| Stage 1: Zod validation → ✓ (pattern: string, include: string) | ||
| Stage 2: validateInput → ✓ | ||
| Stage 3: PreToolUse hooks → no hooks matched | ||
| Stage 4: Permission decision: | ||
| Grep.isReadOnly() = true → auto-allow (read-only tools) | ||
| Stage 5: Execute! | ||
| Hook Execution (04-hook-engine): | ||
| PreToolUse event fired: | ||
| → No matching hooks for 'Grep' pattern | ||
| → Pass through | ||
| Grep Execution: | ||
| Grep tu_1: found matches in src/ink/focus.ts, src/ink/hit-test.ts, ... | ||
| Grep tu_2: found match in src/ink/components/Button.tsx | ||
| → Results as tool_result messages | ||
| Tool Result Budget (02-tool-system): | ||
| maybePersistLargeToolResult(): | ||
| size = 3KB → under 50KB threshold → inline (no persist) | ||
| Post-Tool Hooks: | ||
| PostToolUse event: no matching hooks → pass through | ||
| [Tool results collected: 2 Grep results, ready for Turn 2] | ||
| ``` | ||
| ## Phase 5: Turn 2 — Model Analysis + Read | ||
| ``` | ||
| State Reassembly (query.ts Phase 6): | ||
| state = { | ||
| messages: [...original, assistant_turn1, grep_result1, grep_result2], | ||
| turnCount: 2, | ||
| } | ||
| → LOOP BACK to Phase 1 (Context Assembly) | ||
| Context Assembly (Turn 2): | ||
| Microcompact: token count still low → skip | ||
| Autocompact: still under threshold → skip | ||
| API Call (Turn 2): | ||
| Model sees Grep results, returns: | ||
| { | ||
| content: [ | ||
| { type: 'thinking', thinking: 'Button keyboard path sets isActive, but click path only calls onAction()...' }, | ||
| { type: 'text', text: 'Found it! The click path does not set active state like the keyboard path does.' }, | ||
| { type: 'tool_use', id: 'tu_3', name: 'Read', | ||
| input: { file_path: 'src/ink/components/Button.tsx', offset: 92, limit: 28 } }, | ||
| ] | ||
| } | ||
| Read execution: | ||
| Read.isReadOnly() = true → auto-allow | ||
| Read.isConcurrencySafe() = true | ||
| → reads Button.tsx:92-119 | ||
| ``` | ||
| ## Phase 6: Turn 3 — Edit (Permission Prompt!) | ||
| ``` | ||
| API Call (Turn 3): | ||
| Model returns: | ||
| { | ||
| content: [ | ||
| { type: 'text', text: 'The problem is on line 42...' }, | ||
| { type: 'tool_use', id: 'tu_4', name: 'Edit', | ||
| input: { | ||
| file_path: 'src/ink/components/Button.tsx', | ||
| old_string: 't5 = _e => {\n onAction();\n };', | ||
| new_string: 't5 = _e => {\n setIsActive(true);\n onAction();\n ...\n };' | ||
| } | ||
| }, | ||
| ] | ||
| } | ||
| Permission Check — Edit (07-permission-model): | ||
| checkPermissionsAndCallTool(Edit, input): | ||
| Stage 1: Zod validation → ✓ | ||
| Stage 2: validateInput → ✓ (file exists, old_string found) | ||
| Stage 3: PreToolUse hooks → no match | ||
| Stage 4: Permission decision: | ||
| Edit.isReadOnly() = FALSE | ||
| Edit.isDestructive() = FALSE | ||
| mode = 'default' → check allow/deny rules | ||
| → No allow rule for Edit → behavior: 'ask' | ||
| 🔔 PERMISSION PROMPT: | ||
| ┌─────────────────────────────────────┐ | ||
| │ Edit src/ink/components/Button.tsx │ | ||
| │ │ | ||
| │ - t5 = _e => { │ | ||
| │ - onAction(); │ | ||
| │ - }; │ | ||
| │ + t5 = _e => { │ | ||
| │ + setIsActive(true); │ | ||
| │ + onAction(); ... │ | ||
| │ │ | ||
| │ [Allow] [Allow Always] [Deny] │ | ||
| └─────────────────────────────────────┘ | ||
| User clicks [Allow] | ||
| → permissionDecision = { behavior: 'allow', classification: 'once' } | ||
| Edit execution: | ||
| Edit tool applies patch → file modified | ||
| → tool_result: { data: 'Successfully edited file' } | ||
| ``` | ||
| ## Phase 7: Turn 4 — Completion + Stop Hooks | ||
| ``` | ||
| API Call (Turn 4): | ||
| Model returns: | ||
| { | ||
| content: [ | ||
| { type: 'text', text: 'Fixed the bug where Button click path did not show active state. Now click and keyboard activation are consistent.' } | ||
| ] | ||
| // ← no tool_use! → needsFollowUp = false | ||
| } | ||
| Post-Stream: Stop Hooks (query.ts Phase 3, Path 5): | ||
| handleStopHooks(): | ||
| 1. Memory Extraction (12-memory-dataflow): | ||
| → hasMemoryWritesSince(cursor)? NO | ||
| → turns since last extract ≥ interval? → trigger! | ||
| → runForkedAgent() (cache-safe fork): | ||
| - Reads conversation, existing memories | ||
| - Decides: this fix experience is worth a feedback memory | ||
| - FileWrite ~/.claude/.../memory/feedback_bugfix.md: | ||
| "Ink Button click path should mirror keyboard active-state feedback" | ||
| 2. Auto-Dream check (12-memory-dataflow): | ||
| → hoursSince < 24 → skip gate 1 | ||
| 3. Custom hooks: | ||
| → No matching SessionEnd/Stop hooks | ||
| Token Budget Check: | ||
| → No TOKEN_BUDGET feature → skip | ||
| RETURN { reason: 'completed' } | ||
| [Query loop exits normally] | ||
| ``` | ||
| ## Phase 8: Post-Completion | ||
| ``` | ||
| QueryEngine Result (QueryEngine.ts:1100-1170): | ||
| yield { | ||
| type: 'result', | ||
| usage: { input: 12_450, output: 1_823, cache_read: 8_100 }, | ||
| cost: $0.032, | ||
| session_id: 'sess_abc123', | ||
| turns: 4 | ||
| } | ||
| Transcript Final State (10-session): | ||
| {sessionId}.jsonl contains: | ||
| line 1: { role: 'user', content: 'Find the bug in Button click state...' } | ||
| line 2: { role: 'assistant', content: [thinking, text, tool_use(Grep×2)] } | ||
| line 3: { role: 'user', content: [tool_result(Grep×2)] } | ||
| line 4: { role: 'assistant', content: [thinking, text, tool_use(Read)] } | ||
| line 5: { role: 'user', content: [tool_result(Read)] } | ||
| line 6: { role: 'assistant', content: [text, tool_use(Edit)] } | ||
| line 7: { role: 'user', content: [tool_result(Edit)] } | ||
| line 8: { role: 'assistant', content: [text] } | ||
| // Progress messages NOT persisted | ||
| Memory State: | ||
| New memory written: | ||
| ~/.claude/.../memory/feedback_bugfix.md | ||
| → "Button click path should mirror keyboard active-state behavior" | ||
| MEMORY.md index: | ||
| → auto-loaded next session | ||
| Cost Tracking: | ||
| totalCostUSD += 0.032 | ||
| modelUsage['claude-sonnet-4'] += { input: 12450, output: 1823 } | ||
| [Waiting for next user input] | ||
| ``` | ||
| ## Module Participation Per Turn | ||
| ``` | ||
| T1(Grep) T2(Read) T3(Edit) T4(Done) | ||
| ──────── ──────── ──────── ──────── | ||
| 17 System Prompt ✓ build cache cache cache | ||
| 08 Config ✓ load cache cache cache | ||
| 03 Memory ✓ recall ── ── extract | ||
| 07 Permission Grep→auto Read→auto Edit→ASK ── | ||
| 04 Hooks pre/post pre/post pre/post stop | ||
| 02 Tool System Grep×2∥ Read Edit ── | ||
| 09 Streaming yes yes yes yes | ||
| 05 Compression check check check check | ||
| 10 Session record record record record | ||
| 13 Query Loop all 6 ph all 6 ph all 6 ph phase 1-3 | ||
| 15 Ink UI render render PROMPT! render | ||
| 12 Memory Dataflow ── ── ── write ch2 | ||
| Legend: ∥=parallel →=decision cache=memoized ──=not involved | ||
| ``` | ||
| ## Error Recovery Paths | ||
| ``` | ||
| If Turn 2 triggers 413 (Prompt Too Long): | ||
| Phase 3, Path 2: contextCollapse.recoverFromOverflow() | ||
| → or Path 3: reactiveCompact.tryReactiveCompact() | ||
| → compact → continue → retry API call | ||
| If API returns 529 (Overloaded): | ||
| withRetry() generator: | ||
| → Extract retry-after header | ||
| → Short retry: wait + fast-mode ON | ||
| → Max 10 retries, foreground source prioritized | ||
| If user Ctrl+C mid-stream: | ||
| abortController.signal fires: | ||
| → During streaming: drain + synthetic tool_results + return | ||
| → During tool execution: drain executor + return | ||
| → Abort cascades: parent → all children (01-agent-spawning) | ||
| If Edit tool denied by user: | ||
| permissionDecision = { behavior: 'deny' } | ||
| → tool_result with is_error: true | ||
| → Model sees error → may try alternate approach or ask user | ||
| If session crash: | ||
| conversationRecovery.ts: | ||
| → Load transcript up to last complete pair | ||
| → Offer resume with context (10-session) | ||
| ``` |
+126
-20
@@ -1,39 +0,145 @@ | ||
| [← Back to Index](index.html) | ||
| # 20. Cost Tracking & Budget | ||
| # 20\. Cost Tracking & Budget Medium | ||
| ## Key Files | ||
| Session 累计计费 → 硬 USD 限制 → 软 Token 预算 → 4 种展示通道 | ||
| ``` | ||
| src/cost-tracker.ts — session cost accumulation + storage/restore (L70-360) | ||
| src/costHook.ts — React hook, displays on exit | ||
| src/utils/modelCost.ts — pricing table + calculateUSDCost (L177) | ||
| src/query/tokenBudget.ts — per-turn soft token budget | ||
| src/bootstrap/state.ts — totalCostUSD, modelUsage state | ||
| src/QueryEngine.ts — maxBudgetUsd hard limit (L972-1010) | ||
| src/utils/attachments.ts — budget_usd attachment (L3850) | ||
| src/components/CostThresholdDialog.tsx — $5 interactive warning dialog | ||
| src/commands/cost/cost.ts — /cost command | ||
| ``` | ||
| R | ||
| ## Two-Layer Budget System | ||
| ## Feature Reference | ||
| ### Layer 1: Hard USD Budget (`QueryEngine.ts:972-1010`) | ||
| ### Key Files | ||
| ``` | ||
| maxBudgetUsd parameter (optional; undefined = no limit): | ||
| if (getTotalCost() >= maxBudgetUsd) { | ||
| → STOP. Return error: "Reached maximum budget ($X)" | ||
| → Hard stop — query terminates immediately, no continuation | ||
| } | ||
| src/cost-tracker.ts — 计费累加 + 存储恢复 (L70-360) src/costHook.ts — React hook, exit 时展示 src/utils/modelCost.ts — 定价表 + calculateUSDCost (L177) src/query/tokenBudget.ts — per-turn soft token budget src/bootstrap/state.ts — totalCostUSD, modelUsage state src/QueryEngine.ts — maxBudgetUsd hard limit (L972-1010) src/utils/attachments.ts — budget_usd attachment (L3850) src/components/CostThresholdDialog.tsx — $5 交互式警告 src/commands/cost/cost.ts — /cost command | ||
| Check point: beginning of each query loop iteration | ||
| Sources: CLI --max-budget, SDK config, API parameter | ||
| ``` | ||
| ### Evidence Boundary | ||
| ### Layer 2: Soft Per-Turn Token Budget (`tokenBudget.ts`) | ||
| src/cost-tracker.ts 证明 session cost 的累加、格式化与 project-config 持久化;src/utils/modelCost.ts:177 证明 USD 计算来自定价表;src/QueryEngine.ts:972-1000 证明 hard budget 是真正的 query-stop 条件;src/query/tokenBudget.ts 证明 soft token budget 是单独的 continuation/stopping heuristic。 | ||
| ``` | ||
| COMPLETION_THRESHOLD = 0.9 // start considering stop at 90% | ||
| DIMINISHING_THRESHOLD = 500 // 2 consecutive turns with delta < 500 = diminishing | ||
| ### 两层 Budget — Hard USD + Soft Token | ||
| checkTokenBudget(tracker, agentId, budget, turnTokens): | ||
| → ContinueDecision: continue + inject nudge message | ||
| → StopDecision: stop (diminishing returns detected) | ||
| ═══ Layer 1: Hard USD Budget (QueryEngine.ts:972-1010) ═══ maxBudgetUsd 参数 (可选,undefined=无限制): if (getTotalCost() >= maxBudgetUsd) { → STOP! Return error: "Reached maximum budget ($X)" → 硬限制,query 立即终止,不继续 } // 检查点: query loop 每轮开始时 // 来源: CLI --max-budget, SDK config, API parameter ═══ Layer 2: Soft Per-Turn Token Budget (tokenBudget.ts) ═══ COMPLETION_THRESHOLD = 0.9 // 90% 时开始考虑停止 DIMINISHING_THRESHOLD = 500 // 连续 2 轮 delta < 500 = diminishing checkTokenBudget(tracker, agentId, budget, turnTokens): → ContinueDecision: 继续 + inject nudge message → StopDecision: 停止 (diminishing returns detected) // 软限制: 不强制停,给 model 提示 // auto-continue: continuationCount < 3 且 tokens 正常流动时 // 90% + diminishing → 才真正 stop | ||
| Soft limit: does not force stop, nudges the model | ||
| auto-continue: continuationCount < 3 and tokens flowing normally | ||
| 90% + diminishing → actual stop triggered | ||
| ``` | ||
| ### 计费累加 — addToTotalSessionCost (cost-tracker.ts:195-328) | ||
| ## Cost Accumulation — `addToTotalSessionCost` (`cost-tracker.ts:195-328`) | ||
| 每次 API response 后调用: calculateUSDCost(resolvedModel, usage): → 从 MODEL_COSTS lookup table 查定价 → 计算: input_tokens × input_price \+ output_tokens × output_price \+ cache_read × cache_read_price \+ cache_creation × cache_creation_price \+ web_search × web_search_price → 返回 USD 金额 addToTotalSessionCost(cost, usage, model): STATE.totalCostUSD += cost STATE.modelUsage[model] = updated ModelUsage // Subagent 成本 → 滚入 session 总计 (无隔离): for (const advisorUsage of getAdvisorUsage(usage)): advisorCost = calculateUSDCost(advisorUsage.model, advisorUsage) totalCost += addToTotalSessionCost(advisorCost, ...) → 递归处理嵌套 advisor calls | ||
| ``` | ||
| Called after each API response: | ||
| ### ModelUsage 字段 (cost-tracker.ts:273-306) | ||
| calculateUSDCost(resolvedModel, usage): | ||
| → lookup pricing from MODEL_COSTS table | ||
| → compute: input_tokens × input_price | ||
| + output_tokens × output_price | ||
| + cache_read × cache_read_price | ||
| + cache_creation × cache_creation_price | ||
| + web_search × web_search_price | ||
| → return USD amount | ||
| type ModelUsage = { inputTokens: number outputTokens: number cacheReadInputTokens: number cacheCreationInputTokens: number webSearchRequests: number costUSD: number contextWindow: number maxOutputTokens: number } // Per-model 追踪: STATE.modelUsage = { 'claude-sonnet-4': { inputTokens: 45000, outputTokens: 12000, ... }, 'claude-opus-4': { inputTokens: 8000, outputTokens: 3000, ... }, } | ||
| addToTotalSessionCost(cost, usage, model): | ||
| STATE.totalCostUSD += cost | ||
| STATE.modelUsage[model] = updated ModelUsage | ||
| ### Session 持久化 & Resume (cost-tracker.ts:100-197) | ||
| // Subagent costs rolled into session total (no isolation): | ||
| for (const advisorUsage of getAdvisorUsage(usage)): | ||
| advisorCost = calculateUSDCost(advisorUsage.model, advisorUsage) | ||
| totalCost += addToTotalSessionCost(advisorCost, ...) | ||
| → recursively handles nested advisor calls | ||
| ``` | ||
| // 存储: saveCurrentSessionCosts(fpsMetrics): → 写入 project config 的 last* 字段 → 含: lastCost, lastAPIDuration, lastToolDuration, lastModelUsage, lastSessionId, fpsMetrics // 恢复 (--resume): getStoredSessionCosts(sessionId) (L100-127): → 只有 projectConfig.lastSessionId === sessionId 时才恢复 restoreCostStateForSession(sessionId) (L137-149): → 恢复 STATE.totalCostUSD + modelUsage // 保证: resume 后 cost 继续累加,不从 0 开始 | ||
| ## `ModelUsage` Type (`cost-tracker.ts:273-306`) | ||
| ### 4 种展示通道 | ||
| ```typescript | ||
| type ModelUsage = { | ||
| inputTokens: number | ||
| outputTokens: number | ||
| cacheReadInputTokens: number | ||
| cacheCreationInputTokens: number | ||
| webSearchRequests: number | ||
| costUSD: number | ||
| contextWindow: number | ||
| maxOutputTokens: number | ||
| } | ||
| 1\. Exit Summary (costHook.ts): useCostSummary() → process exit 时打印 formatTotalCost(): "Total cost: $0.032 | API: 4.2s | Wall: 12.1s | +23 lines" Per-model breakdown (short names) → 只在 hasConsoleBillingAccess() 时显示 2\. Interactive Dialog (CostThresholdDialog.tsx): "$5 spent on Anthropic API this session" → 在 cost 达到阈值时弹出 → Links to docs/costs 3\. /cost Slash Command (commands/cost/cost.ts): → 返回 formatTotalCost() 输出 → Subscriber: subscription vs overage status → Ant: full breakdown → Free users: command hidden 4\. Model Context Attachment (attachments.ts:3850-3870): getMaxBudgetUsdAttachment(maxBudgetUsd): → { used, total, remaining } → 注入到 model context (informational) → Model 看到 remaining budget → 自我约束 | ||
| // Per-model tracking in STATE: | ||
| STATE.modelUsage = { | ||
| 'claude-sonnet-4': { inputTokens: 45000, outputTokens: 12000, ... }, | ||
| 'claude-opus-4': { inputTokens: 8000, outputTokens: 3000, ... }, | ||
| } | ||
| ``` | ||
| cost-tracker budget token budget pricing | ||
| ## Session Persistence & Resume (`cost-tracker.ts:100-197`) | ||
| ``` | ||
| // Save: | ||
| saveCurrentSessionCosts(fpsMetrics): | ||
| → writes to project config last* fields | ||
| → includes: lastCost, lastAPIDuration, lastToolDuration, | ||
| lastModelUsage, lastSessionId, fpsMetrics | ||
| // Restore (--resume flag): | ||
| getStoredSessionCosts(sessionId) (L100-127): | ||
| → restores only if projectConfig.lastSessionId === sessionId | ||
| restoreCostStateForSession(sessionId) (L137-149): | ||
| → restores STATE.totalCostUSD + modelUsage | ||
| // Guarantee: after resume, cost accumulates from prior total (not zero) | ||
| ``` | ||
| ## 4 Display Channels | ||
| ### 1. Exit Summary (`costHook.ts`) | ||
| ``` | ||
| useCostSummary() → prints on process exit | ||
| formatTotalCost(): | ||
| "Total cost: $0.032 | API: 4.2s | Wall: 12.1s | +23 lines" | ||
| Per-model breakdown (short names) | ||
| → only shown when hasConsoleBillingAccess() | ||
| ``` | ||
| ### 2. Interactive Dialog (`CostThresholdDialog.tsx`) | ||
| ``` | ||
| "$5 spent on Anthropic API this session" | ||
| → shown when cost reaches threshold | ||
| → links to docs/costs | ||
| ``` | ||
| ### 3. `/cost` Slash Command (`commands/cost/cost.ts`) | ||
| ``` | ||
| → returns formatTotalCost() output | ||
| → Subscriber: subscription vs overage status | ||
| → Ant: full breakdown | ||
| → Free users: command hidden | ||
| ``` | ||
| ### 4. Model Context Attachment (`attachments.ts:3850-3870`) | ||
| ``` | ||
| getMaxBudgetUsdAttachment(maxBudgetUsd): | ||
| → { used, total, remaining } | ||
| → injected into model context (informational) | ||
| → model sees remaining budget → self-regulates | ||
| ``` | ||
| **Tags:** cost-tracker, budget, token budget, pricing |
+220
-26
@@ -1,51 +0,245 @@ | ||
| [← Back to Index](index.html) | ||
| # 21. Agent Definition & Discovery | ||
| # 21\. Agent Definition & Discovery Critical | ||
| ## Key Files | ||
| .claude/agents/*.md → 多源加载 → MCP 过滤 → deny 规则 → prompt 注入 | ||
| ``` | ||
| src/tools/AgentTool/loadAgentsDir.ts — load + parse + merge (L302-750) | ||
| src/tools/AgentTool/builtInAgents.ts — built-in agents (L22-71) | ||
| src/tools/AgentTool/built-in/ — individual built-in definitions | ||
| src/tools/AgentTool/AgentTool.tsx — selection + filter pipeline (L75-360) | ||
| src/tools/AgentTool/prompt.ts — agent list → prompt injection (L66-300) | ||
| src/tools/AgentTool/agentDisplay.ts — override display logic | ||
| src/utils/permissions/permissions.ts — filterDeniedAgents (L325-345) | ||
| src/commands/agents/ — /agents command (list/create/edit/delete) | ||
| ``` | ||
| R | ||
| ## `AgentDefinition` Type (`loadAgentsDir.ts:95-180`) | ||
| ## Feature Reference | ||
| ```typescript | ||
| BaseAgentDefinition = { | ||
| agentType: string // unique identifier ("Explore", "ppt") | ||
| whenToUse: string // description for model selection | ||
| tools?: string[] // tool allowlist | ||
| disallowedTools?: string[] // tool denylist | ||
| skills?: string[] // skills to preload | ||
| mcpServers?: AgentMcpServerSpec[] // MCP servers (ref or inline) | ||
| hooks?: HooksSettings // session-scoped hooks | ||
| color?: AgentColorName // UI color | ||
| model?: string // "sonnet"/"opus"/"haiku"/"inherit" | ||
| effort?: EffortValue // 0-3 or "low"/"medium"/"high" | ||
| permissionMode?: PermissionMode // "implicit"/"explicit" | ||
| maxTurns?: number // max turns before abort | ||
| background?: boolean // auto-background | ||
| initialPrompt?: string // prepend to first turn | ||
| memory?: 'user'|'project'|'local' // persistent memory scope | ||
| isolation?: 'worktree'|'remote' // isolated execution env | ||
| requiredMcpServers?: string[] // pattern-match server names | ||
| filename?: string // original .md path | ||
| baseDir?: string // loaded from which directory | ||
| omitClaudeMd?: boolean // strip CLAUDE.md from context | ||
| } | ||
| ### Key Files | ||
| // 3 source variants: | ||
| BuiltInAgentDefinition = BaseAgent & { source: 'built-in', | ||
| getSystemPrompt(params): string } // dynamic prompt function | ||
| CustomAgentDefinition = BaseAgent & { source: SettingSource, | ||
| getSystemPrompt(): string } // closure prompt | ||
| PluginAgentDefinition = BaseAgent & { source: 'plugin', | ||
| plugin: string } // plugin name | ||
| src/tools/AgentTool/loadAgentsDir.ts — 加载 + 解析 + 合并 (L302-750) src/tools/AgentTool/builtInAgents.ts — 6 个 built-in agents (L22-71) src/tools/AgentTool/built-in/ — 每个 built-in 的定义 src/tools/AgentTool/AgentTool.tsx — 选择 + 过滤 pipeline (L75-360) src/tools/AgentTool/prompt.ts — agent list → prompt 注入 (L66-300) src/tools/AgentTool/agentDisplay.ts — override 展示逻辑 src/utils/permissions/permissions.ts — filterDeniedAgents (L325-345) src/commands/agents/ — /agents 命令 (list/create/edit/delete) | ||
| AgentDefinition = BuiltIn | Custom | Plugin | ||
| ``` | ||
| ### Evidence Boundary | ||
| ## Agent `.md` File Format | ||
| src/tools/AgentTool/loadAgentsDir.ts:94-180 定义 AgentDefinition union;src/tools/AgentTool/loadAgentsDir.ts:193-216 证明真正的 override 顺序;src/tools/AgentTool/builtInAgents.ts:22-68 证明 built-ins 是条件装载,不是永远 6 个;src/tools/AgentTool/prompt.ts:66-170 证明 agent list prompt 有 inline 与 attachment 两种模式。 | ||
| ``` | ||
| === File Locations (multi-source, priority: last-wins-per-type) === | ||
| ~/.claude/agents/*.md — user global | ||
| .claude/agents/*.md — project-level (traverses up to git root) | ||
| managed/policy agents — enterprise managed | ||
| plugins — plugin-provided | ||
| flag settings — feature flag overrides | ||
| ### AgentDefinition 完整类型 (loadAgentsDir.ts:95-180) | ||
| === Markdown File Structure === | ||
| --- | ||
| name: my-agent | ||
| description: >- | ||
| When to use this agent. | ||
| Multi-line supported (\n → actual newline). | ||
| model: opus | ||
| tools: bash, file-read, file-write | ||
| disallowedTools: agent | ||
| skills: code-review, testing | ||
| effort: high | ||
| memory: user | ||
| maxTurns: 10 | ||
| background: false | ||
| permissionMode: implicit | ||
| isolation: worktree | ||
| initialPrompt: "Always start by reading README.md" | ||
| mcpServers: | ||
| - name: my-server | ||
| command: npx | ||
| args: ["-y", "my-mcp-server"] | ||
| hooks: | ||
| onSessionStart: | ||
| - command: npm run db:migrate | ||
| --- | ||
| You are a specialized agent for... | ||
| BaseAgentDefinition = { agentType: string // unique identifier ("Explore", "ppt") whenToUse: string // description for model selection tools?: string[] // tool allowlist disallowedTools?: string[] // tool denylist skills?: string[] // skills to preload mcpServers?: AgentMcpServerSpec[] // MCP servers (ref or inline) hooks?: HooksSettings // session-scoped hooks color?: AgentColorName // UI color model?: string // "sonnet"/"opus"/"haiku"/"inherit" effort?: EffortValue // 0-3 or "low"/"medium"/"high" permissionMode?: PermissionMode // "implicit"/"explicit" maxTurns?: number // max turns before abort background?: boolean // auto-background initialPrompt?: string // prepend to first turn memory?: 'user'|'project'|'local' // persistent memory scope isolation?: 'worktree'|'remote' // isolated execution env requiredMcpServers?: string[] // pattern-match server names filename?: string // original .md path baseDir?: string // loaded from where omitClaudeMd?: boolean // strip CLAUDE.md from context } // 3 种来源: BuiltInAgentDefinition = BaseAgent & { source: 'built-in', getSystemPrompt(params): string } // 动态 prompt 函数 CustomAgentDefinition = BaseAgent & { source: SettingSource, getSystemPrompt(): string } // 闭包 prompt PluginAgentDefinition = BaseAgent & { source: 'plugin', plugin: string } // plugin 名 AgentDefinition = BuiltIn | Custom | Plugin | ||
| Your capabilities: | ||
| - CODE ANALYSIS | ||
| - OPTIMIZATION | ||
| ### Agent .md 文件格式 | ||
| === Parsing Rules === | ||
| - body (everything after ---) = system prompt | ||
| - tools: comma-separated → array | ||
| - skills: comma-separated → array | ||
| - memory enabled → auto-inject FILE_READ/WRITE/EDIT tools | ||
| - mcpServers: array of specs (name ref OR inline config) | ||
| ``` | ||
| ═══ 文件位置 (多源, 优先级 last-wins-per-type) ═══ ~/.claude/agents/*.md — 用户全局 .claude/agents/*.md — 项目级 (向上遍历到 git root) managed/policy agents — 企业管理 plugins — 插件提供 flag settings — feature flag ═══ Markdown 文件结构 ═══ \--- name: my-agent description: >\- When to use this agent. Multi-line supported (\n → actual newline). model: opus tools: bash, file-read, file-write disallowedTools: agent skills: code-review, testing effort: high memory: user maxTurns: 10 background: false permissionMode: implicit isolation: worktree initialPrompt: "Always start by reading README.md" mcpServers: \- name: my-server command: npx args: ["-y", "my-mcp-server"] hooks: onSessionStart: \- command: npm run db:migrate \--- You are a specialized agent for... Your capabilities: \- CODE ANALYSIS \- OPTIMIZATION ═══ 解析规则 ═══ \- body (---后面的全部内容) = system prompt \- tools: comma-separated → array \- skills: comma-separated → array \- memory enabled → auto-inject FILE_READ/WRITE/EDIT tools \- mcpServers: array of specs (name ref OR inline config) | ||
| ## Built-in Agents (`builtInAgents.ts:22-71`) | ||
| ### Built-in Agents (最多 6 个,按 gate/entrypoint 条件出现) | ||
| Up to 6 built-ins, conditionally loaded by gate/entrypoint: | ||
| // builtInAgents.ts:22-71 — getBuiltInAgents() 1\. general-purpose — always active → All tools (*), research + multi-step tasks 2\. Explore — BUILTIN_EXPLORE_PLAN_AGENTS + tengu_amber_stoat → Read-only: Glob, Grep, Read → Fast codebase exploration 3\. Plan — BUILTIN_EXPLORE_PLAN_AGENTS + tengu_amber_stoat → Read-only: architecture design → No writes! 4\. Verification — VERIFICATION_AGENT + tengu_hive_evidence → Break tests, verify implementations 5\. StatuslineSetup — always active → Shell prompt config conversion 6\. claude-code-guide — always active (non-SDK only) → Claude Code + API documentation help // Special modes: COORDINATOR_MODE=true → 替换整个 agent list (getCoordinatorAgents) CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS=1 → noninteractive 时 return [] | ||
| ``` | ||
| 1. general-purpose — always active | ||
| → All tools (*), research + multi-step tasks | ||
| ### Agent 选择 & 过滤 Pipeline (AgentTool.tsx:75-360) | ||
| 2. Explore — BUILTIN_EXPLORE_PLAN_AGENTS + tengu_amber_stoat | ||
| → Read-only: Glob, Grep, Read | ||
| → Fast codebase exploration | ||
| 用户调用 Agent({ subagent_type: "Explore", prompt: "..." }) ═══ 完整 Pipeline ═══ Step 1: Load ALL agents getAgentDefinitionsWithOverrides(cwd): ├─ Load built-in agents ├─ Load plugin agents ├─ Load user ~/.claude/agents/*.md ├─ Load project .claude/agents/*.md (traverse up tree) ├─ Load managed/policy agents └─ Priority: last-wins-per-type (getActiveAgentsFromList) Step 2: Extract available MCP servers from tools tools.filter(t => t.name.startsWith('mcp__')) → Extract server names from tool name prefix Step 3: Filter by MCP requirements filterAgentsByMcpRequirements(agents, mcpServersWithTools): → agent.requiredMcpServers ALL must match → Case-insensitive substring match → "slack" matches "Slack" or "slack-integration" Step 4: Filter by deny rules filterDeniedAgents(agents, toolPermissionContext, "Agent"): → Parse deny rules: DENY Agent(explore) → Set{"Explore"} → Remove agents in denied set Step 5: Filter by allowedAgentTypes (optional) if Agent(x,y) spec → only those types allowed Step 6: Match subagent_type to AgentDefinition find agent by agentType === subagent_type Step 7: Execute ├─ agent.getSystemPrompt() → system prompt ├─ Assemble tool pool (tools / disallowedTools) ├─ Apply overrides (model, maxTurns, isolation, memory...) └─ Spawn/Fork/Background | ||
| 3. Plan — BUILTIN_EXPLORE_PLAN_AGENTS + tengu_amber_stoat | ||
| → Read-only: architecture design (no writes) | ||
| ### Agent List 注入 Prompt — 两种模式 | ||
| 4. Verification — VERIFICATION_AGENT + tengu_hive_evidence | ||
| → Break tests, verify implementations | ||
| // prompt.ts:66-300 getPrompt(agentDefinitions, isCoordinator?, allowedAgentTypes?): ═══ Mode A: Inline (default, legacy) ═══ 直接写入 AgentTool 的 description: "Available agent types: \- general-purpose: Research... (Tools: *) \- Explore: Fast codebase... (Tools: Glob, Grep, Read) \- Plan: Architecture... (Tools: Read, Glob)" 问题: agent list 变化 → cache bust ═══ Mode B: Attachment (tengu_agent_list_attach) ═══ AgentTool description 只写: "Available agent types are listed in <system-reminder> messages" 实际 list 通过 agent_listing_delta attachment 注入: → 不影响 tool definition cache → MCP load, plugin reload 时更新 ═══ Tool List 格式化 ═══ formatAgentLine(agent): "- {agentType}: {whenToUse} (Tools: {toolsDescription})" toolsDescription logic: only allowlist → list specific tools only denylist → "All tools except X, Y" both → effective = allowlist - denylist neither → "All tools" | ||
| 5. StatuslineSetup — always active | ||
| → Shell prompt config conversion | ||
| ### Override 优先级 — last-wins-per-type | ||
| 6. claude-code-guide — always active (non-SDK only) | ||
| → Claude Code + API documentation help | ||
| getActiveAgentsFromList(allAgents): 加载顺序 (low → high): 1\. built-in — 最低优先级 2\. plugin — 覆盖 built-in 3\. userSettings — 用户全局 4\. projectSettings — 项目级 5\. flagSettings — CLI/flag overrides 6\. policySettings — 企业管理最高 同一个 agentType: → later source wins (managed > flag > project > user > plugin > built-in) → /agents 命令显示 "overridden by X" 标注 // 对标 v4 的 find_agent 概念: // CC 用 agentType 字符串精确匹配 // v4 用 output_type + intent 匹配 // CC 的 pipeline 更适合作为 v4 find_agent Level 1 的参考 | ||
| // Special modes: | ||
| COORDINATOR_MODE=true → replaces entire agent list (getCoordinatorAgents) | ||
| CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS=1 → noninteractive: return [] | ||
| ``` | ||
| ### /agents 命令 — CRUD | ||
| ## Agent Selection & Filter Pipeline (`AgentTool.tsx:75-360`) | ||
| // commands/agents/ → AgentsMenu 组件 功能: \- List: 按 source 分组显示 agents \- Detail: 查看 agent definition + system prompt \- Create: 创建新 agent (wizard, 生成 .md) \- Edit: 编辑 custom agent (markdown) \- Delete: 删除 custom agent \- Copy: 复制到 user settings Override display: resolveAgentOverrides(allAgents, activeAgents) → Annotate: "overridden by projectSettings" | ||
| ``` | ||
| User calls Agent({ subagent_type: "Explore", prompt: "..." }) | ||
| ### 对标 Mitsein v4 — 精确映射 | ||
| === Full Pipeline === | ||
| Claude Code ←→ Mitsein v4 ──────────────────────────────────────────────── agentType (string) ←→ slug (agent_config.slug) whenToUse (description) ←→ name + description tools[] / disallowedTools[] ←→ tools[] (JSON) skills[] ←→ skills[] (JSON) getSystemPrompt() ←→ system_prompt (TEXT) model override ←→ (未定义, 可加) maxTurns ←→ (未定义, 可加) memory scope ←→ MemoryCard (跨 agent) background flag ←→ spawn_agent 异步模式 requiredMcpServers ←→ depends_on[{output_type}] source (built-in/custom/...) ←→ source (platform/store/auto/user) filterDeniedAgents ←→ ToolPolicy deny filterByMcpRequirements ←→ depends_on 安装检查 关键差异: CC: agentType 精确匹配 (no intent/output_type) v4: find_agent 支持 output_type 精确 + LLM 意图匹配 → v4 的 find_agent 更高级, 但 CC 的过滤 pipeline 值得复用 CC: 文件系统 (.md) → 无版本控制 v4: DB (agent_config) → 有版本, 有 user_id scope → v4 更适合多租户, CC 的 frontmatter schema 仍可复用 | ||
| Step 1: Load ALL agents | ||
| getAgentDefinitionsWithOverrides(cwd): | ||
| ├─ Load built-in agents | ||
| ├─ Load plugin agents | ||
| ├─ Load user ~/.claude/agents/*.md | ||
| ├─ Load project .claude/agents/*.md (traverse up tree) | ||
| ├─ Load managed/policy agents | ||
| └─ Priority: last-wins-per-type (getActiveAgentsFromList) | ||
| agent definition discovery frontmatter filtering pipeline find_agent | ||
| Step 2: Extract available MCP servers from tools | ||
| tools.filter(t => t.name.startsWith('mcp__')) | ||
| → Extract server names from tool name prefix | ||
| Step 3: Filter by MCP requirements | ||
| filterAgentsByMcpRequirements(agents, mcpServersWithTools): | ||
| → agent.requiredMcpServers: ALL must match | ||
| → Case-insensitive substring match | ||
| → "slack" matches "Slack" or "slack-integration" | ||
| Step 4: Filter by deny rules | ||
| filterDeniedAgents(agents, toolPermissionContext, "Agent"): | ||
| → Parse deny rules: DENY Agent(explore) → Set{"Explore"} | ||
| → Remove agents in denied set | ||
| Step 5: Filter by allowedAgentTypes (optional) | ||
| if Agent(x,y) spec → only those types allowed | ||
| Step 6: Match subagent_type to AgentDefinition | ||
| find agent by agentType === subagent_type | ||
| Step 7: Execute | ||
| ├─ agent.getSystemPrompt() → system prompt | ||
| ├─ Assemble tool pool (tools / disallowedTools) | ||
| ├─ Apply overrides (model, maxTurns, isolation, memory...) | ||
| └─ Spawn/Fork/Background | ||
| ``` | ||
| ## Agent List Prompt Injection — Two Modes (`prompt.ts:66-300`) | ||
| ```typescript | ||
| getPrompt(agentDefinitions, isCoordinator?, allowedAgentTypes?): | ||
| === Mode A: Inline (default, legacy) === | ||
| Written directly into AgentTool's description: | ||
| "Available agent types: | ||
| - general-purpose: Research... (Tools: *) | ||
| - Explore: Fast codebase... (Tools: Glob, Grep, Read) | ||
| - Plan: Architecture... (Tools: Read, Glob)" | ||
| Drawback: agent list changes → cache bust | ||
| === Mode B: Attachment (tengu_agent_list_attach) === | ||
| AgentTool description only says: | ||
| "Available agent types are listed in <system-reminder> messages" | ||
| Actual list injected via agent_listing_delta attachment: | ||
| → does not affect tool definition cache | ||
| → updates on MCP load, plugin reload | ||
| === Tool List Formatting === | ||
| formatAgentLine(agent): | ||
| "- {agentType}: {whenToUse} (Tools: {toolsDescription})" | ||
| toolsDescription logic: | ||
| only allowlist → list specific tools | ||
| only denylist → "All tools except X, Y" | ||
| both → effective = allowlist - denylist | ||
| neither → "All tools" | ||
| ``` | ||
| ## Override Priority — Last-Wins-Per-Type | ||
| ``` | ||
| getActiveAgentsFromList(allAgents): | ||
| Load order (low → high priority): | ||
| 1. built-in — lowest priority | ||
| 2. plugin — overrides built-in | ||
| 3. userSettings — user global | ||
| 4. projectSettings — project-level | ||
| 5. flagSettings — CLI/flag overrides | ||
| 6. policySettings — enterprise managed (highest) | ||
| Same agentType: | ||
| → later source wins | ||
| → /agents command shows "overridden by X" annotation | ||
| ``` | ||
| ## `/agents` Command — CRUD (`commands/agents/`) | ||
| ``` | ||
| // AgentsMenu component | ||
| Operations: | ||
| - List: show agents grouped by source | ||
| - Detail: view agent definition + system prompt | ||
| - Create: create new agent (wizard, generates .md) | ||
| - Edit: edit custom agent (markdown) | ||
| - Delete: delete custom agent | ||
| - Copy: copy to user settings | ||
| Override display: | ||
| resolveAgentOverrides(allAgents, activeAgents) | ||
| → Annotate: "overridden by projectSettings" | ||
| ``` | ||
| **Tags:** agent definition, discovery, frontmatter, filtering pipeline |
+236
-50
@@ -1,99 +0,285 @@ | ||
| [← Back to Index](index.html) | ||
| # 22. Testing Infrastructure & Inferred Practices | ||
| # 22\. Testing Infrastructure & Inferred Practices High | ||
| ## Key Files | ||
| 这个 snapshot 基本不含测试文件;本页只总结源码中可直接看到的测试基础设施,以及由此反推的实践 | ||
| ``` | ||
| src/services/vcr.ts — VCR fixture record/replay (L24-115) | ||
| src/services/mockRateLimits.ts — 15 rate limit scenarios (~500 lines) | ||
| src/tools/testing/TestingPermissionTool.tsx — permission E2E tool (50 lines) | ||
| src/utils/config.ts — TEST_*_FOR_TESTING constants (L763-768) | ||
| src/utils/fsOperations.ts — FsOperations DI abstraction (L1-70) | ||
| src/utils/permissions/autoModeState.ts — _resetForTesting example (L35) | ||
| src/bootstrap/state.ts — resetStateForTests (L919) | ||
| src/context.ts — memoize cache clear (L37) | ||
| src/native-ts/color-diff/index.ts — __test export (L991-998) | ||
| ``` | ||
| R | ||
| ## Directly Confirmed Infrastructure | ||
| ## 逆向证据来源 | ||
| The snapshot contains no `__tests__/`, `.test.*`, or `.spec.*` files. The following is confirmed directly from production source: | ||
| ### 源码里能直接确认的测试基础设施 | ||
| - **30+ `_resetForTesting()` functions** — explicit module state reset contracts | ||
| - **20+ `NODE_ENV === 'test'` branches** — behavioral switch points | ||
| - **VCR fixture system (`vcr.ts`)** — API response record/replay | ||
| - **`TestingPermissionTool`** — permission pipeline E2E verification | ||
| - **`TEST_*_FOR_TESTING` constants** — ESM mock workaround | ||
| - **15 `MockRateLimitScenario` types** — preset billing test scenarios | ||
| - **`__test` export namespace** — whitebox access to internal functions | ||
| - **`FsOperations` abstraction** — filesystem dependency injection | ||
| - **Optional-param DI pattern** — zero-framework dependency injection | ||
| snapshot 中未发现 __tests__/、.test.*、.spec.* 文件 因此无法直接验证测试覆盖率或测试风格占比 以下能力能直接从生产源码确认: 30+ 个 _resetForTesting() 函数 — 模块状态重置契约 20+ 处 NODE_ENV === 'test' 分支 — 行为切换点 VCR fixture 系统 (vcr.ts) — API response 录制/回放 TestingPermissionTool — permission pipeline E2E 验证 TEST_*_FOR_TESTING 常量 — ESM mock 的 workaround 15 种 MockRateLimitScenario — 预置 billing 测试场景 __test export namespace — 白盒测试内部函数 FsOperations 抽象 — 文件系统依赖注入 可选参数 DI 模式 — 零框架依赖注入 结论边界: \- "有这些测试基础设施" = 可证实 \- "团队偏爱某种测试哲学" = 基于这些痕迹的推断,不是直接证据 | ||
| --- | ||
| ### Evidence Boundary | ||
| ## Practice 1: Module State — Explicit Reset Contract | ||
| 本页里的数量词如 “30+ 个 `_resetForTesting()`” 和 “20+ 处 `NODE_ENV === 'test'`” 都是当前 snapshot 的近似 grep 结论,不该被读成长期架构常量。能稳稳落地的只有“存在这些模式”,不是“比例一定如此”。 | ||
| ```typescript | ||
| // autoModeState.ts (30+ modules follow this pattern) | ||
| let autoModeActive = false | ||
| let autoModeCircuitBroken = false | ||
| ### Key Files | ||
| export function _resetForTesting() { | ||
| autoModeActive = false | ||
| autoModeCircuitBroken = false | ||
| } | ||
| src/services/vcr.ts — VCR fixture 录制/回放 (L24-115) src/services/mockRateLimits.ts — 15 种 rate limit 场景 (~500 lines) src/tools/testing/TestingPermissionTool.tsx — permission E2E tool (50 lines) src/utils/config.ts — TEST_*_FOR_TESTING 常量 (L763-768) src/utils/fsOperations.ts — FsOperations DI 抽象 (L1-70) src/utils/permissions/autoModeState.ts — _resetForTesting 典型例 (L35) src/bootstrap/state.ts — resetStateForTests (L919) src/context.ts — memoize cache clear (L37) src/native-ts/color-diff/index.ts — __test export (L991-998) | ||
| // Leading underscore = "test-only, don't call in production" | ||
| // Not private — intentional test contract | ||
| 1 | ||
| // Modules with resets: | ||
| // analytics, fullscreen, autoModeState, bootstrap/state, | ||
| // keybindings, lsp, policy limits, upstream proxy, ... | ||
| ``` | ||
| ## 10 条基础设施模式与推断实践 | ||
| **Principle:** any module with module-level state must have a reset. No reset → tests pollute each other. | ||
| ### Practice 1: 模块状态 = 显式 reset 契约 | ||
| --- | ||
| // 30+ 个模块遵循此模式 // autoModeState.ts let autoModeActive = false let autoModeCircuitBroken = false export function _resetForTesting() { autoModeActive = false autoModeCircuitBroken = false } // 下划线前缀 = "测试用,别在生产调" // 不是 private — 是 intentional test contract // 有 reset 的模块: // analytics (sink, eventQueue) // fullscreen (once-per-session flags) // autoModeState (3-state machine) // bootstrap/state (整个 session state) // keybindings, lsp, policy limits, upstream proxy... | ||
| ## Practice 2: `NODE_ENV='test'` Is Time Compression, Not Disabling | ||
| **设计原则:** 有 module-level state 就必须有 reset。否则测试互相污染。 | ||
| ```typescript | ||
| // 20+ conditional branches — don't disable features, skip slow I/O | ||
| ### Practice 2: NODE_ENV='test' 是时间压缩,不是 mock | ||
| // setup.ts:444 → skip git status (expensive I/O) | ||
| // git filesystem → 10ms watch interval (not 1000ms) | ||
| // GrepTool → faster subprocess kill timeout | ||
| // analytics → disable SEND (not collection) | ||
| // auto-updater → skip fetch | ||
| // ripgrep → use local fixture binary | ||
| // debug output → silence stderr | ||
| // config reload → skip file watcher re-arm | ||
| // 20+ 处条件分支 — 不关闭功能,只跳过慢操作 // setup.ts:444 → skip git status (expensive I/O) // git filesystem → 10ms watch interval (not 1000ms) // GrepTool → faster subprocess kill timeout // analytics → disable SEND (not collect) // auto-updater → skip fetch // ripgrep → use local fixture binary // debug output → silence stderr // config reload → skip file watcher re-arm // 关键: 代码路径仍然执行 // 只是 I/O 被短路 → 逻辑正确性 + 无网络延迟 | ||
| // Code paths still execute. | ||
| // I/O is short-circuited → logical correctness without network latency. | ||
| ``` | ||
| **设计原则:** Test mode ≠ disabled mode。逻辑要跑,I/O 可以跳。 | ||
| **Principle:** test mode ≠ disabled mode. Logic runs; I/O can be skipped. | ||
| ### Practice 3: VCR Fixture — LLM API 测试的杀手锏 | ||
| --- | ||
| // vcr.ts:24-115 function shouldUseVCR(): boolean { if (process.env.NODE_ENV === 'test') return true if (process.env.USER_TYPE === 'ant' && isEnvTruthy(process.env.FORCE_VCR)) return true return false } async function withVCR<T>(messages, f): Promise<T> { // 有 fixture → 返回缓存的 API response (秒级) // 没有 fixture → 真实调 API → 录制 → 写磁盘 } // 工作流: // 1. 开发: VCR_RECORD=true → 真实调 API → 录 JSON fixture // 2. CI: 读 fixture → 不调 API → 秒级完成 // 3. 过期: 删 fixture → 重新录制 // Env vars: // CLAUDE_CODE_TEST_FIXTURES_ROOT — fixture 目录 // VCR_RECORD — 开启录制 // FORCE_VCR — Ant 强制 VCR (behavioral debugging) | ||
| ## Practice 3: VCR Fixture — LLM API Testing | ||
| **Mitsein 建议:** Python 用 `vcrpy` 或 `responses` 库。录一次 LLM response,跑一万次 CI。Agent 测试最大成本就是 API call。 | ||
| ```typescript | ||
| // vcr.ts:24-115 | ||
| ### Practice 4: 依赖注入 = 可选参数 + 生产默认值 | ||
| function shouldUseVCR(): boolean { | ||
| if (process.env.NODE_ENV === 'test') return true | ||
| if (process.env.USER_TYPE === 'ant' | ||
| && isEnvTruthy(process.env.FORCE_VCR)) return true | ||
| return false | ||
| } | ||
| // 零框架。不用 IoC container。 // Pattern A: Constructor optional params constructor(opts?: { getAuthHeaders?: () => Promise<string> }) { this.getAuthHeaders = opts?.getAuthHeaders ?? getSessionIngressAuthHeaders } // 测试: new Client({ getAuthHeaders: () => 'test-token' }) // 生产: new Client() // 用默认 // Pattern B: FsOperations 抽象 (fsOperations.ts:1-70) type FsOperations = { cwd(): string existsSync(path: string): boolean readFile(path: string): Promise<string> writeFile(path: string, content: string): Promise<void> // ... 15+ methods } // 测试: swap to virtual filesystem // 生产: getFsImplementation() → real fs | ||
| async function withVCR<T>(messages, f): Promise<T> { | ||
| // fixture exists → return cached API response (instant) | ||
| // no fixture → real API call → record → write to disk | ||
| } | ||
| **Python 等价:**`def __init__(self, llm=None): self.llm = llm or default_llm_client()` | ||
| // Workflow: | ||
| // 1. Development: VCR_RECORD=true → real API call → record JSON fixture | ||
| // 2. CI: read fixture → no API call → completes in seconds | ||
| // 3. Expired: delete fixture → re-record | ||
| ### Practice 5: "不要 mock 数据库" 是弱证据,不宜过度外推 | ||
| // Env vars: | ||
| // CLAUDE_CODE_TEST_FIXTURES_ROOT — fixture directory | ||
| // VCR_RECORD — enable recording mode | ||
| // FORCE_VCR — Ant: force VCR (behavioral debugging) | ||
| ``` | ||
| // 这句话确实出现在源码里,但位置是在 memory taxonomy 示例文本, // 不是测试框架或测试目录里的政策文件: // memoryTypes.ts:65-66 // "don't mock the database in these tests — // we got burned last quarter when mocked tests // passed but prod migration failed" // 因此可以安全得出的结论: // 1. 这个观点被系统 prompt / memory 内容显式编码过 // 2. 团队至少认为它是值得传播的经验 // // 不能直接得出的结论: // - "他们所有 DB 测试都用真实数据库" // - "这是当前测试套件的硬规则" | ||
| --- | ||
| **更稳妥的表述:** 这更像是一个被写进系统知识的团队教训,而不是我们能直接从 snapshot 证明的测试政策。 | ||
| ## Practice 4: Dependency Injection — Optional Params with Production Defaults | ||
| ### Practice 6: Permission 专用测试 Tool | ||
| ```typescript | ||
| // Zero frameworks. No IoC container. | ||
| // TestingPermissionTool.tsx (50 lines) name: "TestingPermission" isEnabled(): return NODE_ENV === 'test' // 生产不存在 checkPermissions(): always return 'ask' // 必触发 dialog call(): return success immediately // 不做实际操作 // 渲染: 全部 return null (UI clean) // 目的: 不测 tool 功能,只测 permission 系统 // 是否正确拦截/放行 // 注入: tools.ts:244 动态加载 | ||
| // Pattern A: Constructor optional params | ||
| constructor(opts?: { getAuthHeaders?: () => Promise<string> }) { | ||
| this.getAuthHeaders = opts?.getAuthHeaders ?? getSessionIngressAuthHeaders | ||
| } | ||
| // Test: new Client({ getAuthHeaders: () => 'test-token' }) | ||
| // Production: new Client() // uses default | ||
| **设计原则:** 被测系统(permission)和测试工具(tool)解耦。Tool 是 permission 的 fixture,不是反过来。 | ||
| // Pattern B: FsOperations abstraction (fsOperations.ts:1-70) | ||
| type FsOperations = { | ||
| cwd(): string | ||
| existsSync(path: string): boolean | ||
| readFile(path: string): Promise<string> | ||
| writeFile(path: string, content: string): Promise<void> | ||
| // ... 15+ methods | ||
| } | ||
| // Test: swap to virtual filesystem | ||
| // Production: getFsImplementation() → real fs | ||
| ``` | ||
| ### Practice 7: Memoize 缓存必须可清 | ||
| --- | ||
| // 每个 memoized 函数都有 .cache.clear?.() getUserContext.cache?.clear() // context.ts getSystemContext.cache?.clear() // context.ts getAgentDefinitionsWithOverrides.cache?.clear() // print.ts // Optional chaining (?.) 处理 cache 不存在的情况 // 用在 test teardown / hot-reload 后刷新 // 不清 = 上一个 test 的 cached 值影响下一个 // 这是测试最隐蔽的 bug | ||
| ## Practice 5: The "Don't Mock the Database" Observation | ||
| ### Practice 8: 预置 Mock 场景 (业务语义命名) | ||
| ``` | ||
| // memoryTypes.ts:65-66 (within memory taxonomy example text): | ||
| // "don't mock the database in these tests — | ||
| // we got burned last quarter when mocked tests | ||
| // passed but prod migration failed" | ||
| // mockRateLimits.ts (~500 lines) setMockRateLimitScenario('weekly-limit-reached') // NOT: setMockHeader('x-ratelimit-remaining', '0') // 15 种预置场景: 'normal' | 'session-limit-reached' 'approaching-weekly-limit' | 'weekly-limit-reached' 'overage-active' | 'overage-warning' | 'overage-exhausted' 'out-of-credits' | 'org-zero-credit-limit' 'member-zero-credit-limit' | 'seat-tier-zero-credit-limit' 'opus-limit' | 'opus-warning' 'sonnet-limit' | 'sonnet-warning' 'fast-mode-limit' // 读起来像规格说明,不是 HTTP header 操作 // → 测试意图一目了然 | ||
| // What this confirms: | ||
| // 1. This perspective was explicitly encoded in system prompt / memory content | ||
| // 2. The team considered it worth propagating as institutional knowledge | ||
| **设计原则:** 用业务语义命名 mock,不用技术细节。`scenario('weekly-limit-reached')` > `header('x-remaining', '0')`。 | ||
| // What this does NOT confirm: | ||
| // - "all DB tests use real databases" | ||
| // - "this is a hard rule in the current test suite" | ||
| ``` | ||
| ### Practice 9: __test namespace 白盒导出 | ||
| This is a team lesson written into system knowledge, not a directly provable test policy from the snapshot. | ||
| // color-diff/index.ts:991-998 export const __test = { tokenize, findAdjacentPairs, wordDiffStrings, ansi256FromRgb, colorToEscape, detectColorMode, detectLanguage, } // 内部函数不公开 API → 但测试要访问 // __test namespace = "内部的,但测试用" // 不污染 public API surface | ||
| --- | ||
| ### Practice 10: ESM Mock 的务实妥协 | ||
| ## Practice 6: Permission-Dedicated Test Tool | ||
| // config.ts:763 注释原文: // "We have to put this test code here because // Jest doesn't support mocking ES modules :O" export const TEST_GLOBAL_CONFIG_FOR_TESTING = { auth: { ... }, onboarding: { ... }, autoUpdates: disabled, } export const TEST_PROJECT_CONFIG_FOR_TESTING = { ... } // 测试配置直接写在生产代码里 // 用 NODE_ENV === 'test' 守卫 // 不优雅,但 works // → 务实 > 教条 | ||
| ```typescript | ||
| // TestingPermissionTool.tsx (50 lines) | ||
| **教训:** 如果工具(Jest/ESM)的限制迫使你妥协,不要花 2 周绕过它——直接在代码里标注原因,继续前进。 | ||
| name: "TestingPermission" | ||
| isEnabled(): return NODE_ENV === 'test' // absent in production | ||
| 2 | ||
| checkPermissions(): always return 'ask' // always triggers dialog | ||
| call(): return success immediately // no actual operation | ||
| ## 从基础设施反推的三条倾向 | ||
| // Renders: all return null (clean UI) | ||
| ### Integration > Unit 是合理推断,但不是硬证据 | ||
| // Purpose: test the permission system, not the tool itself | ||
| // Does it correctly intercept / allow through? | ||
| // Injected: tools.ts:244 dynamic loading | ||
| ``` | ||
| 证据链: 1\. VCR fixture — 录真实 API response, 不是纯 stub 2\. TestingPermissionTool — 单独打 permission pipeline 3\. mockRateLimits — 用业务语义场景测限额逻辑 4\. memoryTypes 里保存了 "don't mock the database" 的团队经验 5\. snapshot 未包含测试文件,所以看不到最终测试分层 更稳妥的结论: 他们显然重视 integration / E2E harness 但无法只凭这个 snapshot 断言 "集成测试多于单测" Golden Path: 单元测试 → 纯函数 (tokenize, calculateUSDCost) 集成测试 → 带 VCR 的 API 调用 + 真实依赖优先 E2E 测试 → TestingPermissionTool + /mock-limits | ||
| **Principle:** the system under test (permission) and the test fixture (tool) are decoupled. The tool is a fixture for permissions, not the reverse. | ||
| ### State Reset 是硬性要求 | ||
| --- | ||
| 30+ 个模块有 _resetForTesting() → analytics, permissions, fullscreen, config, keybindings, lsp, policy, proxy, compact, bridge, ... 每个有 module-level let 的文件 = 潜在测试污染源 → 必须有 reset → 否则 test A 影响 test B (最难 debug 的 flaky test) Memoize cache 同理: → .cache.clear?.() 是 test teardown 的标配 | ||
| ## Practice 7: Memoize Caches Must Be Clearable | ||
| ### 测试辅助代码明确住在生产模块里 | ||
| ```typescript | ||
| // Every memoized function has .cache.clear?.() | ||
| Claude Code 的选择: ✓ _resetForTesting() 在 production module 里 ✓ TEST_* 常量在 production module 里 ✓ VCR 逻辑在 production module 里 ✓ MockRateLimits 在 production module 里 ✓ __test export 在 production module 里 全部用 NODE_ENV === 'test' 守卫 好处: \- 测试基础设施跟被测代码同源 → 不会 drift \- 不需要复杂的 test util 包 \- Refactor 时一起改 坏处: \- 生产 bundle 稍大 (但 tree-shakeable) \- 新人可能困惑 "这是测试还是生产?" → 用下划线前缀 + 注释解决 | ||
| getUserContext.cache?.clear() // context.ts | ||
| getSystemContext.cache?.clear() // context.ts | ||
| getAgentDefinitionsWithOverrides.cache?.clear() // print.ts | ||
| testing VCR fixture _resetForTesting dependency injection integration > unit | ||
| // Optional chaining (?.) handles absent cache | ||
| // Used in test teardown / after hot-reload | ||
| // Not clearing = previous test's cached value leaks into next test | ||
| // This is the most hidden bug category in test suites | ||
| ``` | ||
| --- | ||
| ## Practice 8: Preset Mock Scenarios with Business Semantics | ||
| ```typescript | ||
| // mockRateLimits.ts (~500 lines) | ||
| setMockRateLimitScenario('weekly-limit-reached') | ||
| // NOT: setMockHeader('x-ratelimit-remaining', '0') | ||
| // 15 preset scenarios: | ||
| 'normal' | 'session-limit-reached' | ||
| 'approaching-weekly-limit' | 'weekly-limit-reached' | ||
| 'overage-active' | 'overage-warning' | 'overage-exhausted' | ||
| 'out-of-credits' | 'org-zero-credit-limit' | ||
| 'member-zero-credit-limit' | 'seat-tier-zero-credit-limit' | ||
| 'opus-limit' | 'opus-warning' | ||
| 'sonnet-limit' | 'sonnet-warning' | ||
| 'fast-mode-limit' | ||
| // Reads like a spec, not HTTP header manipulation | ||
| // Test intent is immediately clear | ||
| ``` | ||
| **Principle:** name mocks by business semantics, not technical details. | ||
| --- | ||
| ## Practice 9: `__test` Namespace for Whitebox Access | ||
| ```typescript | ||
| // color-diff/index.ts:991-998 | ||
| export const __test = { | ||
| tokenize, | ||
| findAdjacentPairs, | ||
| wordDiffStrings, | ||
| ansi256FromRgb, | ||
| colorToEscape, | ||
| detectColorMode, | ||
| detectLanguage, | ||
| } | ||
| // Internal functions not part of public API → but tests need access | ||
| // __test namespace = "internal but available for testing" | ||
| // Does not pollute public API surface | ||
| ``` | ||
| --- | ||
| ## Practice 10: ESM Mock Pragmatic Workaround | ||
| ```typescript | ||
| // config.ts:763 comment (verbatim): | ||
| // "We have to put this test code here because | ||
| // Jest doesn't support mocking ES modules :O" | ||
| export const TEST_GLOBAL_CONFIG_FOR_TESTING = { | ||
| auth: { ... }, | ||
| onboarding: { ... }, | ||
| autoUpdates: disabled, | ||
| } | ||
| export const TEST_PROJECT_CONFIG_FOR_TESTING = { ... } | ||
| // Test config lives directly in production module | ||
| // Guarded by NODE_ENV === 'test' | ||
| // Not elegant, but works | ||
| ``` | ||
| --- | ||
| ## Inferred Tendencies from Infrastructure | ||
| ### State Reset Is Non-Negotiable | ||
| 30+ modules have `_resetForTesting()` covering: analytics, permissions, fullscreen, config, keybindings, lsp, policy, proxy, compact, bridge, and others. | ||
| Every file with module-level `let` is a potential test contamination source. Memoize caches require `.cache.clear?.()` in test teardown. | ||
| ### Test Helper Code Lives in Production Modules | ||
| - `_resetForTesting()` — in production module | ||
| - `TEST_*` constants — in production module | ||
| - VCR logic — in production module | ||
| - `MockRateLimits` — in production module | ||
| - `__test` export — in production module | ||
| All guarded by `NODE_ENV === 'test'`. | ||
| Tradeoff: test infrastructure co-locates with tested code (no drift, refactors stay together); slightly larger production bundle (tree-shakeable). | ||
| **Tags:** testing, VCR fixture, _resetForTesting, dependency injection |
@@ -1,69 +0,87 @@ | ||
| [← Back to Index](index.html) | ||
| # 23. Command System | ||
| # 23\. Command System High | ||
| ## Key Files | ||
| /command 不是单纯 parser,而是发现、过滤、分发、prompt 暴露与运行时刷新的一整层 | ||
| ``` | ||
| README.md — command system overview | ||
| src/commands.ts — registration, loading, filtering, caching | ||
| src/types/command.ts — Command type definition | ||
| src/commands/ — built-in slash commands | ||
| src/skills/loadSkillsDir.ts — skills → commands | ||
| src/utils/plugins/loadPluginCommands.ts — plugins → commands/skills | ||
| ``` | ||
| R | ||
| ## Command Sources — Multi-Origin Load (`commands.ts:460-518`) | ||
| ## Feature Reference | ||
| `loadAllCommands(cwd)` assembles from 7 sources in order: | ||
| ### Key Files | ||
| 1. `bundledSkills` | ||
| 2. `builtinPluginSkills` | ||
| 3. `skillDirCommands` | ||
| 4. `workflowCommands` | ||
| 5. `pluginCommands` | ||
| 6. `pluginSkills` | ||
| 7. `COMMANDS()` built-ins | ||
| README.md — command system 总览 src/commands.ts — 注册、加载、过滤、缓存 src/types/command.ts — Command 类型定义 src/commands/ — built-in slash commands src/skills/loadSkillsDir.ts — skills → commands src/utils/plugins/loadPluginCommands.ts — plugins → commands/skills | ||
| `getCommands(cwd)`: | ||
| - Applies `availability` / `isEnabled` filtering | ||
| - Inserts `dynamicSkills` between plugin skills and built-in commands | ||
| - Refreshes on auth changes; reuses memoized reload results | ||
| ### 命令来源不是单一目录 | ||
| ## Command Types (`commands.ts:191`) | ||
| loadAllCommands(cwd): 1\. bundledSkills 2\. builtinPluginSkills 3\. skillDirCommands 4\. workflowCommands 5\. pluginCommands 6\. pluginSkills 7\. COMMANDS() 内建命令 getCommands(cwd): → 先做 availability / isEnabled 过滤 → 再把 dynamicSkills 插入到 plugin skills 与 built-in commands 之间 → auth 变化时 fresh check,重用 memoized 重载结果 | ||
| ``` | ||
| type: 'local' | ||
| → local UI / state commands | ||
| → examples: /cost, /context, /vim, /keybindings, /voice | ||
| src/commands.ts:460-518 | ||
| type: 'prompt' | ||
| → generates a prompt, handed off to the model for execution | ||
| → examples: /commit, /review, /init, /statusline, /insights | ||
| ### 命令有两类,不只是 UI action | ||
| Additional dimensions: | ||
| source: builtin / plugin / skills / bundled / commands_DEPRECATED | ||
| loadedFrom: determines eligibility for SkillTool and slash-command-tool | ||
| ``` | ||
| type: 'local' → 本地 UI / 状态命令 → 例: /cost, /context, /vim, /keybindings, /voice type: 'prompt' → 生成 prompt,交给模型继续执行 → 例: /commit, /review, /init, /statusline, /insights 额外维度: source: builtin / plugin / skills / bundled / commands_DEPRECATED loadedFrom: 决定它是否进入 SkillTool 与 slash-command-tool | ||
| ## Feature Flag Dead-Code Elimination (`commands.ts:58-117`) | ||
| src/commands.ts:191 src/commands/vim/index.ts:1-9 | ||
| Feature flags remove entire command subtrees at bundle time, not at runtime: | ||
| ### Feature Flag 直接裁掉命令树 | ||
| ``` | ||
| feature('VOICE_MODE') → /voice | ||
| feature('BRIDGE_MODE') → /bridge | ||
| feature('DAEMON') → /remoteControlServer | ||
| feature('KAIROS') → /assistant | ||
| feature('WORKFLOW_SCRIPTS') → /workflows | ||
| ``` | ||
| feature('VOICE_MODE') → /voice feature('BRIDGE_MODE') → /bridge feature('DAEMON') → /remoteControlServer feature('KAIROS') → /assistant feature('WORKFLOW_SCRIPTS') → /workflows 不是运行时 if,而是 bun bundle dead-code elimination。 | ||
| This is bun bundle dead-code elimination, not a runtime `if` check. | ||
| src/commands.ts:58-117 | ||
| ## Dual-Surface Architecture | ||
| S | ||
| Slash commands serve two chains simultaneously (`commands.ts:541-590`): | ||
| ## System Model | ||
| 1. **Human UX surface** — user types `/review`, `/config`, etc. | ||
| 2. **Model capability surface** — `prompt`-type commands are exposed to the model via `SkillTool` as callable capabilities | ||
| ### Slash Commands 同时服务两条链路 | ||
| The command system is both a human UX router and a model capability router. | ||
| * 用户手动输入 `/review`、`/config` 这类 slash command。 | ||
| * 模型通过 SkillTool 间接看到一部分 prompt-type commands,把它们当成可调用能力。 | ||
| * 因此 command system 同时承担 human UX surface 与 model capability surface。 | ||
| ## Skill/Command Convergence (`commands.ts`) | ||
| src/commands.ts:541-590 | ||
| ```typescript | ||
| getSkillToolCommands(): | ||
| // retains prompt-type + model-invocable commands | ||
| ### Skill 与 Command 在这里真正汇流 | ||
| getSlashCommandToolSkills(): | ||
| // further filters to "skills-like" command set | ||
| // sources: skills / plugin / bundled / disableModelInvocation | ||
| ``` | ||
| getSkillToolCommands(): 保留 prompt-type + 可被模型调用的命令 getSlashCommandToolSkills(): 再过滤成 "skills-like" 的命令集合 来源可为 skills / plugin / bundled / disableModelInvocation | ||
| The skill system and command system merge here. A skill loaded from `loadSkillsDir.ts` or a plugin becomes a slash command and potentially a SkillTool-invocable capability. | ||
| 所以前面那章 _Skill System_ 只覆盖了技能本身,还没把它如何注入 slash command 与 SkillTool 讲完整。 | ||
| ## Remote Mode Command Whitelist (`commands.ts:613-620`) | ||
| ### Remote Mode 还有一套命令白名单 | ||
| The system maintains `REMOTE_SAFE_COMMANDS` — an explicit allowlist of commands that remain locally executable in remote mode. Commands not on this list are not forwarded to or executed by the remote session. | ||
| 源码明确维护 `REMOTE_SAFE_COMMANDS`。这说明命令系统还承担“哪些命令在远程模式仍可本地执行”的职责,而不是简单交给 remote session。 | ||
| src/commands.ts:613-620 | ||
| C | ||
| ## Coverage Verdict | ||
| ### 为什么这一章不能省 | ||
| * `00-feature-discovery` 列出了很多命令,但没有解释命令是怎么注册、裁剪、缓存和暴露给模型的。 | ||
| * `06-skill-system` 讲了 skill,却没有把 slash command 与 skill/tool 的交叉层讲透。 | ||
| * 如果缺少这一章,会误以为 Claude Code 的命令只是薄 UI,而不是 capability router。 | ||
| 这一章补上之后,学习地图里 “用户输入 / 命令系统 / SkillTool / prompt command / local command / remote-safe whitelist” 这条线才闭环。 | ||
| slash commands skills feature flags remote mode | ||
| **Tags:** slash commands, skills, feature flags, remote mode |
@@ -1,64 +0,74 @@ | ||
| [← Back to Index](index.html) | ||
| # 24. Plugin Architecture | ||
| # 24\. Plugin Architecture High | ||
| ## Key Files | ||
| plugin 不只是加命令,它还能注入 skills、agents、MCP、LSP、settings 与 marketplace 生命周期 | ||
| ``` | ||
| README.md — plugin architecture entry point | ||
| src/utils/plugins/pluginLoader.ts — primary loader | ||
| src/utils/plugins/loadPluginCommands.ts — markdown → commands/skills | ||
| src/services/plugins/PluginInstallationManager.ts — marketplace install & refresh | ||
| src/utils/plugins/marketplaceManager.ts — marketplace metadata | ||
| src/utils/plugins/refresh.ts — refreshActivePlugins() | ||
| ``` | ||
| R | ||
| ## Three-Source Merge (`pluginLoader.ts:3137-3195`) | ||
| ## Feature Reference | ||
| `assemblePluginLoadResult()` merges from three origins: | ||
| ### Key Files | ||
| 1. **marketplace plugins** | ||
| 2. **session-only plugins** (`--plugin-dir` / inline) | ||
| 3. **built-in plugins** | ||
| README.md — plugin architecture 入口 src/utils/plugins/pluginLoader.ts — 主加载器 src/utils/plugins/loadPluginCommands.ts — markdown → commands/skills src/services/plugins/PluginInstallationManager.ts — marketplace 安装与刷新 src/utils/plugins/marketplaceManager.ts — marketplace 元数据 src/utils/plugins/refresh.ts — refreshActivePlugins() | ||
| `mergePluginSources()`: | ||
| - session overrides installed | ||
| - managed settings can lock installed plugins | ||
| ### 插件来源是三路合并 | ||
| `verifyAndDemote()`: | ||
| - missing dependencies do not hard-fail | ||
| - instead: demote to `disabled` state + log error | ||
| assemblePluginLoadResult(): 1\. marketplace plugins 2\. session-only plugins (--plugin-dir / inline) 3\. built-in plugins mergePluginSources(): session overrides installed 但 managed settings 可锁住 installed plugin verifyAndDemote(): 依赖缺失时不 hard fail 而是 demote 为 disabled + 记录错误 | ||
| ## Startup Cache-Only Path (`pluginLoader.ts:3096-3149`) | ||
| src/utils/plugins/pluginLoader.ts:3137-3195 | ||
| ``` | ||
| loadAllPlugins() — full load; may trigger network / clone operations | ||
| loadAllPluginsCacheOnly() — reads installed cache only; does not block REPL startup | ||
| ### 启动期有 cache-only 路径 | ||
| Strategy: | ||
| startup consumers → cache-only | ||
| /plugins / refresh → full load | ||
| SYNC_PLUGIN_INSTALL=1 → force full load | ||
| ``` | ||
| loadAllPlugins() — full load,可触发网络 / clone loadAllPluginsCacheOnly() — 只读 installed cache,不阻塞交互启动 使用策略: startup consumers → cache-only /plugins / refresh → full load SYNC_PLUGIN_INSTALL=1 → 强制 full load | ||
| Cache-only startup exists to prevent plugin clone operations from blocking the interactive REPL. | ||
| 这是为了避免启动时因为 plugin clone 阻塞 REPL。 | ||
| ## Marketplace Installation Lifecycle (`PluginInstallationManager.ts:45-163`) | ||
| src/utils/plugins/pluginLoader.ts:3096-3149 | ||
| Background installation does not block the main flow: | ||
| S | ||
| ``` | ||
| performBackgroundPluginInstallations(): | ||
| diff declared vs materialized marketplaces | ||
| → AppState marks: pending → installing → installed / failed | ||
| → reconcileMarketplaces() | ||
| → on installed: auto-call refreshActivePlugins() | ||
| → on update only: mark needsRefresh, prompt user to run /reload-plugins | ||
| ``` | ||
| ## Lifecycle | ||
| ## Plugin Markdown → Command / Skill (`loadPluginCommands.ts:35-220`) | ||
| ### Marketplace 安装是后台任务,不阻塞主流程 | ||
| - `walkPluginMarkdown()` recursively collects markdown files from the plugin directory | ||
| - A file named `SKILL.md` is recognized as a skill directory, not a plain command file | ||
| - Namespace comes from directory hierarchy; final name format: `plugin:namespace:command` | ||
| - Frontmatter fields control description, tools, model, shell behavior, etc. | ||
| performBackgroundPluginInstallations(): diff declared vs materialized marketplaces → AppState 先标记 pending/installing/installed/failed → reconcileMarketplaces() → installed: 自动 refreshActivePlugins() → updated only: 标记 needsRefresh,提示 /reload-plugins | ||
| ## Scope of Effect | ||
| src/services/plugins/PluginInstallationManager.ts:45-163 | ||
| Plugin loader output flows into: | ||
| - Command system (slash commands + SkillTool) | ||
| - Agent definitions (`loadAgentsDir.ts`) | ||
| - MCP / LSP configuration | ||
| - Settings cache | ||
| ### Plugin Markdown 会直接变成 Command / Skill | ||
| A plugin is a cross-cutting extension layer, not an isolated feature module. Loading, refreshing, or installing a plugin can affect commands, agents, MCP servers, LSP servers, and settings simultaneously. | ||
| * `walkPluginMarkdown()` 递归收集 markdown。 | ||
| * `SKILL.md` 会被识别成 skill directory,而不是普通命令文件。 | ||
| * 命名空间来自目录层级,最终名字形如 `plugin:namespace:command`。 | ||
| * frontmatter 决定 description、tools、model、shell frontmatter 等行为。 | ||
| src/utils/plugins/loadPluginCommands.ts:35-220 | ||
| ### 为什么这不是“附属系统” | ||
| plugin loader 的结果会流入命令系统、agent 定义、MCP/LSP 配置与 settings 缓存。也就是说 plugin 是 Claude Code 的横切扩展层,而不是单独功能页。 | ||
| C | ||
| ## Coverage Verdict | ||
| ### 这一章缺失时会造成的误解 | ||
| * 会把 plugin 误读成“第三方 slash commands”。 | ||
| * 会漏掉 cache-only startup、background install、refresh 边界。 | ||
| * 会看不懂为什么 command/agent/MCP/LSP 都受 plugin refresh 影响。 | ||
| Claude Code 的 plugin architecture 更接近 “可插拔能力分发层”,不是“扩展商店 UI”。 | ||
| plugins marketplace cache-only startup refresh | ||
| **Tags:** plugins, marketplace, cache-only startup, refresh |
@@ -1,58 +0,78 @@ | ||
| [← Back to Index](index.html) | ||
| # 25. Remote Sessions & Direct Connect | ||
| # 25\. Remote Sessions & Direct Connect High | ||
| ## Key Files | ||
| bridge 讲 IDE↔CLI,中这一章讲 session 真正如何远程创建、订阅、发消息、回权限与中断 | ||
| ``` | ||
| src/remote/RemoteSessionManager.ts — CCR remote session management | ||
| src/remote/SessionsWebSocket.ts — subscription + reconnect logic | ||
| src/server/createDirectConnectSession.ts — direct-connect session creation | ||
| src/server/directConnectManager.ts — direct-connect WebSocket manager | ||
| src/utils/teleport/api.ts — remote session HTTP message sending | ||
| ``` | ||
| R | ||
| ## Two Remote Modes | ||
| ## Feature Reference | ||
| ### Mode A: CCR Remote Session | ||
| ### Key Files | ||
| ``` | ||
| HTTP POST → send user event | ||
| WebSocket → subscribe to SDK messages / permission requests | ||
| ``` | ||
| src/remote/RemoteSessionManager.ts — CCR 远程会话管理 src/remote/SessionsWebSocket.ts — 订阅与重连 src/server/createDirectConnectSession.ts — direct-connect 建 session src/server/directConnectManager.ts — direct-connect websocket manager src/utils/teleport/api.ts — remote session HTTP 发送消息 | ||
| Reference: `src/remote/RemoteSessionManager.ts:77-175` | ||
| ### 这里至少有两种远程模式 | ||
| ### Mode B: Direct Connect | ||
| Mode A: CCR Remote Session HTTP POST 发送 user event WebSocket 订阅 SDK message / permission requests Mode B: Direct Connect POST {serverUrl}/sessions 建会话 server 返回 {session_id, ws_url, work_dir} 后续全走 direct WebSocket 所以它不等于 bridge,也不等于简单 remote shell。 | ||
| ``` | ||
| POST {serverUrl}/sessions → create session | ||
| Server responds: { session_id, ws_url, work_dir } | ||
| Subsequent traffic: all over direct WebSocket | ||
| ``` | ||
| src/remote/RemoteSessionManager.ts:77-175 src/server/createDirectConnectSession.ts:18-79 | ||
| Reference: `src/server/createDirectConnectSession.ts:18-79` | ||
| S | ||
| These are distinct from the bridge system (IDE↔CLI). Remote sessions deal with session network protocol and control flow. | ||
| ## Protocol Shape | ||
| ## `RemoteSessionManager` — HTTP + WebSocket Dual Channel (`RemoteSessionManager.ts:91-308`) | ||
| ### RemoteSessionManager 是 HTTP + WS 双通道 | ||
| ``` | ||
| connect(): | ||
| → establish SessionsWebSocket | ||
| → callbacks: onMessage / onPermissionRequest / onReconnecting | ||
| connect(): → 建 SessionsWebSocket → onMessage / onPermissionRequest / onReconnecting sendMessage(): → sendEventToRemoteSession() 走 HTTP respondToPermissionRequest(): → control_response 走 WebSocket cancelSession(): → control_request { subtype: 'interrupt' } | ||
| sendMessage(): | ||
| → sendEventToRemoteSession() over HTTP | ||
| src/remote/RemoteSessionManager.ts:91-308 | ||
| respondToPermissionRequest(): | ||
| → control_response over WebSocket | ||
| ### SessionsWebSocket 自带重连与 close-code 语义 | ||
| cancelSession(): | ||
| → control_request { subtype: 'interrupt' } | ||
| ``` | ||
| * 2s reconnect delay,最多 5 次。 | ||
| * `4003` 被视为永久拒绝,停止重连。 | ||
| * `4001` 允许短暂重试,因为 compaction 期间 session 可能暂时不可见。 | ||
| * 30s ping 维持连接。 | ||
| ## `SessionsWebSocket` — Reconnect & Close-Code Semantics (`SessionsWebSocket.ts`) | ||
| src/remote/SessionsWebSocket.ts:14-34 src/remote/SessionsWebSocket.ts:69-195 | ||
| | Close Code | Behavior | | ||
| |---|---| | ||
| | `4003` | Permanent rejection — stop reconnecting | | ||
| | `4001` | Allow brief retry — session may be temporarily invisible during compaction | | ||
| ### Direct Connect 更像 self-hosted structured I/O | ||
| Additional behavior: | ||
| - 2s reconnect delay, max 5 attempts (`SessionsWebSocket.ts:14-34`) | ||
| - 30s ping to keep connection alive (`SessionsWebSocket.ts:69-195`) | ||
| createDirectConnectSession(): POST /sessions with { cwd, dangerously_skip_permissions? } DirectConnectSessionManager: WebSocket 收换行分隔 JSON 识别 control_request / control_response / SDK messages sendInterrupt() 直接发 structured control_request | ||
| ## Direct Connect — Structured I/O Protocol (`directConnectManager.ts:12-189`) | ||
| src/server/directConnectManager.ts:12-189 | ||
| ``` | ||
| createDirectConnectSession(): | ||
| POST /sessions with { cwd, dangerously_skip_permissions? } | ||
| C | ||
| DirectConnectSessionManager: | ||
| WebSocket receives newline-delimited JSON | ||
| Recognizes: control_request / control_response / SDK messages | ||
| sendInterrupt() → sends structured control_request directly | ||
| ``` | ||
| ## Coverage Verdict | ||
| Direct Connect behaves like a self-hosted structured I/O channel rather than a remote shell. | ||
| ### 为什么不能只写 Bridge System | ||
| * `14-bridge-system` 主要解释 IDE extension 与 child CLI 之间的桥。 | ||
| * remote/direct-connect 这章解释的是 session 网络协议与控制流。 | ||
| * 两者有交集,但抽象层级不同,源码目录也不同。 | ||
| 如果把 remote session 并入 bridge,一定会把 “谁负责 session 建立、谁负责 permission 回传、谁负责 reconnect” 这几条边界揉混。 | ||
| remote sessions direct connect websocket permissions | ||
| **Tags:** remote sessions, direct connect, WebSocket, permissions |
@@ -1,59 +0,70 @@ | ||
| [← Back to Index](index.html) | ||
| # 26. Swarm / Teams / Coordinator | ||
| # 26\. Swarm / Teams / Coordinator Critical | ||
| ## Key Files | ||
| 这不是普通 subagent;它引入 team 文件、leader 约束、backend 选择与 coordinator prompt | ||
| ``` | ||
| src/coordinator/coordinatorMode.ts — coordinator prompt / mode switching | ||
| src/tools/TeamCreateTool/TeamCreateTool.ts — team creation | ||
| src/tools/TeamDeleteTool/TeamDeleteTool.ts — team cleanup | ||
| src/utils/swarm/backends/registry.ts — pane vs in-process backend selection | ||
| src/utils/swarm/teamHelpers.ts — team file read/write + cleanup | ||
| ``` | ||
| R | ||
| ## Coordinator Mode — Explicit, Not Implicit (`coordinatorMode.ts:31-219`) | ||
| ## Feature Reference | ||
| Coordinator mode is activated explicitly, not inferred: | ||
| ### Key Files | ||
| ``` | ||
| isCoordinatorMode(): | ||
| feature('COORDINATOR_MODE') | ||
| + CLAUDE_CODE_COORDINATOR_MODE env var | ||
| src/coordinator/coordinatorMode.ts — coordinator prompt / mode 切换 src/tools/TeamCreateTool/TeamCreateTool.ts — 建 team src/tools/TeamDeleteTool/TeamDeleteTool.ts — 清理 team src/utils/swarm/backends/registry.ts — pane vs in-process backend src/utils/swarm/teamHelpers.ts — team file 读写与 cleanup | ||
| getCoordinatorSystemPrompt(): | ||
| → explicitly self-identifies as coordinator | ||
| → emphasizes: research / synthesis / implementation / verification phases | ||
| → treats worker results as internal signals, not conversation partners | ||
| ``` | ||
| ### Coordinator Mode 是显式模式,不是隐含策略 | ||
| ## `TeamCreateTool` — Persistent Team State (`TeamCreateTool.ts:71-210`) | ||
| isCoordinatorMode(): feature('COORDINATOR_MODE') \+ CLAUDE_CODE_COORDINATOR_MODE env getCoordinatorSystemPrompt(): 明确把自己定义为 coordinator 强调 research / synthesis / implementation / verification phases 把 worker results 视为 internal signals,而非对话对象 | ||
| ``` | ||
| TeamCreate: | ||
| → enforces: leader can manage only one team at a time | ||
| → generates deterministic leadAgentId | ||
| → writes team file to disk | ||
| → registerTeamForSessionCleanup() | ||
| → resetTaskList() + ensureTasksDir() | ||
| → setLeaderTeamName() | ||
| → updates AppState.teamContext | ||
| ``` | ||
| src/coordinator/coordinatorMode.ts:31-219 | ||
| ## `TeamDeleteTool` — Guarded Cleanup (`TeamDeleteTool.ts:61-133`) | ||
| S | ||
| Delete is not unconditional: | ||
| - Excludes the team lead; only checks non-lead members | ||
| - `isActive !== false` counts as an active member | ||
| - Refuses cleanup if active members remain — requires graceful termination first | ||
| - On success: clears directory, color, task inbox; cancels session-end cleanup registration | ||
| ## Team Lifecycle | ||
| ## Execution Backend Abstraction (`backends/registry.ts:400-455`) | ||
| ### TeamCreateTool 写的是持久 team state | ||
| ``` | ||
| getTeammateExecutor(preferInProcess): | ||
| preferInProcess && enabled → InProcessBackend | ||
| otherwise → PaneBackendExecutor | ||
| ``` | ||
| TeamCreate: → 限制 leader 一次只能管理一个 team → 生成 deterministic leadAgentId → 写 team file 到磁盘 → registerTeamForSessionCleanup() → resetTaskList() + ensureTasksDir() → setLeaderTeamName() → 更新 AppState.teamContext | ||
| Swarm execution is not bound to tmux panes. The registry performs policy dispatch between `InProcessBackend` and `PaneBackendExecutor`. Either can run a teammate. | ||
| src/tools/TeamCreateTool/TeamCreateTool.ts:71-210 | ||
| ## Scope Relative to Other Agent Systems | ||
| ### TeamDeleteTool 不是盲删,它会先检查活动成员 | ||
| | System | Concern | | ||
| |---|---| | ||
| | `01-agent-spawning` | Generic subagent fork/spawn | | ||
| | `11-agent-communication` | Message passback | | ||
| | `16-task-system` | Task management | | ||
| | **Swarm/Teams** | Leader identity, team files, backend selection, cleanup rules | | ||
| * 排除 team lead,只看非 lead members。 | ||
| * `isActive !== false` 才算活动成员。 | ||
| * 仍有活动成员时拒绝 cleanup,要求先 graceful terminate teammates。 | ||
| * 成功后清目录、清颜色、清 task inbox、取消 session-end cleanup。 | ||
| Swarm/Teams is the layer where Claude Code moves from multi-agent to multi-worker orchestration with explicit organizational structure. | ||
| src/tools/TeamDeleteTool/TeamDeleteTool.ts:61-133 | ||
| ### 执行 backend 还有一层抽象 | ||
| getTeammateExecutor(preferInProcess): preferInProcess && enabled → InProcessBackend 否则 → PaneBackendExecutor 说明 swarm 并不绑定 tmux pane; 只是 registry 会在 pane backend 与 in-process backend 之间做策略分发。 | ||
| src/utils/swarm/backends/registry.ts:400-455 | ||
| C | ||
| ## Coverage Verdict | ||
| ### 为什么 01 / 11 / 16 还不够 | ||
| * `01-agent-spawning` 讲的是通用 subagent fork/spawn。 | ||
| * `11-agent-communication` 讲的是消息回传。 | ||
| * `16-task-system` 讲的是任务管理。 | ||
| * 但 swarm/team 是额外一层组织结构,包含 leader、team file、backend、cleanup 规则。 | ||
| 这一章是“Claude Code 何时从 multi-agent 进入 multi-worker orchestration”的分界线,应该独立出来。 | ||
| swarm team lifecycle coordinator mode in-process backend | ||
| **Tags:** swarm, team lifecycle, coordinator mode, in-process backend |
+38
-38
@@ -1,52 +0,52 @@ | ||
| [← Back to Index](index.html) | ||
| # 27. Voice Mode | ||
| # 27\. Voice Mode Medium | ||
| ## Key Files | ||
| /voice 背后不是一个 flag toggle,而是 OAuth、GrowthBook、voice_stream WebSocket 和 hold-to-talk 交互 | ||
| ``` | ||
| src/commands/voice/index.ts — /voice command registration | ||
| src/voice/voiceModeEnabled.ts — visibility + runtime enablement | ||
| src/services/voiceStreamSTT.ts — voice_stream WebSocket STT client | ||
| src/voice/ — voice mode UI / hooks | ||
| ``` | ||
| R | ||
| ## Command Visibility vs. Runtime Availability — Two Checks | ||
| ## Feature Reference | ||
| ``` | ||
| /voice command: | ||
| isEnabled() → isVoiceGrowthBookEnabled() | ||
| isHidden → !isVoiceModeEnabled() | ||
| ### Key Files | ||
| isVoiceModeEnabled(): | ||
| hasVoiceAuth() && isVoiceGrowthBookEnabled() | ||
| src/commands/voice/index.ts — /voice 命令注册 src/voice/voiceModeEnabled.ts — 可见性与 runtime enablement src/services/voiceStreamSTT.ts — voice_stream WebSocket STT client src/voice/ — 语音模式 UI / hooks | ||
| hasVoiceAuth(): | ||
| requires Anthropic OAuth | ||
| API key / Bedrock / Vertex / Foundry → not eligible | ||
| ``` | ||
| ### 命令显隐与真正可用是两层判断 | ||
| Reference: `src/commands/voice/index.ts:1-18`, `src/voice/voiceModeEnabled.ts:13-52` | ||
| /voice command: isEnabled() → isVoiceGrowthBookEnabled() isHidden → !isVoiceModeEnabled() isVoiceModeEnabled(): hasVoiceAuth() && isVoiceGrowthBookEnabled() hasVoiceAuth(): 必须是 Anthropic OAuth API key / Bedrock / Vertex / Foundry 不行 | ||
| The command can be enabled (visible in the menu) while voice mode itself is unavailable (hidden from the user) — these are separate gating conditions. | ||
| src/commands/voice/index.ts:1-18 src/voice/voiceModeEnabled.ts:13-52 | ||
| ## `voice_stream` WebSocket STT Client (`voiceStreamSTT.ts:1-115`) | ||
| S | ||
| ``` | ||
| connectVoiceStream(): | ||
| → refresh OAuth token | ||
| → connect to /api/ws/speech_to_text/voice_stream | ||
| → JSON control messages + binary audio frames | ||
| → server sends: TranscriptText / TranscriptEndpoint / TranscriptError | ||
| ## Streaming Model | ||
| Interaction model: | ||
| hold-to-talk | ||
| hold = recording; release = send CloseStream | ||
| ``` | ||
| ### voice_stream 是专门的 WebSocket STT 客户端 | ||
| ## Production Details (`voiceStreamSTT.ts:28-68`, `130-171`) | ||
| connectVoiceStream(): → 刷新 OAuth token → 连接 /api/ws/speech_to_text/voice_stream → JSON control messages + binary audio frames → server 返回 TranscriptText / TranscriptEndpoint / TranscriptError 交互模型: hold-to-talk 按住录音,松开发 CloseStream | ||
| - **8s keepalive** — prevents idle disconnect during long recordings | ||
| - `finalize()` has two timeout layers: `noData` and `safety` — avoids hanging on WebSocket teardown | ||
| - Supports `language` and `keyterms` parameters | ||
| - GrowthBook gate can switch backend to conversation-engine + Deepgram Nova 3 | ||
| src/services/voiceStreamSTT.ts:1-115 | ||
| ### 这个实现已经考虑到生产细节 | ||
| * 8s keepalive,避免长录音时连接闲置。 | ||
| * `finalize()` 有 `noData` 与 `safety` 两层超时,不死等 WebSocket teardown。 | ||
| * 支持 language 与 keyterms 参数。 | ||
| * 可通过 GrowthBook gate 切换到 conversation-engine + Deepgram Nova 3。 | ||
| src/services/voiceStreamSTT.ts:28-68 src/services/voiceStreamSTT.ts:130-171 | ||
| C | ||
| ## Coverage Verdict | ||
| ### 为什么值得单列 | ||
| * 源码里 voice 拥有独立命令、auth gate、feature gate、流式 STT 客户端。 | ||
| * 这已经是完整子系统,不只是 feature flag 样例。 | ||
| * 如果省略,会低估 Claude Code 在 multimodal terminal UX 上的复杂度。 | ||
| 这一章更像“语音输入前端 + 私有 STT 协议层”,而不是“给模型开麦克风”这么简单。 | ||
| voice oauth websocket speech-to-text | ||
| **Tags:** voice, OAuth, WebSocket, speech-to-text |
@@ -1,61 +0,64 @@ | ||
| [← Back to Index](index.html) | ||
| # 28. Keybindings + Vim | ||
| # 28\. Keybindings + Vim Medium | ||
| ## Key Files | ||
| 终端交互层不只有 Ink 组件,还包括可配置键位、context 优先级、chord 解析和 Vim 状态机 | ||
| ``` | ||
| src/commands/keybindings/index.ts — /keybindings command | ||
| src/commands/vim/index.ts — /vim command | ||
| src/keybindings/useKeybinding.ts — hook API | ||
| src/keybindings/KeybindingContext.tsx — active context / pending chord | ||
| src/keybindings/resolver.ts — chord resolution | ||
| src/vim/transitions.ts — Vim state transition table | ||
| ``` | ||
| R | ||
| ## Keybinding System — Not a Simple `onKeyDown` Map | ||
| ## Feature Reference | ||
| ### Hook API (`useKeybinding.ts:1-180`) | ||
| ### Key Files | ||
| ```typescript | ||
| useKeybinding(action, handler, { context, isActive }): | ||
| → registers handler into KeybindingContext | ||
| → on useInput: resolves by activeContexts + current context + Global priority | ||
| → resolve() returns: chord_started | chord_cancelled | unbound | match | ||
| ``` | ||
| src/commands/keybindings/index.ts — /keybindings src/commands/vim/index.ts — /vim src/keybindings/useKeybinding.ts — hook API src/keybindings/KeybindingContext.tsx — active context / pending chord src/keybindings/resolver.ts — chord resolution src/vim/transitions.ts — Vim 状态转移表 | ||
| ### `KeybindingContext` — Three State Categories (`KeybindingContext.tsx:12-116`) | ||
| ### Keybinding 不是简单 onKeyDown map | ||
| 1. **`bindings`** — current binding table used for resolution | ||
| 2. **active UI contexts** — determines which component's keybindings take priority | ||
| 3. **pending chord** — supports multi-keystroke combinations like `ctrl+k ctrl+s` | ||
| useKeybinding(action, handler, { context, isActive }): → handler 注册到 KeybindingContext → useInput 时按 activeContexts + 当前 context + Global 做优先级解析 → resolve() 支持 chord_started / chord_cancelled / unbound / match | ||
| ### Chord Resolver — Pure Function (`resolver.ts:145-219`) | ||
| src/keybindings/useKeybinding.ts:1-180 | ||
| ``` | ||
| resolveKeyWithChordState(): | ||
| 1. escape → cancel pending chord | ||
| 2. build current keystroke representation | ||
| 3. check if keystroke is a prefix of a longer chord | ||
| 4. check for exact match (last match wins) | ||
| 5. supports action=null for explicit unbind override | ||
| ``` | ||
| S | ||
| ## Vim Mode — Independent State Machine (`vim/transitions.ts:1-181`) | ||
| ## Interaction Model | ||
| ``` | ||
| transition(state, input, ctx): | ||
| States: | ||
| idle / count / operator / operatorFind / operatorTextObj | ||
| find / g / operatorG / replace / indent | ||
| ### KeybindingContext 维护的是三类状态 | ||
| handleNormalInput(): | ||
| handles: motion, operator, paste, dot-repeat, undo, | ||
| i/a/o/O insertion mode transitions | ||
| ``` | ||
| * 当前解析用的 `bindings`。 | ||
| * 当前活跃的 UI contexts,用于决定哪个组件的快捷键优先。 | ||
| * 当前 pending chord,支持 `ctrl+k ctrl+s` 这种多击组合。 | ||
| ## Architectural Position | ||
| src/keybindings/KeybindingContext.tsx:12-116 | ||
| `/keybindings` and `/vim` are not settings pages — they are subsystems: | ||
| ### Chord 解析器是纯函数逻辑 | ||
| - **`/keybindings`** — user-configurable input mapping with context priority and chord support | ||
| - **`/vim`** — modal editing state machine | ||
| resolveKeyWithChordState(): 1\. escape 取消 pending chord 2\. build current keystroke 3\. 先检查是否是更长 chord 的前缀 4\. 再检查 exact match (last one wins) 5\. 支持 action=null 的显式 unbind 覆盖 | ||
| Together with Ink UI components, they form the complete terminal interaction layer, split between render layer (`15-ink-terminal-ui`) and input semantics (this document). | ||
| src/keybindings/resolver.ts:145-219 | ||
| ### Vim 模式是独立状态机 | ||
| transition(state, input, ctx): idle / count / operator / operatorFind / operatorTextObj find / g / operatorG / replace / indent handleNormalInput(): 直接处理 motion, operator, paste, dot-repeat, undo, i/a/o/O 等插入态转换 | ||
| src/vim/transitions.ts:1-181 | ||
| ### 因此 /keybindings 和 /vim 都不只是设置页 | ||
| 一个负责用户可配置输入映射,另一个负责 modal editing。它们和 Ink UI 共同组成完整终端交互层。 | ||
| C | ||
| ## Coverage Verdict | ||
| ### 为什么 15 章不足以覆盖这一块 | ||
| * `15-ink-terminal-ui` 偏 UI 组件与渲染层。 | ||
| * 这里讲的是输入语义、交互模式与编辑状态机。 | ||
| * 如果漏掉,会低估 Claude Code 在 TUI 可用性上的投入。 | ||
| “Ink Terminal UI” 和 “Keybindings + Vim” 分开写,才符合源码真实分层: render layer vs input semantics。 | ||
| keybindings vim mode chords terminal UX | ||
| **Tags:** keybindings, vim mode, chords, terminal UX |
@@ -1,64 +0,82 @@ | ||
| [← Back to Index](index.html) | ||
| # 29. LSP & Diagnostics | ||
| # 29\. LSP & Diagnostics High | ||
| ## Key Files | ||
| 这不是单个 LSPTool,而是一套 manager、server instance、lazy startup 与 passive diagnostics 输送链 | ||
| ``` | ||
| src/tools/LSPTool/LSPTool.ts — model-facing LSP tool | ||
| src/services/lsp/manager.ts — global singleton lifecycle | ||
| src/services/lsp/LSPServerManager.ts — multi-server management + extension routing | ||
| src/services/lsp/LSPClient.ts — stdio JSON-RPC client | ||
| src/services/lsp/passiveFeedback.ts — diagnostics → attachment pipeline | ||
| ``` | ||
| R | ||
| ## `LSPTool` — Model-Facing Entry Point (`LSPTool.ts:49-216`) | ||
| ## Feature Reference | ||
| ``` | ||
| Supported operations: | ||
| goToDefinition / goToImplementation | ||
| findReferences / hover | ||
| documentSymbol / workspaceSymbol | ||
| prepareCallHierarchy / incomingCalls / outgoingCalls | ||
| ### Key Files | ||
| Properties: | ||
| isLsp = true | ||
| isConcurrencySafe = true | ||
| isReadOnly = true | ||
| still subject to read permission check | ||
| ``` | ||
| src/tools/LSPTool/LSPTool.ts — 面向模型的 LSP tool src/services/lsp/manager.ts — 全局 singleton lifecycle src/services/lsp/LSPServerManager.ts — 多 server 管理与扩展名路由 src/services/lsp/LSPClient.ts — stdio JSON-RPC client src/services/lsp/passiveFeedback.ts — diagnostics → attachment pipeline | ||
| ## `manager.ts` — Non-Blocking Initialization (`manager.ts:12-195`) | ||
| ### LSPTool 只是外层入口 | ||
| ``` | ||
| initializeLspServerManager(): | ||
| bare mode → skip entirely | ||
| otherwise: | ||
| create singleton → state = pending | ||
| async initialize() → does not block CLI startup | ||
| on success: register passive diagnostics handlers | ||
| on failure: clear instance, retain failed state | ||
| ``` | ||
| 支持操作: goToDefinition / goToImplementation findReferences / hover documentSymbol / workspaceSymbol prepareCallHierarchy / incomingCalls / outgoingCalls 特性: isLsp=true isConcurrencySafe=true isReadOnly=true 仍然要走 read permission 检查 | ||
| ## `LSPServerManager` — Multi-Language Routing (`LSPServerManager.ts:58-220`) | ||
| src/tools/LSPTool/LSPTool.ts:49-216 | ||
| - `getAllLspServers()` reads configuration | ||
| - Builds reverse mapping: `extensionToLanguage` → extension → server | ||
| - Selects server by file extension; starts server on first request (lazy) | ||
| - Maintains `openedFiles` set; synchronizes `didOpen` / `didChange` / `didSave` / `didClose` | ||
| S | ||
| ## `LSPClient` — stdio JSON-RPC Process Wrapper (`LSPClient.ts:42-210`) | ||
| ## Lifecycle | ||
| Responsibilities: | ||
| - Spawns the language server process | ||
| - Waits for confirmed successful spawn | ||
| - Establishes `vscode-jsonrpc` connection | ||
| - Monitors stderr | ||
| - Handles crash and close events | ||
| ### manager.ts 负责非阻塞初始化 | ||
| This is a full process management layer, not a toy wrapper. | ||
| initializeLspServerManager(): bare mode 直接跳过 创建 singleton → state=pending 异步 initialize(),不阻塞 CLI 启动 success 时注册 passive diagnostics handlers failure 时清空 instance,保留 failed state | ||
| ## Passive Diagnostics Pipeline (`passiveFeedback.ts:17-206`) | ||
| src/services/lsp/manager.ts:12-195 | ||
| Diagnostics are not only available on explicit tool call — they are pushed passively into the attachment system: | ||
| ### LSPServerManager 做的是多语言路由 | ||
| ``` | ||
| registerLSPNotificationHandlers(): | ||
| listen: textDocument/publishDiagnostics | ||
| → formatDiagnosticsForAttachment() | ||
| → registerPendingLSPDiagnostic() | ||
| → async delivery via attachment system | ||
| ``` | ||
| * `getAllLspServers()` 读取配置。 | ||
| * 把 `extensionToLanguage` 反向构建为 extension → server 映射。 | ||
| * 按文件后缀选择 server,首次请求时才启动。 | ||
| * 维护 openedFiles,同步 didOpen/didChange/didSave/didClose。 | ||
| This means Claude receives compiler errors and warnings as passive context without requiring an explicit LSP tool invocation. | ||
| src/services/lsp/LSPServerManager.ts:58-220 | ||
| ## System Architecture | ||
| ### LSPClient 是标准 stdio JSON-RPC 包装器 | ||
| The LSP subsystem spans: | ||
| - **Tool layer** — `LSPTool` for model-invocable code intelligence operations | ||
| - **Service layer** — `LSPServerManager` + `LSPClient` managing server processes | ||
| - **Attachment system** — `passiveFeedback.ts` routing diagnostics to model context | ||
| 它负责 spawn server、等待真正 spawn 成功、建立 `vscode-jsonrpc` connection、监听 stderr、处理 crash 与 close。这里不是 toy wrapper,而是一个相对健壮的进程管理层。 | ||
| This is an editor-grade language service integrated into a CLI agent environment. | ||
| src/services/lsp/LSPClient.ts:42-210 | ||
| ### Diagnostics 还会被被动推送到 Claude 的附件系统 | ||
| registerLSPNotificationHandlers(): 监听 textDocument/publishDiagnostics → formatDiagnosticsForAttachment() → registerPendingLSPDiagnostic() → 异步交给 attachment system 送达 | ||
| src/services/lsp/passiveFeedback.ts:17-206 | ||
| C | ||
| ## Coverage Verdict | ||
| ### 为什么这一块必须独立成章 | ||
| * 它覆盖 code intelligence 与 passive diagnostics 两条链路。 | ||
| * 它既连接工具层,也连接服务层和附件系统。 | ||
| * 仅在 `02-tool-system` 里提一句 LSPTool,会严重低估这套系统的厚度。 | ||
| Claude Code 的 LSP 实现不是“给模型一个 go-to-definition 工具”,而是“把编辑器级语言服务搬进 CLI 代理环境”。 | ||
| lsp diagnostics json-rpc code intelligence | ||
| **Tags:** lsp, diagnostics, json-rpc, code intelligence |
+1
-1
| { | ||
| "name": "claude-code-advisor", | ||
| "version": "1.0.0", | ||
| "version": "1.1.0", | ||
| "description": "Claude Code Advisor — distilled architecture docs + agent/skill for building on top of Claude Code", | ||
@@ -5,0 +5,0 @@ "bin": { |
Sorry, the diff of this file is too big to display
256780
-44.21%34
-2.86%