fix: 内存优化 — 预测性 compact 阈值、增量 lookups orphaned 修复、deferred slice 引用优化
- P0: REPL.tsx 用 useMemo 包裹 deferred messages slice,避免每次渲染创建新数组引用导致不必要的后台重渲染 - P1: 预测性 compact 阈值改用 effectiveContextWindow - growth,消除与 autocompact buffer 的双重预留;TOOL_RESULT_GROWTH_ESTIMATE 从 20K 降至 15K - P2: 增量 lookups 增加 lastAssistantMsgId 一致性检查和 orphaned server_tool_use/mcp_tool_use 扫描,防止 UI 永久 loading - P3: reactiveCompact 类型断言改为直接使用 'compact' 字面量 - docs: CLAUDE.md 统一使用 precheck 替代分散的 typecheck/lint/test 命令 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
23ea5a67d3
commit
aff7b0e853
331
CLAUDE.md
331
CLAUDE.md
|
|
@ -1,10 +1,25 @@
|
||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) and other AI coding agents when working with code in this repository.
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This is a **reverse-engineered / decompiled** version of Anthropic's official Claude Code CLI tool. The goal is to restore core functionality while trimming secondary capabilities. Many modules are stubbed or feature-flagged off. The codebase has ~1341 tsc errors from decompilation (mostly `unknown`/`never`/`{}` types) — these do **not** block Bun runtime execution.
|
This is a **reverse-engineered / decompiled** version of Anthropic's official Claude Code CLI tool. The goal is to restore core functionality while trimming secondary capabilities. Many modules are stubbed or feature-flagged off. TypeScript strict mode is enforced — **`bun run precheck` 必须零错误通过**(包含 typecheck + lint fix + test)。
|
||||||
|
|
||||||
|
## Git Commit Message Convention
|
||||||
|
|
||||||
|
使用 **Conventional Commits** 规范:
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>: <描述>
|
||||||
|
```
|
||||||
|
|
||||||
|
常见 type:`feat`、`fix`、`docs`、`chore`、`refactor`
|
||||||
|
|
||||||
|
示例:
|
||||||
|
- `feat: 添加模型 1M 上下文切换`
|
||||||
|
- `fix: 修复初次登陆的校验问题`
|
||||||
|
- `chore: remove prefetchOfficialMcpUrls call on startup`
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|
@ -21,18 +36,23 @@ bun run dev:inspect
|
||||||
# Pipe mode
|
# Pipe mode
|
||||||
echo "say hello" | bun run src/entrypoints/cli.tsx -p
|
echo "say hello" | bun run src/entrypoints/cli.tsx -p
|
||||||
|
|
||||||
# Build (code splitting, outputs dist/cli.js + ~450 chunk files)
|
# Build (code splitting, outputs dist/cli.js + chunk files)
|
||||||
bun run build
|
bun run build
|
||||||
|
|
||||||
# Test
|
# Build with Vite (alternative build pipeline)
|
||||||
bun test # run all tests
|
bun run build:vite
|
||||||
bun test src/utils/__tests__/hash.test.ts # run single file
|
|
||||||
bun test --coverage # with coverage report
|
|
||||||
|
|
||||||
# Lint & Format (Biome)
|
# Test
|
||||||
bun run lint # check only
|
bun test # run all tests
|
||||||
bun run lint:fix # auto-fix
|
bun test src/utils/__tests__/hash.test.ts # run single file
|
||||||
bun run format # format all src/
|
bun test --coverage # with coverage report
|
||||||
|
|
||||||
|
# Lint & Format (Biome) — 日常开发用 precheck 代替单独调用
|
||||||
|
bun run lint # lint check (全项目)
|
||||||
|
bun run lint:fix # auto-fix lint issues
|
||||||
|
bun run format # format all (全项目)
|
||||||
|
bun run check # lint + format check (全项目)
|
||||||
|
bun run check:fix # lint + format auto-fix
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
bun run health
|
bun run health
|
||||||
|
|
@ -40,6 +60,12 @@ bun run health
|
||||||
# Check unused exports
|
# Check unused exports
|
||||||
bun run check:unused
|
bun run check:unused
|
||||||
|
|
||||||
|
# Full check (typecheck + lint fix + test) — 任务完成后必须运行
|
||||||
|
bun run precheck
|
||||||
|
|
||||||
|
# Remote Control Server
|
||||||
|
bun run rcs
|
||||||
|
|
||||||
# Docs dev server (Mintlify)
|
# Docs dev server (Mintlify)
|
||||||
bun run docs:dev
|
bun run docs:dev
|
||||||
```
|
```
|
||||||
|
|
@ -52,14 +78,16 @@ bun run docs:dev
|
||||||
|
|
||||||
- **Runtime**: Bun (not Node.js). All imports, builds, and execution use Bun APIs.
|
- **Runtime**: Bun (not Node.js). All imports, builds, and execution use Bun APIs.
|
||||||
- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。Build 默认启用 19 个 feature(见下方 Feature Flag 段)。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。构建时会将 `vendor/audio-capture/` 和 `src/utils/vendor/ripgrep/` 复制到 `dist/vendor/` 下。
|
- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。Build 默认启用 19 个 feature(见下方 Feature Flag 段)。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。构建时会将 `vendor/audio-capture/` 和 `src/utils/vendor/ripgrep/` 复制到 `dist/vendor/` 下。
|
||||||
- **Build (Vite)**: `vite.config.ts` + `scripts/post-build.ts`,代码分割模式,chunk 输出到 `dist/chunks/`。post-build 遍历 `dist/` 和 `dist/chunks/` 下所有 `.js` 文件做 `globalThis.Bun` 解构 patch,复制 vendor 文件到 `dist/vendor/`。
|
- **Build (Vite)**: `vite.config.ts` + `scripts/post-build.ts`,chunk 输出到 `dist/chunks/`。post-build 同样复制 vendor 文件到 `dist/vendor/`。
|
||||||
- **Vendor 路径解析**: 构建后 chunk 文件位于 `dist/` 或 `dist/chunks/` 下,vendor 二进制在 `dist/vendor/`。`src/utils/distRoot.ts` 提供共享的 `distRoot` 函数,通过 `import.meta.url` 路径中 `lastIndexOf('dist')` 或 `lastIndexOf('src')` 定位根目录。`ripgrep.ts`、`computerUse/setup.ts`、`claudeInChrome/setup.ts` 均使用 `distRoot` 而非内联 `import.meta.url` 路径推算。
|
- **Vendor 路径解析**: 构建后 chunk 文件位于 `dist/` 或 `dist/chunks/` 下,vendor 二进制在 `dist/vendor/`。`src/utils/ripgrep.ts` 和 `packages/audio-capture-napi/src/index.ts` 均通过 `import.meta.url` 路径中 `lastIndexOf('dist')` 定位 dist 根目录,再拼接 `vendor/` 子路径,确保不同构建产物层级下路径一致。
|
||||||
- **为什么 Vite 必须代码分割**: Bun/JSC 会全量解析单个大 JS 文件的 bytecode 和 JIT,单文件 17MB 产物导致 RSS 暴涨至 ~1GB(Node/V8 懒解析仅需 ~220MB)。代码分割为 600+ 小 chunk 后 Bun 按需加载,`--version` RSS 从 966MB 降至 35MB,完整加载从 1GB+ 降至 ~500MB。
|
|
||||||
- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines,运行 `src/entrypoints/cli.tsx`。默认启用全部 feature。
|
- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines,运行 `src/entrypoints/cli.tsx`。默认启用全部 feature。
|
||||||
- **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform.
|
- **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform.
|
||||||
- **Monorepo**: Bun workspaces — internal packages live in `packages/` resolved via `workspace:*`.
|
- **Monorepo**: Bun workspaces — 15 个 workspace packages + 若干辅助目录 in `packages/` resolved via `workspace:*`。
|
||||||
- **Lint/Format**: Biome (`biome.json`)。`bun run lint` / `bun run lint:fix` / `bun run format`。
|
- **Lint/Format**: Biome (`biome.json`)。覆盖 `src/`、`scripts/`、`packages/` 全项目(含 `packages/@ant/`)。`bun run lint` / `bun run lint:fix` / `bun run format` / `bun run check` / `bun run check:fix`。42 条规则因 decompiled 代码被关闭,仅保留 `recommended` 基线。
|
||||||
|
- **Pre-commit**: husky + lint-staged。提交时自动对暂存文件执行 `biome check --fix`(TS/JS)和 `biome format --write`(JSON)。
|
||||||
|
- **CI Lint**: `ci.yml` 在依赖安装后、类型检查前执行 `bunx biome ci .`,lint 或格式化不达标则 CI 失败。
|
||||||
- **Defines**: 集中管理在 `scripts/defines.ts`。当前版本 `2.1.888`。
|
- **Defines**: 集中管理在 `scripts/defines.ts`。当前版本 `2.1.888`。
|
||||||
|
- **CI**: GitHub Actions — `ci.yml`(lint + 构建 + 测试)、`release-rcs.yml`(RCS 发布)、`update-contributors.yml`(自动更新贡献者)。
|
||||||
|
|
||||||
### Entry & Bootstrap
|
### Entry & Bootstrap
|
||||||
|
|
||||||
|
|
@ -67,13 +95,16 @@ bun run docs:dev
|
||||||
- `--version` / `-v` — 零模块加载
|
- `--version` / `-v` — 零模块加载
|
||||||
- `--dump-system-prompt` — feature-gated (DUMP_SYSTEM_PROMPT)
|
- `--dump-system-prompt` — feature-gated (DUMP_SYSTEM_PROMPT)
|
||||||
- `--claude-in-chrome-mcp` / `--chrome-native-host`
|
- `--claude-in-chrome-mcp` / `--chrome-native-host`
|
||||||
|
- `--computer-use-mcp` — 独立 MCP server 模式
|
||||||
- `--daemon-worker=<kind>` — feature-gated (DAEMON)
|
- `--daemon-worker=<kind>` — feature-gated (DAEMON)
|
||||||
- `remote-control` / `rc` / `bridge` — feature-gated (BRIDGE_MODE)
|
- `remote-control` / `rc` / `remote` / `sync` / `bridge` — feature-gated (BRIDGE_MODE)
|
||||||
- `daemon` — feature-gated (DAEMON)
|
- `daemon` [subcommand] — feature-gated (DAEMON)
|
||||||
- `ps` / `logs` / `attach` / `kill` / `--bg` — feature-gated (BG_SESSIONS)
|
- `ps` / `logs` / `attach` / `kill` / `--bg` — feature-gated (BG_SESSIONS)
|
||||||
|
- `new` / `list` / `reply` — Template job commands
|
||||||
|
- `environment-runner` / `self-hosted-runner` — BYOC runner
|
||||||
- `--tmux` + `--worktree` 组合
|
- `--tmux` + `--worktree` 组合
|
||||||
- 默认路径:加载 `main.tsx` 启动完整 CLI
|
- 默认路径:加载 `main.tsx` 启动完整 CLI
|
||||||
2. **`src/main.tsx`** (~4680 行) — Commander.js CLI definition。注册大量 subcommands:`mcp` (serve/add/remove/list...)、`server`、`ssh`、`open`、`auth`、`plugin`、`agents`、`auto-mode`、`doctor`、`update` 等。主 `.action()` 处理器负责权限、MCP、会话恢复、REPL/Headless 模式分发。
|
2. **`src/main.tsx`** (~6981 行) — Commander.js CLI definition。注册大量 subcommands:`mcp` (serve/add/remove/list...)、`server`、`ssh`、`open`、`auth`、`plugin`、`agents`、`auto-mode`、`doctor`、`update` 等。主 `.action()` 处理器负责权限、MCP、会话恢复、REPL/Headless 模式分发。
|
||||||
3. **`src/entrypoints/init.ts`** — One-time initialization (telemetry, config, trust dialog)。
|
3. **`src/entrypoints/init.ts`** — One-time initialization (telemetry, config, trust dialog)。
|
||||||
|
|
||||||
### Core Loop
|
### Core Loop
|
||||||
|
|
@ -85,21 +116,28 @@ bun run docs:dev
|
||||||
### API Layer
|
### API Layer
|
||||||
|
|
||||||
- **`src/services/api/claude.ts`** — Core API client. Builds request params (system prompt, messages, tools, betas), calls the Anthropic SDK streaming endpoint, and processes `BetaRawMessageStreamEvent` events.
|
- **`src/services/api/claude.ts`** — Core API client. Builds request params (system prompt, messages, tools, betas), calls the Anthropic SDK streaming endpoint, and processes `BetaRawMessageStreamEvent` events.
|
||||||
- Supports multiple providers: Anthropic direct, AWS Bedrock, Google Vertex, Azure.
|
- **7 providers**: `firstParty` (Anthropic direct), `bedrock` (AWS), `vertex` (Google Cloud), `foundry`, `openai`, `gemini`, `grok` (xAI)。
|
||||||
- Provider selection in `src/utils/model/providers.ts`.
|
- Provider selection in `src/utils/model/providers.ts`。优先级:modelType 参数 > 环境变量 > 默认 firstParty。
|
||||||
|
|
||||||
### Tool System
|
### Tool System
|
||||||
|
|
||||||
- **`src/Tool.ts`** — Tool interface definition (`Tool` type) and utilities (`findToolByName`, `toolMatchesName`).
|
- **`src/Tool.ts`** — Tool interface definition (`Tool` type) and utilities (`findToolByName`, `toolMatchesName`).
|
||||||
- **`src/tools.ts`** — Tool registry. Assembles the tool list; some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`.
|
- **`src/tools.ts`** — Tool registry. Assembles the tool list; tools are imported from `@claude-code-best/builtin-tools` package. Some tools are conditionally loaded via `feature()` flags or `process.env.USER_TYPE`.
|
||||||
- **`src/tools/<ToolName>/`** — 61 个 tool 目录(如 BashTool, FileEditTool, GrepTool, AgentTool, WebFetchTool, LSPTool, MCPTool 等)。每个 tool 包含 `name`、`description`、`inputSchema`、`call()` 及可选的 React 渲染组件。
|
- **`packages/builtin-tools/src/tools/`** — 59 个子目录(含 shared/testing 等工具目录),通过 `@claude-code-best/builtin-tools` 包导出。主要分类:
|
||||||
- **`src/tools/shared/`** — Tool 共享工具函数。
|
- **文件操作**: FileEditTool, FileReadTool, FileWriteTool, GlobTool, GrepTool
|
||||||
|
- **Shell/执行**: BashTool, PowerShellTool, REPLTool
|
||||||
|
- **Agent 系统**: AgentTool, TaskCreateTool, TaskUpdateTool, TaskListTool, TaskGetTool
|
||||||
|
- **规划**: EnterPlanModeTool, ExitPlanModeV2Tool, VerifyPlanExecutionTool
|
||||||
|
- **Web/MCP**: WebFetchTool, WebSearchTool, MCPTool, McpAuthTool
|
||||||
|
- **调度**: CronCreateTool, CronDeleteTool, CronListTool
|
||||||
|
- **其他**: LSPTool, ConfigTool, SkillTool, EnterWorktreeTool, ExitWorktreeTool 等
|
||||||
|
- **`src/tools/shared/`** / **`packages/builtin-tools/src/tools/shared/`** — Tool 共享工具函数。
|
||||||
|
|
||||||
### UI Layer (Ink)
|
### UI Layer (Ink)
|
||||||
|
|
||||||
- **`src/ink.ts`** — Ink render wrapper with ThemeProvider injection.
|
- **`src/ink.ts`** — Ink render wrapper with ThemeProvider injection.
|
||||||
- **`src/ink/`** — Custom Ink framework (forked/internal): custom reconciler, hooks (`useInput`, `useTerminalSize`, `useSearchHighlight`), virtual list rendering.
|
- **`packages/@ant/ink/`** — Custom Ink framework(forked/internal),包含 components、core、hooks、keybindings、theme、utils。注意:不是 `src/ink/`。
|
||||||
- **`src/components/`** — 大量 React 组件(170+ 项),渲染于终端 Ink 环境中。关键组件:
|
- **`src/components/`** — 149 个组件目录/文件,渲染于终端 Ink 环境中。关键组件:
|
||||||
- `App.tsx` — Root provider (AppState, Stats, FpsMetrics)
|
- `App.tsx` — Root provider (AppState, Stats, FpsMetrics)
|
||||||
- `Messages.tsx` / `MessageRow.tsx` — Conversation message rendering
|
- `Messages.tsx` / `MessageRow.tsx` — Conversation message rendering
|
||||||
- `PromptInput/` — User input handling
|
- `PromptInput/` — User input handling
|
||||||
|
|
@ -115,10 +153,45 @@ bun run docs:dev
|
||||||
- **`src/state/selectors.ts`** — State selectors.
|
- **`src/state/selectors.ts`** — State selectors.
|
||||||
- **`src/bootstrap/state.ts`** — Module-level singletons for session-global state (session ID, CWD, project root, token counts, model overrides, client type, permission mode).
|
- **`src/bootstrap/state.ts`** — Module-level singletons for session-global state (session ID, CWD, project root, token counts, model overrides, client type, permission mode).
|
||||||
|
|
||||||
|
### Workspace Packages
|
||||||
|
|
||||||
|
| Package | 说明 |
|
||||||
|
|---------|------|
|
||||||
|
| `packages/@ant/ink/` | Forked Ink 框架(components、hooks、keybindings、theme) |
|
||||||
|
| `packages/@ant/computer-use-mcp/` | Computer Use MCP server(截图/键鼠/剪贴板/应用管理) |
|
||||||
|
| `packages/@ant/computer-use-input/` | 键鼠模拟(dispatcher + darwin/win32/linux backend) |
|
||||||
|
| `packages/@ant/computer-use-swift/` | 截图 + 应用管理(dispatcher + per-platform backend) |
|
||||||
|
| `packages/@ant/claude-for-chrome-mcp/` | Chrome 浏览器控制(通过 `--chrome` 启用) |
|
||||||
|
| `packages/@ant/model-provider/` | Model provider 抽象层 |
|
||||||
|
| `packages/builtin-tools/` | 内置工具集(60 个 tool 实现,通过 `@claude-code-best/builtin-tools` 导出) |
|
||||||
|
| `packages/agent-tools/` | Agent 工具集 |
|
||||||
|
| `packages/acp-link/` | ACP 代理服务器(WebSocket → ACP agent 桥接) |
|
||||||
|
| `packages/cc-knowledge/` | Claude Code 知识库(非 workspace 包) |
|
||||||
|
| `packages/langfuse-dashboard/` | Langfuse 可观测性面板(非 workspace 包) |
|
||||||
|
| `packages/mcp-client/` | MCP 客户端库 |
|
||||||
|
| `packages/mcp-server/` | MCP 服务端库(非 workspace 包) |
|
||||||
|
| `packages/remote-control-server/` | 自托管 Remote Control Server(Docker 部署,含 Web UI)— Web UI 已重构为 React + Vite + Radix UI,支持 ACP agent 接入 |
|
||||||
|
| `packages/swarm/` | Swarm 解耦模块(非 workspace 包) |
|
||||||
|
| `packages/shell/` | Shell 抽象(非 workspace 包) |
|
||||||
|
| `packages/audio-capture-napi/` | 原生音频捕获(已恢复) |
|
||||||
|
| `packages/color-diff-napi/` | 颜色差异计算(完整实现,11 tests) |
|
||||||
|
| `packages/image-processor-napi/` | 图像处理(已恢复) |
|
||||||
|
| `packages/modifiers-napi/` | 键盘修饰键检测(macOS FFI 实现) |
|
||||||
|
| `packages/url-handler-napi/` | URL scheme 处理(环境变量 + CLI 参数读取) |
|
||||||
|
|
||||||
### Bridge / Remote Control
|
### Bridge / Remote Control
|
||||||
|
|
||||||
- **`src/bridge/`** (~35 files) — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`。
|
- **`src/bridge/`** — Remote Control / Bridge 模式。feature-gated by `BRIDGE_MODE`。包含 bridge API、会话管理、JWT 认证、消息传输、权限回调等。Entry: `bridgeMain.ts`。
|
||||||
|
- **`packages/remote-control-server/`** — 自托管 RCS,支持 Docker 部署,含 Web UI 控制面板(React 19 + Vite + Radix UI)。支持 ACP agent 通过 acp-link 接入(ACP WebSocket handler、relay handler、SSE event stream)。通过 `bun run rcs` 启动。
|
||||||
- CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge`。
|
- CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge`。
|
||||||
|
- 详见 `docs/features/remote-control-self-hosting.md`。
|
||||||
|
|
||||||
|
### ACP Protocol (Agent Client Protocol)
|
||||||
|
|
||||||
|
- **`src/services/acp/`** — ACP agent 实现,包含 `agent.ts`(AcpAgent 类)、`bridge.ts`(Claude Code ↔ ACP 桥接)、`permissions.ts`(权限处理)、`entry.ts`(入口)。
|
||||||
|
- **`packages/acp-link/`** — ACP 代理服务器,将 WebSocket 客户端桥接到 ACP agent。提供 `acp-link` CLI 命令,支持自定义端口/HTTPS/认证/会话管理、RCS 集成(REST 注册 + WS identify 两步流程)、权限模式透传(fallback: 客户端传值 > config > `ACP_PERMISSION_MODE` 环境变量)。
|
||||||
|
- ACP 权限管道改进:`createAcpCanUseTool` 统一权限流水线,`applySessionMode` 模式同步,`bypassPermissions` 可用性检测(非 root/sandbox 环境)。
|
||||||
|
- ACP Plan 可视化已支持 `session/update plan` 类型的消息展示(PlanView 组件,含进度条/状态图标/优先级标签)。
|
||||||
|
|
||||||
### Daemon Mode
|
### Daemon Mode
|
||||||
|
|
||||||
|
|
@ -131,91 +204,70 @@ bun run docs:dev
|
||||||
|
|
||||||
### Feature Flag System
|
### Feature Flag System
|
||||||
|
|
||||||
Feature flags control which functionality is enabled at runtime:
|
Feature flags control which functionality is enabled at runtime. 代码中统一通过 `import { feature } from 'bun:bundle'` 导入,调用 `feature('FLAG_NAME')` 返回 `boolean`。
|
||||||
|
|
||||||
- **在代码中使用**: 统一通过 `import { feature } from 'bun:bundle'` 导入,调用 `feature('FLAG_NAME')` 返回 `boolean`。**不要**在 `cli.tsx` 或其他文件里自己定义 `feature` 函数或覆盖这个 import。
|
**启用方式**: 环境变量 `FEATURE_<FLAG_NAME>=1`。例如 `FEATURE_BUDDY=1 bun run dev`。
|
||||||
- **启用方式**: 通过环境变量 `FEATURE_<FLAG_NAME>=1`。例如 `FEATURE_BUDDY=1 bun run dev` 启用 BUDDY 功能。
|
|
||||||
- **Dev 默认 features**: `BUDDY`、`TRANSCRIPT_CLASSIFIER`、`BRIDGE_MODE`、`AGENT_TRIGGERS_REMOTE`、`CHICAGO_MCP`、`VOICE_MODE`(见 `scripts/dev.ts`)。
|
**Build 默认 features**(19 个,见 `build.ts`):
|
||||||
- **Build 默认 features**: `AGENT_TRIGGERS_REMOTE`、`CHICAGO_MCP`、`VOICE_MODE`(见 `build.ts`)。
|
- 基础: `BUDDY`, `TRANSCRIPT_CLASSIFIER`, `BRIDGE_MODE`, `AGENT_TRIGGERS_REMOTE`, `CHICAGO_MCP`, `VOICE_MODE`
|
||||||
- **常见 flag**: `BUDDY`, `DAEMON`, `BRIDGE_MODE`, `BG_SESSIONS`, `PROACTIVE`, `KAIROS`, `VOICE_MODE`, `FORK_SUBAGENT`, `SSH_REMOTE`, `DIRECT_CONNECT`, `TEMPLATES`, `CHICAGO_MCP`, `BYOC_ENVIRONMENT_RUNNER`, `SELF_HOSTED_RUNNER`, `COORDINATOR_MODE`, `UDS_INBOX`, `LODESTONE`, `ABLATION_BASELINE` 等。
|
- 统计/缓存: `SHOT_STATS`, `PROMPT_CACHE_BREAK_DETECTION`, `TOKEN_BUDGET`
|
||||||
- **类型声明**: `src/types/internal-modules.d.ts` 中声明了 `bun:bundle` 模块的 `feature` 函数签名。
|
- P0 本地: `AGENT_TRIGGERS`, `ULTRATHINK`, `BUILTIN_EXPLORE_PLAN_AGENTS`, `LODESTONE`
|
||||||
|
- P1 API 依赖: `EXTRACT_MEMORIES`, `VERIFICATION_AGENT`, `KAIROS_BRIEF`, `AWAY_SUMMARY`, `ULTRAPLAN`
|
||||||
|
- P2: `DAEMON`
|
||||||
|
|
||||||
|
**Dev mode 默认**: 全部启用(见 `scripts/dev.ts`)。
|
||||||
|
|
||||||
|
**类型声明**: `src/types/internal-modules.d.ts` 中声明了 `bun:bundle` 模块的 `feature` 函数签名。
|
||||||
|
|
||||||
**新增功能的正确做法**: 保留 `import { feature } from 'bun:bundle'` + `feature('FLAG_NAME')` 的标准模式,在运行时通过环境变量或配置控制,不要绕过 feature flag 直接 import。
|
**新增功能的正确做法**: 保留 `import { feature } from 'bun:bundle'` + `feature('FLAG_NAME')` 的标准模式,在运行时通过环境变量或配置控制,不要绕过 feature flag 直接 import。
|
||||||
|
|
||||||
|
### Multi-API 兼容层
|
||||||
|
|
||||||
|
所有兼容层均采用流适配器模式:将第三方 API 格式转为 Anthropic 内部格式,下游代码完全不改。通过 `/login` 命令配置。
|
||||||
|
|
||||||
|
#### OpenAI 兼容层
|
||||||
|
|
||||||
|
通过 `CLAUDE_CODE_USE_OPENAI=1` 启用,支持 Ollama/DeepSeek/vLLM 等任意 OpenAI Chat Completions 协议端点。含 DeepSeek thinking mode 支持。
|
||||||
|
|
||||||
|
- **`src/services/api/openai/`** — client、消息/工具转换、流适配、模型映射
|
||||||
|
- 关键环境变量:`CLAUDE_CODE_USE_OPENAI`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL`
|
||||||
|
|
||||||
|
#### Gemini 兼容层
|
||||||
|
|
||||||
|
通过 `CLAUDE_CODE_USE_GEMINI=1` 启用。独立环境变量体系。
|
||||||
|
|
||||||
|
- **`src/services/api/gemini/`** — client、模型映射、类型定义
|
||||||
|
- 关键环境变量:`GEMINI_API_KEY`(必填)、`GEMINI_MODEL`(直接指定)、`GEMINI_DEFAULT_SONNET_MODEL`/`GEMINI_DEFAULT_OPUS_MODEL`(按能力映射)
|
||||||
|
- 模型映射优先级:`GEMINI_MODEL` > `GEMINI_DEFAULT_*_MODEL` > `ANTHROPIC_DEFAULT_*_MODEL`(已废弃) > 原样返回
|
||||||
|
|
||||||
|
#### Grok 兼容层
|
||||||
|
|
||||||
|
通过 `CLAUDE_CODE_USE_GROK=1` 启用。自定义模型映射支持 xAI Grok API。
|
||||||
|
|
||||||
|
- **`src/services/api/grok/`** — client、模型映射
|
||||||
|
|
||||||
|
详见各兼容层的 docs 文档。
|
||||||
|
|
||||||
|
### 穷鬼模式(Budget Mode)
|
||||||
|
|
||||||
|
- 通过 `/poor` 命令切换,持久化到 `settings.json`。
|
||||||
|
- 启用后跳过 `extract_memories`、`prompt_suggestion` 和 `verification_agent`,显著减少 token 消耗。
|
||||||
|
- 实现在 `src/commands/poor/poorMode.ts`。
|
||||||
|
|
||||||
### Stubbed/Deleted Modules
|
### Stubbed/Deleted Modules
|
||||||
|
|
||||||
| Module | Status |
|
| Module | Status |
|
||||||
|--------|--------|
|
|--------|--------|
|
||||||
| Computer Use (`@ant/*`) | Restored — `computer-use-swift`, `computer-use-input`, `computer-use-mcp`, `claude-for-chrome-mcp` 均有完整实现,macOS + Windows 可用,Linux 后端待完成 |
|
| Computer Use (`@ant/*`) | Restored — macOS + Windows + Linux(后端完整度不一) |
|
||||||
| `*-napi` packages | `audio-capture-napi`、`image-processor-napi` 已恢复实现;`color-diff-napi` 完整实现;`url-handler-napi`、`modifiers-napi` 仍为 stub |
|
| `*-napi` packages | 全部已恢复/实现:`audio-capture-napi`、`image-processor-napi` 已恢复;`color-diff-napi` 完整;`modifiers-napi`(macOS FFI);`url-handler-napi`(环境变量+CLI) |
|
||||||
| Voice Mode | Restored — `src/voice/`、`src/hooks/useVoiceIntegration.tsx`、`src/services/voiceStreamSTT.ts` 等,Push-to-Talk 语音输入(需 Anthropic OAuth) |
|
| Voice Mode | Restored — Push-to-Talk 语音输入(需 Anthropic OAuth) |
|
||||||
| OpenAI 兼容层 | Restored — `src/services/api/openai/`,支持 Ollama/DeepSeek/vLLM 等任意 OpenAI 协议端点,通过 `CLAUDE_CODE_USE_OPENAI=1` 启用 |
|
| OpenAI/Gemini/Grok 兼容层 | Restored |
|
||||||
|
| Remote Control Server | Restored — 自托管 RCS + Web UI |
|
||||||
| Analytics / GrowthBook / Sentry | Empty implementations |
|
| Analytics / GrowthBook / Sentry | Empty implementations |
|
||||||
| Magic Docs / LSP Server | Removed |
|
| Magic Docs / LSP Server | Restored — Magic Docs 自动更新 + LSP 服务器管理器 |
|
||||||
| Plugins / Marketplace | Removed |
|
| Plugins / Marketplace | Restored — 插件安装/卸载/启用/禁用 + Marketplace 浏览 |
|
||||||
| MCP OAuth | Simplified |
|
| MCP OAuth | Simplified |
|
||||||
|
|
||||||
### Computer Use
|
|
||||||
|
|
||||||
Feature flag `CHICAGO_MCP`,dev/build 默认启用。实现跨平台屏幕操控(macOS + Windows 可用,Linux 待完成)。
|
|
||||||
|
|
||||||
- **`packages/@ant/computer-use-mcp/`** — MCP server,注册截图/键鼠/剪贴板/应用管理工具
|
|
||||||
- **`packages/@ant/computer-use-input/`** — 键鼠模拟,dispatcher + per-platform backend(`backends/darwin.ts`、`win32.ts`、`linux.ts`)
|
|
||||||
- **`packages/@ant/computer-use-swift/`** — 截图 + 应用管理,同样 dispatcher + per-platform backend
|
|
||||||
- **`packages/@ant/claude-for-chrome-mcp/`** — Chrome 浏览器控制(独立于 Computer Use,通过 `--chrome` CLI 参数启用)
|
|
||||||
|
|
||||||
详见 `docs/features/computer-use.md`。
|
|
||||||
|
|
||||||
### Voice Mode
|
|
||||||
|
|
||||||
Feature flag `VOICE_MODE`,dev/build 默认启用。Push-to-Talk 语音输入,音频通过 WebSocket 流式传输到 Anthropic STT(Nova 3)。需要 Anthropic OAuth(非 API key)。
|
|
||||||
|
|
||||||
- **`src/voice/voiceModeEnabled.ts`** — 三层门控(feature flag + GrowthBook + OAuth auth)
|
|
||||||
- **`src/hooks/useVoice.ts`** — React hook 管理录音状态和 WebSocket 连接
|
|
||||||
- **`src/services/voiceStreamSTT.ts`** — STT WebSocket 流式传输
|
|
||||||
|
|
||||||
详见 `docs/features/voice-mode.md`。
|
|
||||||
|
|
||||||
### OpenAI 兼容层
|
|
||||||
|
|
||||||
通过 `CLAUDE_CODE_USE_OPENAI=1` 环境变量启用,支持任意 OpenAI Chat Completions 协议端点(Ollama、DeepSeek、vLLM 等)。流适配器模式:在 `queryModel()` 中将 Anthropic 格式请求转为 OpenAI 格式,再将 SSE 流转换回 `BetaRawMessageStreamEvent`,下游代码完全不改。
|
|
||||||
|
|
||||||
- **`src/services/api/openai/`** — client、消息/工具转换、流适配、模型映射
|
|
||||||
- **`src/utils/model/providers.ts`** — 添加 `'openai'` provider 类型(最高优先级)
|
|
||||||
|
|
||||||
关键环境变量:`CLAUDE_CODE_USE_OPENAI`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL`、`OPENAI_DEFAULT_OPUS_MODEL`、`OPENAI_DEFAULT_SONNET_MODEL`、`OPENAI_DEFAULT_HAIKU_MODEL`。详见 `docs/plans/openai-compatibility.md`。
|
|
||||||
|
|
||||||
### Gemini 兼容层
|
|
||||||
|
|
||||||
通过 `CLAUDE_CODE_USE_GEMINI=1` 环境变量或 `modelType: "gemini"` 设置启用,支持 Google Gemini API。独立的环境变量体系,不与 OpenAI 或 Anthropic 配置混杂。
|
|
||||||
|
|
||||||
- **`src/services/api/gemini/`** — client、模型映射、类型定义
|
|
||||||
- **`src/utils/model/providers.ts`** — 添加 `'gemini'` provider 类型
|
|
||||||
- **`src/utils/managedEnvConstants.ts`** — Gemini 专用的 managed env vars
|
|
||||||
|
|
||||||
关键环境变量:
|
|
||||||
- `CLAUDE_CODE_USE_GEMINI` - 启用 Gemini provider
|
|
||||||
- `GEMINI_API_KEY` - API 密钥(必填)
|
|
||||||
- `GEMINI_BASE_URL` - API 端点(可选,默认 `https://generativelanguage.googleapis.com/v1beta`)
|
|
||||||
- `GEMINI_MODEL` - 直接指定模型(最高优先级)
|
|
||||||
- `GEMINI_DEFAULT_HAIKU_MODEL` / `GEMINI_DEFAULT_SONNET_MODEL` / `GEMINI_DEFAULT_OPUS_MODEL` - 按能力级别映射
|
|
||||||
- `GEMINI_DEFAULT_HAIKU_MODEL_NAME` / `DESCRIPTION` / `SUPPORTED_CAPABILITIES` - 显示名称和描述
|
|
||||||
- `GEMINI_SMALL_FAST_MODEL` - 快速任务使用的模型(可选)
|
|
||||||
|
|
||||||
模型映射优先级(`src/services/api/gemini/modelMapping.ts`):
|
|
||||||
1. `GEMINI_MODEL` - 直接覆盖
|
|
||||||
2. `GEMINI_DEFAULT_*_MODEL` - 独立配置(推荐)
|
|
||||||
3. `ANTHROPIC_DEFAULT_*_MODEL` - 向后兼容 fallback(已废弃)
|
|
||||||
4. 原样返回 Anthropic 模型名
|
|
||||||
|
|
||||||
使用示例:
|
|
||||||
```bash
|
|
||||||
export CLAUDE_CODE_USE_GEMINI=1
|
|
||||||
export GEMINI_API_KEY="your-api-key"
|
|
||||||
export GEMINI_DEFAULT_SONNET_MODEL="gemini-2.5-flash"
|
|
||||||
export GEMINI_DEFAULT_OPUS_MODEL="gemini-2.5-pro"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Type Files
|
### Key Type Files
|
||||||
|
|
||||||
- **`src/types/global.d.ts`** — Declares `MACRO`, `BUILD_TARGET`, `BUILD_ENV` and internal Anthropic-only identifiers.
|
- **`src/types/global.d.ts`** — Declares `MACRO`, `BUILD_TARGET`, `BUILD_ENV` and internal Anthropic-only identifiers.
|
||||||
|
|
@ -230,16 +282,83 @@ export GEMINI_DEFAULT_OPUS_MODEL="gemini-2.5-pro"
|
||||||
- **集成测试**: `tests/integration/` — 4 个文件(cli-arguments, context-build, message-pipeline, tool-chain)
|
- **集成测试**: `tests/integration/` — 4 个文件(cli-arguments, context-build, message-pipeline, tool-chain)
|
||||||
- **共享 mock/fixture**: `tests/mocks/`(api-responses, file-system, fixtures/)
|
- **共享 mock/fixture**: `tests/mocks/`(api-responses, file-system, fixtures/)
|
||||||
- **命名**: `describe("functionName")` + `test("behavior description")`,英文
|
- **命名**: `describe("functionName")` + `test("behavior description")`,英文
|
||||||
- **Mock 模式**: 对重依赖模块使用 `mock.module()` + `await import()` 解锁(必须内联在测试文件中,不能从共享 helper 导入)
|
- **包测试**: `packages/` 下各包也有独立测试(如 `color-diff-napi` 11 tests)
|
||||||
- **当前状态**: ~1623 tests / 114 files (110 unit + 4 integration) / 0 fail(详见 `docs/testing-spec.md`)
|
|
||||||
|
### Mock 使用规范
|
||||||
|
|
||||||
|
**只 mock 有副作用的依赖链,不 mock 纯函数/纯数据模块。**
|
||||||
|
|
||||||
|
被迫 mock 的根源:`log.ts` / `debug.ts` → `bootstrap/state.ts`(模块级 `realpathSync` / `randomUUID` 副作用)。必须 mock 的模块:`log.ts`、`debug.ts`、`bun:bundle`、`settings/settings.js`、`config.ts`、`auth.ts`、第三方网络库。
|
||||||
|
|
||||||
|
**`log.ts` 和 `debug.ts` 使用共享 mock**(`tests/mocks/log.ts` / `tests/mocks/debug.ts`),不要在测试文件中内联 mock 定义。使用方式:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { logMock } from "../../../tests/mocks/log";
|
||||||
|
mock.module("src/utils/log.ts", logMock);
|
||||||
|
|
||||||
|
import { debugMock } from "../../../../tests/mocks/debug";
|
||||||
|
mock.module("src/utils/debug.ts", debugMock);
|
||||||
|
```
|
||||||
|
|
||||||
|
源文件导出变更时只需更新 `tests/mocks/` 下的对应文件,不需要逐个修改测试。
|
||||||
|
|
||||||
|
不要 mock:纯函数模块(`errors.ts`、`stringUtils.js`)、mock 值与真实实现相同的模块、mock 路径与实际 import 不匹配的模块。
|
||||||
|
|
||||||
|
路径规则:统一用 `.ts` 扩展名 + `src/*` 别名路径,禁止双重 mock 同一模块。
|
||||||
|
|
||||||
|
### 类型检查
|
||||||
|
|
||||||
|
项目使用 TypeScript strict 模式,**tsc 必须零错误**。每次修改后运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run precheck
|
||||||
|
```
|
||||||
|
|
||||||
|
**类型规范**:
|
||||||
|
- 生产代码禁止 `as any`;测试文件中 mock 数据可用 `as any`
|
||||||
|
- 类型不匹配优先用 `as unknown as SpecificType` 双重断言,或补充 interface
|
||||||
|
- 未知结构对象用 `Record<string, unknown>` 替代 `any`
|
||||||
|
- 联合类型用类型守卫(type guard)收窄,不要强转
|
||||||
|
- `msg.request` 属性访问:`const req = msg.request as Record<string, unknown>`
|
||||||
|
- Ink `color` prop:用 `as keyof Theme` 而非 `as any`
|
||||||
|
|
||||||
## Working with This Codebase
|
## Working with This Codebase
|
||||||
|
|
||||||
- **Don't try to fix all tsc errors** — they're from decompilation and don't affect runtime.
|
- **precheck must pass** — `bun run precheck`(typecheck + lint fix + test)必须零错误,任何修改都不能引入新的类型/lint/测试错误。
|
||||||
- **Feature flags** — 默认全部关闭(`feature()` 返回 `false`)。Dev/build 各有自己的默认启用列表。不要在 `cli.tsx` 中重定义 `feature` 函数。
|
- **Feature flags** — 默认全部关闭(`feature()` 返回 `false`)。Dev/build 各有自己的默认启用列表。不要在 `cli.tsx` 中重定义 `feature` 函数。
|
||||||
- **React Compiler output** — Components have decompiled memoization boilerplate (`const $ = _c(N)`). This is normal.
|
- **React Compiler output** — Components have decompiled memoization boilerplate (`const $ = _c(N)`). This is normal.
|
||||||
- **`bun:bundle` import** — `import { feature } from 'bun:bundle'` 是 Bun 内置模块,由运行时/构建器解析。不要用自定义函数替代它。
|
- **`bun:bundle` import** — `import { feature } from 'bun:bundle'` 是 Bun 内置模块,由运行时/构建器解析。不要用自定义函数替代它。**`feature()` 只能直接用在 `if` 语句或三元表达式的条件位置**(Bun 编译器限制),不能赋值给变量、不能放在箭头函数体里、不能作为 `&&` 链的一部分。正确:`if (feature('X')) {}` 或 `feature('X') ? a : b`。
|
||||||
- **`src/` path alias** — tsconfig maps `src/*` to `./src/*`. Imports like `import { ... } from 'src/utils/...'` are valid.
|
- **`src/` path alias** — tsconfig maps `src/*` to `./src/*`. Imports like `import { ... } from 'src/utils/...'` are valid.
|
||||||
- **MACRO defines** — 集中管理在 `scripts/defines.ts`。Dev mode 通过 `bun -d` 注入,build 通过 `Bun.build({ define })` 注入。修改版本号等常量只改这个文件。
|
- **MACRO defines** — 集中管理在 `scripts/defines.ts`。Dev mode 通过 `bun -d` 注入,build 通过 `Bun.build({ define })` 注入。修改版本号等常量只改这个文件。
|
||||||
- **构建产物兼容 Node.js** — `build.ts` 会自动后处理 `import.meta.require`,产物可直接用 `node dist/cli.js` 运行。
|
- **构建产物兼容 Node.js** — `build.ts` 会自动后处理 `import.meta.require`,产物可直接用 `node dist/cli.js` 运行。
|
||||||
- **Biome 配置** — 大量 lint 规则被关闭(decompiled 代码不适合严格 lint)。`.tsx` 文件用 120 行宽 + 强制分号;其他文件 80 行宽 + 按需分号。
|
- **Biome 配置** — 42 条 lint 规则因 decompiled 代码被关闭,仅保留 `recommended` 基线。格式化覆盖全项目(`src/`、`scripts/`、`packages/`,含 `packages/@ant/`)。`.tsx` 文件用 120 行宽 + 强制分号;其他文件 80 行宽 + 按需分号。JSON 格式化已启用。`.editorconfig` 与 Biome 配置对齐(2-space 缩进)。修改任何代码后应运行 `bun run precheck` 确认无类型/lint/格式/测试问题,pre-commit hook 会自动拦截不合格提交。
|
||||||
|
- **tsc 与 Biome 冲突处理** — 当 tsc 要求声明属性(赋值使用)但 biome 报 `noUnusedPrivateClassMembers`(只写不读)时,用 `// biome-ignore lint/correctness/noUnusedPrivateClassMembers: <原因>` 抑制 lint 警告,保留类型声明。`biome ci` 必须零 warnings。
|
||||||
|
- **`@ts-expect-error` 维护** — 只在下方代码确实有类型错误时保留 `@ts-expect-error`。如果类型系统已更新导致 directive 变为 unused(TS2578),直接移除注释。MACRO 替换产生的永假比较(如 `'production' === 'development'`)仍需保留 `@ts-expect-error`。
|
||||||
|
- **Ink 框架在 `packages/@ant/ink/`** — 不是 `src/ink/`(该目录不存在)。Ink 相关的组件、hooks、keybindings 都在 packages 中。
|
||||||
|
- **Provider 优先级** — `modelType` 参数 > 环境变量 > 默认 `firstParty`。新增 provider 需在 `src/utils/model/providers.ts` 注册。
|
||||||
|
|
||||||
|
## Design Context
|
||||||
|
|
||||||
|
Impeccable 设计上下文保存在 `.impeccable.md` 中。设计 Web UI(RCS 控制面板、文档站、着陆页)时必须参考该文件。
|
||||||
|
|
||||||
|
### 核心设计原则
|
||||||
|
|
||||||
|
1. **Considered over clever** — 每个设计选择都应感觉有意为之,而非追逐潮流
|
||||||
|
2. **Warmth through subtlety** — 通过橙色色调的中性色、留白布局、有温度的文案来传达温暖
|
||||||
|
3. **Density with clarity** — 技术用户需要信息密度,但不能混乱
|
||||||
|
4. **Community voice** — 设计应感觉是由使用者创造的,而非遥远的设计团队
|
||||||
|
5. **Anthropic's shadow** — 遵循 Anthropic 的设计直觉:干净的布局、充足的间距、温暖的色温
|
||||||
|
|
||||||
|
### 品牌色
|
||||||
|
|
||||||
|
- 主色:Claude Orange `#D77757`(terra cotta)
|
||||||
|
- 辅色:Claude Blue `#5769F7`
|
||||||
|
- 暗色模式使用温暖的深色表面(非冷蓝黑色)
|
||||||
|
|
||||||
|
### 目标用户
|
||||||
|
|
||||||
|
技术团队/企业,在专业工作流中使用 AI 辅助编程。友好的开源社区氛围,非企业 SaaS 风格。
|
||||||
|
|
||||||
|
### 视觉参考
|
||||||
|
|
||||||
|
Anthropic 公司的设计风格 — 干净、考究、温暖的底色。大量留白,以排版为核心。避免 AI 产品常见的设计套路(渐变文字、玻璃态、霓虹色)。
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import type { Tools } from '../Tool.js';
|
||||||
import { findToolByName } from '../Tool.js';
|
import { findToolByName } from '../Tool.js';
|
||||||
import type { AgentDefinitionsResult } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js';
|
import type { AgentDefinitionsResult } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js';
|
||||||
import type {
|
import type {
|
||||||
|
AssistantMessage,
|
||||||
Message as MessageType,
|
Message as MessageType,
|
||||||
NormalizedMessage,
|
NormalizedMessage,
|
||||||
ProgressMessage as ProgressMessageType,
|
ProgressMessage as ProgressMessageType,
|
||||||
|
|
@ -36,6 +37,7 @@ import {
|
||||||
buildMessageLookups,
|
buildMessageLookups,
|
||||||
computeMessageStructureKey,
|
computeMessageStructureKey,
|
||||||
type MessageLookups,
|
type MessageLookups,
|
||||||
|
updateMessageLookupsIncremental,
|
||||||
createAssistantMessage,
|
createAssistantMessage,
|
||||||
deriveUUID,
|
deriveUUID,
|
||||||
getMessagesAfterCompactBoundary,
|
getMessagesAfterCompactBoundary,
|
||||||
|
|
@ -516,7 +518,13 @@ const MessagesImpl = ({
|
||||||
// message content changed during streaming (text/thinking deltas). The key
|
// message content changed during streaming (text/thinking deltas). The key
|
||||||
// captures only structural info (types, IDs), so content-only deltas skip
|
// captures only structural info (types, IDs), so content-only deltas skip
|
||||||
// the rebuild entirely.
|
// the rebuild entirely.
|
||||||
const lookupsCacheRef = useRef<{ key: string; lookups: MessageLookups } | null>(null);
|
const lookupsCacheRef = useRef<{
|
||||||
|
key: string;
|
||||||
|
lookups: MessageLookups;
|
||||||
|
normalizedCount: number;
|
||||||
|
messageCount: number;
|
||||||
|
lastAssistantMsgId: string | undefined;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// Expensive message transforms — filter, reorder, group, collapse, lookups.
|
// Expensive message transforms — filter, reorder, group, collapse, lookups.
|
||||||
// All O(n) over 27k messages. Split from the renderRange slice so scrolling
|
// All O(n) over 27k messages. Split from the renderRange slice so scrolling
|
||||||
|
|
@ -587,12 +595,57 @@ const MessagesImpl = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const lookupsKey = computeMessageStructureKey(normalizedMessages, messagesToShow as MessageType[]);
|
const lookupsKey = computeMessageStructureKey(normalizedMessages, messagesToShow as MessageType[]);
|
||||||
|
const currentLastAssistantMsgId = (() => {
|
||||||
|
const lastMsg = (messagesToShow as MessageType[]).at(-1);
|
||||||
|
return lastMsg?.type === 'assistant' ? (lastMsg as AssistantMessage).message?.id : undefined;
|
||||||
|
})();
|
||||||
let lookups: MessageLookups;
|
let lookups: MessageLookups;
|
||||||
if (lookupsCacheRef.current && lookupsCacheRef.current.key === lookupsKey) {
|
if (lookupsCacheRef.current && lookupsCacheRef.current.key === lookupsKey) {
|
||||||
lookups = lookupsCacheRef.current.lookups;
|
lookups = lookupsCacheRef.current.lookups;
|
||||||
|
} else if (
|
||||||
|
lookupsCacheRef.current &&
|
||||||
|
normalizedMessages.length >= lookupsCacheRef.current.normalizedCount &&
|
||||||
|
(messagesToShow as MessageType[]).length >= lookupsCacheRef.current.messageCount &&
|
||||||
|
// If lastAssistantMsgId changed, previous "in-progress" assistant may
|
||||||
|
// now be orphaned — force a full rebuild to pick up the new status.
|
||||||
|
lookupsCacheRef.current.lastAssistantMsgId === currentLastAssistantMsgId
|
||||||
|
) {
|
||||||
|
// Try incremental update when only new messages were appended
|
||||||
|
const updated = updateMessageLookupsIncremental(
|
||||||
|
lookupsCacheRef.current.lookups,
|
||||||
|
lookupsCacheRef.current.normalizedCount,
|
||||||
|
lookupsCacheRef.current.messageCount,
|
||||||
|
normalizedMessages,
|
||||||
|
messagesToShow as MessageType[],
|
||||||
|
);
|
||||||
|
if (updated) {
|
||||||
|
lookups = updated;
|
||||||
|
lookupsCacheRef.current = {
|
||||||
|
key: lookupsKey,
|
||||||
|
lookups,
|
||||||
|
normalizedCount: normalizedMessages.length,
|
||||||
|
messageCount: (messagesToShow as MessageType[]).length,
|
||||||
|
lastAssistantMsgId: currentLastAssistantMsgId,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
lookups = buildMessageLookups(normalizedMessages, messagesToShow as MessageType[]);
|
||||||
|
lookupsCacheRef.current = {
|
||||||
|
key: lookupsKey,
|
||||||
|
lookups,
|
||||||
|
normalizedCount: normalizedMessages.length,
|
||||||
|
messageCount: (messagesToShow as MessageType[]).length,
|
||||||
|
lastAssistantMsgId: currentLastAssistantMsgId,
|
||||||
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
lookups = buildMessageLookups(normalizedMessages, messagesToShow as MessageType[]);
|
lookups = buildMessageLookups(normalizedMessages, messagesToShow as MessageType[]);
|
||||||
lookupsCacheRef.current = { key: lookupsKey, lookups };
|
lookupsCacheRef.current = {
|
||||||
|
key: lookupsKey,
|
||||||
|
lookups,
|
||||||
|
normalizedCount: normalizedMessages.length,
|
||||||
|
messageCount: (messagesToShow as MessageType[]).length,
|
||||||
|
lastAssistantMsgId: currentLastAssistantMsgId,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const hiddenMessageCount = messagesToShowNotTruncated.length - MAX_MESSAGES_TO_SHOW_IN_TRANSCRIPT_MODE;
|
const hiddenMessageCount = messagesToShowNotTruncated.length - MAX_MESSAGES_TO_SHOW_IN_TRANSCRIPT_MODE;
|
||||||
|
|
|
||||||
|
|
@ -315,7 +315,9 @@ export function isSyntheticMessage(message: Message): boolean {
|
||||||
message.type !== 'system' &&
|
message.type !== 'system' &&
|
||||||
Array.isArray(message.message?.content) &&
|
Array.isArray(message.message?.content) &&
|
||||||
message.message?.content[0]?.type === 'text' &&
|
message.message?.content[0]?.type === 'text' &&
|
||||||
SYNTHETIC_MESSAGES.has((message.message?.content[0] as { text: string }).text)
|
SYNTHETIC_MESSAGES.has(
|
||||||
|
(message.message?.content[0] as { text: string }).text,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -754,7 +756,9 @@ export function normalizeMessages(messages: Message[]): NormalizedMessage[] {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'assistant': {
|
case 'assistant': {
|
||||||
const aMsg = message as AssistantMessage
|
const aMsg = message as AssistantMessage
|
||||||
const assistantContent = Array.isArray(aMsg.message.content) ? aMsg.message.content : []
|
const assistantContent = Array.isArray(aMsg.message.content)
|
||||||
|
? aMsg.message.content
|
||||||
|
: []
|
||||||
isNewChain = isNewChain || assistantContent.length > 1
|
isNewChain = isNewChain || assistantContent.length > 1
|
||||||
return assistantContent.map((_, index) => {
|
return assistantContent.map((_, index) => {
|
||||||
const uuid = isNewChain
|
const uuid = isNewChain
|
||||||
|
|
@ -813,10 +817,17 @@ export function normalizeMessages(messages: Message[]): NormalizedMessage[] {
|
||||||
...createUserMessage({
|
...createUserMessage({
|
||||||
content: [_],
|
content: [_],
|
||||||
toolUseResult: uMsg.toolUseResult,
|
toolUseResult: uMsg.toolUseResult,
|
||||||
mcpMeta: uMsg.mcpMeta as { _meta?: Record<string, unknown>; structuredContent?: Record<string, unknown> },
|
mcpMeta: uMsg.mcpMeta as {
|
||||||
|
_meta?: Record<string, unknown>
|
||||||
|
structuredContent?: Record<string, unknown>
|
||||||
|
},
|
||||||
isMeta: uMsg.isMeta === true ? true : undefined,
|
isMeta: uMsg.isMeta === true ? true : undefined,
|
||||||
isVisibleInTranscriptOnly: uMsg.isVisibleInTranscriptOnly === true ? true : undefined,
|
isVisibleInTranscriptOnly:
|
||||||
isVirtual: (uMsg.isVirtual as boolean | undefined) === true ? true : undefined,
|
uMsg.isVisibleInTranscriptOnly === true ? true : undefined,
|
||||||
|
isVirtual:
|
||||||
|
(uMsg.isVirtual as boolean | undefined) === true
|
||||||
|
? true
|
||||||
|
: undefined,
|
||||||
timestamp: uMsg.timestamp as string | undefined,
|
timestamp: uMsg.timestamp as string | undefined,
|
||||||
imagePasteIds: imageId !== undefined ? [imageId] : undefined,
|
imagePasteIds: imageId !== undefined ? [imageId] : undefined,
|
||||||
origin: uMsg.origin as MessageOrigin | undefined,
|
origin: uMsg.origin as MessageOrigin | undefined,
|
||||||
|
|
@ -842,7 +853,9 @@ export function isToolUseRequestMessage(
|
||||||
message.type === 'assistant' &&
|
message.type === 'assistant' &&
|
||||||
// Note: stop_reason === 'tool_use' is unreliable -- it's not always set correctly
|
// Note: stop_reason === 'tool_use' is unreliable -- it's not always set correctly
|
||||||
Array.isArray(message.message?.content) &&
|
Array.isArray(message.message?.content) &&
|
||||||
(message.message?.content as Array<{type: string}>).some(_ => _.type === 'tool_use')
|
(message.message?.content as Array<{ type: string }>).some(
|
||||||
|
_ => _.type === 'tool_use',
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -856,7 +869,8 @@ export function isToolUseResultMessage(
|
||||||
return (
|
return (
|
||||||
message.type === 'user' &&
|
message.type === 'user' &&
|
||||||
((Array.isArray(message.message?.content) &&
|
((Array.isArray(message.message?.content) &&
|
||||||
(message.message?.content as Array<{type: string}>)[0]?.type === 'tool_result') ||
|
(message.message?.content as Array<{ type: string }>)[0]?.type ===
|
||||||
|
'tool_result') ||
|
||||||
Boolean(message.toolUseResult))
|
Boolean(message.toolUseResult))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -930,7 +944,8 @@ export function reorderMessagesInUI(
|
||||||
Array.isArray(message.message.content) &&
|
Array.isArray(message.message.content) &&
|
||||||
message.message.content[0]?.type === 'tool_result'
|
message.message.content[0]?.type === 'tool_result'
|
||||||
) {
|
) {
|
||||||
const toolUseID = (message.message.content[0] as ToolResultBlockParam).tool_use_id
|
const toolUseID = (message.message.content[0] as ToolResultBlockParam)
|
||||||
|
.tool_use_id
|
||||||
if (!toolUseGroups.has(toolUseID)) {
|
if (!toolUseGroups.has(toolUseID)) {
|
||||||
toolUseGroups.set(toolUseID, {
|
toolUseGroups.set(toolUseID, {
|
||||||
toolUse: null,
|
toolUse: null,
|
||||||
|
|
@ -958,7 +973,6 @@ export function reorderMessagesInUI(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
toolUseGroups.get(toolUseID)!.postHooks.push(message)
|
toolUseGroups.get(toolUseID)!.postHooks.push(message)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1062,8 +1076,10 @@ function getInProgressHookCount(
|
||||||
messages,
|
messages,
|
||||||
_ =>
|
_ =>
|
||||||
_.type === 'progress' &&
|
_.type === 'progress' &&
|
||||||
(_.data as { type: string; hookEvent: HookEvent }).type === 'hook_progress' &&
|
(_.data as { type: string; hookEvent: HookEvent }).type ===
|
||||||
(_.data as { type: string; hookEvent: HookEvent }).hookEvent === hookEvent &&
|
'hook_progress' &&
|
||||||
|
(_.data as { type: string; hookEvent: HookEvent }).hookEvent ===
|
||||||
|
hookEvent &&
|
||||||
_.parentToolUseID === toolUseID,
|
_.parentToolUseID === toolUseID,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1112,11 +1128,21 @@ export function getToolResultIDs(normalizedMessages: NormalizedMessage[]): {
|
||||||
} {
|
} {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
normalizedMessages.flatMap(_ =>
|
normalizedMessages.flatMap(_ =>
|
||||||
_.type === 'user' && Array.isArray(_.message?.content) && (_.message?.content as Array<{type:string}>)[0]?.type === 'tool_result'
|
_.type === 'user' &&
|
||||||
|
Array.isArray(_.message?.content) &&
|
||||||
|
(_.message?.content as Array<{ type: string }>)[0]?.type === 'tool_result'
|
||||||
? [
|
? [
|
||||||
[
|
[
|
||||||
((_.message?.content as Array<{type:string}>)[0] as ToolResultBlockParam).tool_use_id,
|
(
|
||||||
((_.message?.content as Array<{type:string}>)[0] as ToolResultBlockParam).is_error ?? false,
|
(
|
||||||
|
_.message?.content as Array<{ type: string }>
|
||||||
|
)[0] as ToolResultBlockParam
|
||||||
|
).tool_use_id,
|
||||||
|
(
|
||||||
|
(
|
||||||
|
_.message?.content as Array<{ type: string }>
|
||||||
|
)[0] as ToolResultBlockParam
|
||||||
|
).is_error ?? false,
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
: ([] as [string, boolean][]),
|
: ([] as [string, boolean][]),
|
||||||
|
|
@ -1137,7 +1163,9 @@ export function getSiblingToolUseIDs(
|
||||||
(_): _ is AssistantMessage =>
|
(_): _ is AssistantMessage =>
|
||||||
_.type === 'assistant' &&
|
_.type === 'assistant' &&
|
||||||
Array.isArray(_.message?.content) &&
|
Array.isArray(_.message?.content) &&
|
||||||
(_.message?.content as Array<{type:string; id?:string}>).some(block => block.type === 'tool_use' && block.id === toolUseID),
|
(_.message?.content as Array<{ type: string; id?: string }>).some(
|
||||||
|
block => block.type === 'tool_use' && block.id === toolUseID,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if (!unnormalizedMessage) {
|
if (!unnormalizedMessage) {
|
||||||
return new Set()
|
return new Set()
|
||||||
|
|
@ -1152,7 +1180,9 @@ export function getSiblingToolUseIDs(
|
||||||
return new Set(
|
return new Set(
|
||||||
siblingMessages.flatMap(_ =>
|
siblingMessages.flatMap(_ =>
|
||||||
Array.isArray(_.message?.content)
|
Array.isArray(_.message?.content)
|
||||||
? (_.message?.content as Array<{type:string; id?:string}>).filter(_ => _.type === 'tool_use').map(_ => _.id!)
|
? (_.message?.content as Array<{ type: string; id?: string }>)
|
||||||
|
.filter(_ => _.type === 'tool_use')
|
||||||
|
.map(_ => _.id!)
|
||||||
: [],
|
: [],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -1205,7 +1235,10 @@ export function buildMessageLookups(
|
||||||
const toolUseContent = content as ToolUseBlock
|
const toolUseContent = content as ToolUseBlock
|
||||||
toolUseIDs.add(toolUseContent.id)
|
toolUseIDs.add(toolUseContent.id)
|
||||||
toolUseIDToMessageID.set(toolUseContent.id, id)
|
toolUseIDToMessageID.set(toolUseContent.id, id)
|
||||||
toolUseByToolUseID.set(toolUseContent.id, content as ToolUseBlockParam)
|
toolUseByToolUseID.set(
|
||||||
|
toolUseContent.id,
|
||||||
|
content as ToolUseBlockParam,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1256,7 +1289,7 @@ export function buildMessageLookups(
|
||||||
|
|
||||||
// Build tool result lookup and resolved/errored sets
|
// Build tool result lookup and resolved/errored sets
|
||||||
if (msg.type === 'user' && Array.isArray(msg.message?.content)) {
|
if (msg.type === 'user' && Array.isArray(msg.message?.content)) {
|
||||||
for (const content of (msg.message?.content ?? [])) {
|
for (const content of msg.message?.content ?? []) {
|
||||||
if (typeof content !== 'string' && content.type === 'tool_result') {
|
if (typeof content !== 'string' && content.type === 'tool_result') {
|
||||||
const tr = content as ToolResultBlockParam
|
const tr = content as ToolResultBlockParam
|
||||||
toolResultByToolUseID.set(tr.tool_use_id, msg)
|
toolResultByToolUseID.set(tr.tool_use_id, msg)
|
||||||
|
|
@ -1269,7 +1302,7 @@ export function buildMessageLookups(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
|
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
|
||||||
for (const content of (msg.message?.content ?? [])) {
|
for (const content of msg.message?.content ?? []) {
|
||||||
if (typeof content === 'string') continue
|
if (typeof content === 'string') continue
|
||||||
// Track all server-side *_tool_result blocks (advisor, web_search,
|
// Track all server-side *_tool_result blocks (advisor, web_search,
|
||||||
// code_execution, mcp, etc.) — any block with tool_use_id is a result.
|
// code_execution, mcp, etc.) — any block with tool_use_id is a result.
|
||||||
|
|
@ -1364,6 +1397,172 @@ export function buildMessageLookups(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incrementally update lookups by processing only newly appended messages.
|
||||||
|
* Returns the same lookups object (mutated in place) if update succeeds,
|
||||||
|
* or null if a full rebuild is needed (e.g., messages were removed).
|
||||||
|
*/
|
||||||
|
export function updateMessageLookupsIncremental(
|
||||||
|
existing: MessageLookups,
|
||||||
|
previousNormalizedCount: number,
|
||||||
|
previousMessageCount: number,
|
||||||
|
normalizedMessages: NormalizedMessage[],
|
||||||
|
messages: Message[],
|
||||||
|
): MessageLookups | null {
|
||||||
|
// Safety check: only handle append-only case
|
||||||
|
if (
|
||||||
|
normalizedMessages.length < previousNormalizedCount ||
|
||||||
|
messages.length < previousMessageCount
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// No new messages — nothing to do
|
||||||
|
if (
|
||||||
|
normalizedMessages.length === previousNormalizedCount &&
|
||||||
|
messages.length === previousMessageCount
|
||||||
|
) {
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process new messages entries (pass 1: assistant tool_use blocks)
|
||||||
|
const newMessageStart = previousMessageCount
|
||||||
|
for (let i = newMessageStart; i < messages.length; i++) {
|
||||||
|
const msg = messages[i]!
|
||||||
|
if (msg.type === 'assistant') {
|
||||||
|
const aMsg = msg as AssistantMessage
|
||||||
|
const id = aMsg.message.id!
|
||||||
|
if (Array.isArray(aMsg.message.content)) {
|
||||||
|
const newToolUseIDs: string[] = []
|
||||||
|
for (const content of aMsg.message.content) {
|
||||||
|
if (typeof content !== 'string' && content.type === 'tool_use') {
|
||||||
|
const toolUseContent = content as ToolUseBlock
|
||||||
|
newToolUseIDs.push(toolUseContent.id)
|
||||||
|
existing.toolUseByToolUseID.set(
|
||||||
|
toolUseContent.id,
|
||||||
|
content as ToolUseBlockParam,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update sibling lookup: all tool_use IDs in this message share siblings
|
||||||
|
const allSiblings = new Set(newToolUseIDs)
|
||||||
|
for (const toolUseID of newToolUseIDs) {
|
||||||
|
existing.siblingToolUseIDs.set(toolUseID, allSiblings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process new normalizedMessages entries (pass 2: progress, hooks, tool results)
|
||||||
|
const newNormalizedStart = previousNormalizedCount
|
||||||
|
for (let i = newNormalizedStart; i < normalizedMessages.length; i++) {
|
||||||
|
const msg = normalizedMessages[i]!
|
||||||
|
|
||||||
|
if (msg.type === 'progress') {
|
||||||
|
const toolUseID = msg.parentToolUseID as string
|
||||||
|
const existing2 = existing.progressMessagesByToolUseID.get(toolUseID)
|
||||||
|
if (existing2) {
|
||||||
|
existing2.push(msg as ProgressMessage)
|
||||||
|
} else {
|
||||||
|
existing.progressMessagesByToolUseID.set(toolUseID, [
|
||||||
|
msg as ProgressMessage,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressData = msg.data as { type: string; hookEvent: HookEvent }
|
||||||
|
if (progressData.type === 'hook_progress') {
|
||||||
|
const hookEvent = progressData.hookEvent
|
||||||
|
let byHookEvent = existing.inProgressHookCounts.get(toolUseID)
|
||||||
|
if (!byHookEvent) {
|
||||||
|
byHookEvent = new Map()
|
||||||
|
existing.inProgressHookCounts.set(toolUseID, byHookEvent)
|
||||||
|
}
|
||||||
|
byHookEvent.set(hookEvent, (byHookEvent.get(hookEvent) ?? 0) + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'user' && Array.isArray(msg.message?.content)) {
|
||||||
|
for (const content of msg.message?.content ?? []) {
|
||||||
|
if (typeof content !== 'string' && content.type === 'tool_result') {
|
||||||
|
const tr = content as ToolResultBlockParam
|
||||||
|
existing.toolResultByToolUseID.set(tr.tool_use_id, msg)
|
||||||
|
existing.resolvedToolUseIDs.add(tr.tool_use_id)
|
||||||
|
if (tr.is_error) {
|
||||||
|
existing.erroredToolUseIDs.add(tr.tool_use_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
|
||||||
|
for (const content of msg.message?.content ?? []) {
|
||||||
|
if (typeof content === 'string') continue
|
||||||
|
if (
|
||||||
|
'tool_use_id' in content &&
|
||||||
|
typeof (content as { tool_use_id: string }).tool_use_id === 'string'
|
||||||
|
) {
|
||||||
|
existing.resolvedToolUseIDs.add(
|
||||||
|
(content as { tool_use_id: string }).tool_use_id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if ((content.type as string) === 'advisor_tool_result') {
|
||||||
|
const result = content as {
|
||||||
|
tool_use_id: string
|
||||||
|
content: { type: string }
|
||||||
|
}
|
||||||
|
if (result.content.type === 'advisor_tool_result_error') {
|
||||||
|
existing.erroredToolUseIDs.add(result.tool_use_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHookAttachmentMessage(msg)) {
|
||||||
|
const toolUseID = msg.attachment.toolUseID
|
||||||
|
const hookEvent = msg.attachment.hookEvent
|
||||||
|
const hookName = (msg.attachment as HookAttachmentWithName).hookName
|
||||||
|
if (hookName !== undefined) {
|
||||||
|
let byHookEvent = existing.resolvedHookCounts.get(toolUseID)
|
||||||
|
if (!byHookEvent) {
|
||||||
|
byHookEvent = new Map()
|
||||||
|
existing.resolvedHookCounts.set(toolUseID, byHookEvent)
|
||||||
|
}
|
||||||
|
byHookEvent.set(hookEvent, (byHookEvent.get(hookEvent) ?? 0) + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.normalizedMessageCount = normalizedMessages.length
|
||||||
|
|
||||||
|
// Mark orphaned server_tool_use / mcp_tool_use blocks as errored.
|
||||||
|
// Only scan the new normalizedMessages since the previous count —
|
||||||
|
// existing entries were already checked by a prior full build.
|
||||||
|
const lastMsg = messages.at(-1)
|
||||||
|
const lastAssistantMsgId =
|
||||||
|
lastMsg?.type === 'assistant' ? lastMsg.message?.id : undefined
|
||||||
|
for (let i = newNormalizedStart; i < normalizedMessages.length; i++) {
|
||||||
|
const msg = normalizedMessages[i]!
|
||||||
|
if (msg.type !== 'assistant') continue
|
||||||
|
const aMsg = msg as AssistantMessage
|
||||||
|
if (aMsg.message.id === lastAssistantMsgId) continue
|
||||||
|
if (!Array.isArray(aMsg.message.content)) continue
|
||||||
|
for (const content of aMsg.message.content) {
|
||||||
|
if (
|
||||||
|
typeof content !== 'string' &&
|
||||||
|
((content.type as string) === 'server_tool_use' ||
|
||||||
|
(content.type as string) === 'mcp_tool_use') &&
|
||||||
|
!existing.resolvedToolUseIDs.has((content as { id: string }).id)
|
||||||
|
) {
|
||||||
|
const id = (content as { id: string }).id
|
||||||
|
existing.resolvedToolUseIDs.add(id)
|
||||||
|
existing.erroredToolUseIDs.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute a lightweight structural fingerprint for buildMessageLookups caching.
|
* Compute a lightweight structural fingerprint for buildMessageLookups caching.
|
||||||
* Only captures information that affects lookup results (types, IDs, counts),
|
* Only captures information that affects lookup results (types, IDs, counts),
|
||||||
|
|
@ -1457,7 +1656,10 @@ export function buildSubagentLookups(
|
||||||
if (msg.type === 'assistant' && Array.isArray(msg.message.content)) {
|
if (msg.type === 'assistant' && Array.isArray(msg.message.content)) {
|
||||||
for (const content of msg.message.content) {
|
for (const content of msg.message.content) {
|
||||||
if (typeof content !== 'string' && content.type === 'tool_use') {
|
if (typeof content !== 'string' && content.type === 'tool_use') {
|
||||||
toolUseByToolUseID.set((content as ToolUseBlock).id, content as ToolUseBlockParam)
|
toolUseByToolUseID.set(
|
||||||
|
(content as ToolUseBlock).id,
|
||||||
|
content as ToolUseBlockParam,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.type === 'user' && Array.isArray(msg.message.content)) {
|
} else if (msg.type === 'user' && Array.isArray(msg.message.content)) {
|
||||||
|
|
@ -1541,9 +1743,10 @@ export function getToolUseIDs(
|
||||||
(_): _ is NormalizedAssistantMessage<BetaToolUseBlock> =>
|
(_): _ is NormalizedAssistantMessage<BetaToolUseBlock> =>
|
||||||
_.type === 'assistant' &&
|
_.type === 'assistant' &&
|
||||||
Array.isArray(_.message?.content) &&
|
Array.isArray(_.message?.content) &&
|
||||||
(_.message?.content as Array<{type:string}>)[0]?.type === 'tool_use',
|
(_.message?.content as Array<{ type: string }>)[0]?.type ===
|
||||||
|
'tool_use',
|
||||||
)
|
)
|
||||||
.map(_ => ((_.message?.content as Array<BetaToolUseBlock>)[0]).id),
|
.map(_ => (_.message?.content as Array<BetaToolUseBlock>)[0].id),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1573,7 +1776,8 @@ export function reorderAttachmentsForAPI(messages: Message[]): Message[] {
|
||||||
message.type === 'assistant' ||
|
message.type === 'assistant' ||
|
||||||
(message.type === 'user' &&
|
(message.type === 'user' &&
|
||||||
Array.isArray(message.message?.content) &&
|
Array.isArray(message.message?.content) &&
|
||||||
(message.message?.content as Array<{type:string}>)[0]?.type === 'tool_result')
|
(message.message?.content as Array<{ type: string }>)[0]?.type ===
|
||||||
|
'tool_result')
|
||||||
|
|
||||||
if (isStoppingPoint && pendingAttachments.length > 0) {
|
if (isStoppingPoint && pendingAttachments.length > 0) {
|
||||||
// Hit a stopping point — attachments stop here (go after the stopping point).
|
// Hit a stopping point — attachments stop here (go after the stopping point).
|
||||||
|
|
@ -1816,10 +2020,15 @@ export function stripToolReferenceBlocksFromUserMessage(
|
||||||
export function stripCallerFieldFromAssistantMessage(
|
export function stripCallerFieldFromAssistantMessage(
|
||||||
message: AssistantMessage,
|
message: AssistantMessage,
|
||||||
): AssistantMessage {
|
): AssistantMessage {
|
||||||
const contentArr = Array.isArray(message.message.content) ? message.message.content : []
|
const contentArr = Array.isArray(message.message.content)
|
||||||
|
? message.message.content
|
||||||
|
: []
|
||||||
const hasCallerField = contentArr.some(
|
const hasCallerField = contentArr.some(
|
||||||
block =>
|
block =>
|
||||||
typeof block !== 'string' && block.type === 'tool_use' && 'caller' in block && block.caller !== null,
|
typeof block !== 'string' &&
|
||||||
|
block.type === 'tool_use' &&
|
||||||
|
'caller' in block &&
|
||||||
|
block.caller !== null,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!hasCallerField) {
|
if (!hasCallerField) {
|
||||||
|
|
@ -2285,11 +2494,16 @@ export function normalizeMessagesForAPI(
|
||||||
...message,
|
...message,
|
||||||
message: {
|
message: {
|
||||||
...message.message,
|
...message.message,
|
||||||
content: (Array.isArray(message.message.content) ? message.message.content : []).map(block => {
|
content: (Array.isArray(message.message.content)
|
||||||
|
? message.message.content
|
||||||
|
: []
|
||||||
|
).map(block => {
|
||||||
if (typeof block === 'string') return block
|
if (typeof block === 'string') return block
|
||||||
if (block.type === 'tool_use') {
|
if (block.type === 'tool_use') {
|
||||||
const toolUseBlk = block as ToolUseBlock
|
const toolUseBlk = block as ToolUseBlock
|
||||||
const tool = tools.find(t => toolMatchesName(t, toolUseBlk.name))
|
const tool = tools.find(t =>
|
||||||
|
toolMatchesName(t, toolUseBlk.name),
|
||||||
|
)
|
||||||
const normalizedInput = tool
|
const normalizedInput = tool
|
||||||
? normalizeToolInputForAPI(
|
? normalizeToolInputForAPI(
|
||||||
tool,
|
tool,
|
||||||
|
|
@ -2310,8 +2524,9 @@ export function normalizeMessagesForAPI(
|
||||||
// When tool search is NOT enabled, strip tool-search-only fields
|
// When tool search is NOT enabled, strip tool-search-only fields
|
||||||
// like 'caller', but preserve other provider metadata attached to
|
// like 'caller', but preserve other provider metadata attached to
|
||||||
// the block (for example Gemini thought signatures on tool_use).
|
// the block (for example Gemini thought signatures on tool_use).
|
||||||
const { caller: _caller, ...toolUseRest } = block as ToolUseBlock &
|
const { caller: _caller, ...toolUseRest } =
|
||||||
Record<string, unknown> & { caller?: unknown }
|
block as ToolUseBlock &
|
||||||
|
Record<string, unknown> & { caller?: unknown }
|
||||||
return {
|
return {
|
||||||
...toolUseRest,
|
...toolUseRest,
|
||||||
type: 'tool_use' as const,
|
type: 'tool_use' as const,
|
||||||
|
|
@ -2341,7 +2556,6 @@ export function normalizeMessagesForAPI(
|
||||||
result[i] = mergeAssistantMessages(msg, normalizedMessage)
|
result[i] = mergeAssistantMessages(msg, normalizedMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2455,8 +2669,12 @@ export function mergeUserMessagesAndToolResults(
|
||||||
a: UserMessage,
|
a: UserMessage,
|
||||||
b: UserMessage,
|
b: UserMessage,
|
||||||
): UserMessage {
|
): UserMessage {
|
||||||
const lastContent = normalizeUserTextContent(a.message.content as string | ContentBlockParam[])
|
const lastContent = normalizeUserTextContent(
|
||||||
const currentContent = normalizeUserTextContent(b.message.content as string | ContentBlockParam[])
|
a.message.content as string | ContentBlockParam[],
|
||||||
|
)
|
||||||
|
const currentContent = normalizeUserTextContent(
|
||||||
|
b.message.content as string | ContentBlockParam[],
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
...a,
|
...a,
|
||||||
message: {
|
message: {
|
||||||
|
|
@ -2490,12 +2708,18 @@ function isToolResultMessage(msg: Message): boolean {
|
||||||
}
|
}
|
||||||
const content = msg.message?.content
|
const content = msg.message?.content
|
||||||
if (!content || typeof content === 'string') return false
|
if (!content || typeof content === 'string') return false
|
||||||
return (content as Array<{type:string}>).some(block => block.type === 'tool_result')
|
return (content as Array<{ type: string }>).some(
|
||||||
|
block => block.type === 'tool_result',
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeUserMessages(a: UserMessage, b: UserMessage): UserMessage {
|
export function mergeUserMessages(a: UserMessage, b: UserMessage): UserMessage {
|
||||||
const lastContent = normalizeUserTextContent(a.message.content as string | ContentBlockParam[])
|
const lastContent = normalizeUserTextContent(
|
||||||
const currentContent = normalizeUserTextContent(b.message.content as string | ContentBlockParam[])
|
a.message.content as string | ContentBlockParam[],
|
||||||
|
)
|
||||||
|
const currentContent = normalizeUserTextContent(
|
||||||
|
b.message.content as string | ContentBlockParam[],
|
||||||
|
)
|
||||||
if (feature('HISTORY_SNIP')) {
|
if (feature('HISTORY_SNIP')) {
|
||||||
// A merged message is only meta if ALL merged messages are meta. If any
|
// A merged message is only meta if ALL merged messages are meta. If any
|
||||||
// operand is real user content, the result must not be flagged isMeta
|
// operand is real user content, the result must not be flagged isMeta
|
||||||
|
|
@ -2855,9 +3079,15 @@ export function getToolUseID(message: NormalizedMessage): string | null {
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
case 'assistant': {
|
case 'assistant': {
|
||||||
const aContent = Array.isArray(message.message?.content) ? message.message?.content : []
|
const aContent = Array.isArray(message.message?.content)
|
||||||
|
? message.message?.content
|
||||||
|
: []
|
||||||
const firstBlock = aContent![0]
|
const firstBlock = aContent![0]
|
||||||
if (!firstBlock || typeof firstBlock === 'string' || firstBlock.type !== 'tool_use') {
|
if (
|
||||||
|
!firstBlock ||
|
||||||
|
typeof firstBlock === 'string' ||
|
||||||
|
firstBlock.type !== 'tool_use'
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (firstBlock as ToolUseBlock).id
|
return (firstBlock as ToolUseBlock).id
|
||||||
|
|
@ -2866,9 +3096,15 @@ export function getToolUseID(message: NormalizedMessage): string | null {
|
||||||
if (message.sourceToolUseID) {
|
if (message.sourceToolUseID) {
|
||||||
return message.sourceToolUseID as string
|
return message.sourceToolUseID as string
|
||||||
}
|
}
|
||||||
const uContent = Array.isArray(message.message?.content) ? message.message?.content : []
|
const uContent = Array.isArray(message.message?.content)
|
||||||
|
? message.message?.content
|
||||||
|
: []
|
||||||
const firstUBlock = uContent![0]
|
const firstUBlock = uContent![0]
|
||||||
if (!firstUBlock || typeof firstUBlock === 'string' || firstUBlock.type !== 'tool_result') {
|
if (
|
||||||
|
!firstUBlock ||
|
||||||
|
typeof firstUBlock === 'string' ||
|
||||||
|
firstUBlock.type !== 'tool_result'
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return (firstUBlock as ToolResultBlockParam).tool_use_id
|
return (firstUBlock as ToolResultBlockParam).tool_use_id
|
||||||
|
|
@ -2897,7 +3133,11 @@ export function filterUnresolvedToolUses(messages: Message[]): Message[] {
|
||||||
if (msg.type !== 'user' && msg.type !== 'assistant') continue
|
if (msg.type !== 'user' && msg.type !== 'assistant') continue
|
||||||
const content = msg.message?.content
|
const content = msg.message?.content
|
||||||
if (!Array.isArray(content)) continue
|
if (!Array.isArray(content)) continue
|
||||||
for (const block of content as Array<{type:string; id?:string; tool_use_id?:string}>) {
|
for (const block of content as Array<{
|
||||||
|
type: string
|
||||||
|
id?: string
|
||||||
|
tool_use_id?: string
|
||||||
|
}>) {
|
||||||
if (block.type === 'tool_use') {
|
if (block.type === 'tool_use') {
|
||||||
toolUseIds.add(block.id!)
|
toolUseIds.add(block.id!)
|
||||||
}
|
}
|
||||||
|
|
@ -2921,7 +3161,7 @@ export function filterUnresolvedToolUses(messages: Message[]): Message[] {
|
||||||
const content = msg.message?.content
|
const content = msg.message?.content
|
||||||
if (!Array.isArray(content)) return true
|
if (!Array.isArray(content)) return true
|
||||||
const toolUseBlockIds: string[] = []
|
const toolUseBlockIds: string[] = []
|
||||||
for (const b of content as Array<{type:string; id?:string}>) {
|
for (const b of content as Array<{ type: string; id?: string }>) {
|
||||||
if (b.type === 'tool_use') {
|
if (b.type === 'tool_use') {
|
||||||
toolUseBlockIds.push(b.id!)
|
toolUseBlockIds.push(b.id!)
|
||||||
}
|
}
|
||||||
|
|
@ -2940,7 +3180,7 @@ export function getAssistantMessageText(message: Message): string | null {
|
||||||
// For content blocks array, extract and concatenate text blocks
|
// For content blocks array, extract and concatenate text blocks
|
||||||
if (Array.isArray(message.message?.content)) {
|
if (Array.isArray(message.message?.content)) {
|
||||||
return (
|
return (
|
||||||
(message.message?.content as Array<{type:string; text?:string}>)
|
(message.message?.content as Array<{ type: string; text?: string }>)
|
||||||
.filter(block => block.type === 'text')
|
.filter(block => block.type === 'text')
|
||||||
.map(block => block.text ?? '')
|
.map(block => block.text ?? '')
|
||||||
.join('\n')
|
.join('\n')
|
||||||
|
|
@ -3055,11 +3295,17 @@ export function handleMessageFromStream(
|
||||||
// Capture complete thinking blocks for real-time display in transcript mode
|
// Capture complete thinking blocks for real-time display in transcript mode
|
||||||
if (message.type === 'assistant') {
|
if (message.type === 'assistant') {
|
||||||
const assistMsg = message as Message
|
const assistMsg = message as Message
|
||||||
const contentArr = Array.isArray(assistMsg.message?.content) ? assistMsg.message.content : []
|
const contentArr = Array.isArray(assistMsg.message?.content)
|
||||||
|
? assistMsg.message.content
|
||||||
|
: []
|
||||||
const thinkingBlock = contentArr.find(
|
const thinkingBlock = contentArr.find(
|
||||||
block => typeof block !== 'string' && block.type === 'thinking',
|
block => typeof block !== 'string' && block.type === 'thinking',
|
||||||
)
|
)
|
||||||
if (thinkingBlock && typeof thinkingBlock !== 'string' && thinkingBlock.type === 'thinking') {
|
if (
|
||||||
|
thinkingBlock &&
|
||||||
|
typeof thinkingBlock !== 'string' &&
|
||||||
|
thinkingBlock.type === 'thinking'
|
||||||
|
) {
|
||||||
const tb = thinkingBlock as ThinkingBlock
|
const tb = thinkingBlock as ThinkingBlock
|
||||||
onStreamingThinking?.(() => ({
|
onStreamingThinking?.(() => ({
|
||||||
thinking: tb.thinking,
|
thinking: tb.thinking,
|
||||||
|
|
@ -3082,7 +3328,28 @@ export function handleMessageFromStream(
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point, message is a stream event with an `event` property
|
// At this point, message is a stream event with an `event` property
|
||||||
const streamMsg = message as { type: string; event: { type: string; content_block: { type: string; id?: string; name?: string; input?: Record<string, unknown> }; index: number; delta: { type: string; text: string; partial_json: string; thinking: string }; [key: string]: unknown }; ttftMs?: number; [key: string]: unknown }
|
const streamMsg = message as {
|
||||||
|
type: string
|
||||||
|
event: {
|
||||||
|
type: string
|
||||||
|
content_block: {
|
||||||
|
type: string
|
||||||
|
id?: string
|
||||||
|
name?: string
|
||||||
|
input?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
index: number
|
||||||
|
delta: {
|
||||||
|
type: string
|
||||||
|
text: string
|
||||||
|
partial_json: string
|
||||||
|
thinking: string
|
||||||
|
}
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
ttftMs?: number
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
if (streamMsg.event.type === 'message_start') {
|
if (streamMsg.event.type === 'message_start') {
|
||||||
if (streamMsg.ttftMs != null) {
|
if (streamMsg.ttftMs != null) {
|
||||||
|
|
@ -3597,7 +3864,6 @@ Read the team config to discover your teammates' names. Check the task list peri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// skill_discovery handled here (not in the switch) so the 'skill_discovery'
|
// skill_discovery handled here (not in the switch) so the 'skill_discovery'
|
||||||
// string literal lives inside a feature()-guarded block. A case label can't
|
// string literal lives inside a feature()-guarded block. A case label can't
|
||||||
// be gated, but this pattern can — same approach as teammate_mailbox above.
|
// be gated, but this pattern can — same approach as teammate_mailbox above.
|
||||||
|
|
@ -3837,8 +4103,7 @@ Read the team config to discover your teammates' names. Check the task list peri
|
||||||
case 'queued_command': {
|
case 'queued_command': {
|
||||||
// Prefer explicit origin carried from the queue; fall back to commandMode
|
// Prefer explicit origin carried from the queue; fall back to commandMode
|
||||||
// for task notifications (which predate origin).
|
// for task notifications (which predate origin).
|
||||||
const origin =
|
const origin = (attachment.origin ??
|
||||||
(attachment.origin ??
|
|
||||||
(attachment.commandMode === 'task-notification'
|
(attachment.commandMode === 'task-notification'
|
||||||
? { kind: 'task-notification' }
|
? { kind: 'task-notification' }
|
||||||
: undefined)) as MessageOrigin | undefined
|
: undefined)) as MessageOrigin | undefined
|
||||||
|
|
@ -4124,7 +4389,10 @@ You have exited auto mode. The user may now want to interact more directly. You
|
||||||
case 'async_hook_response': {
|
case 'async_hook_response': {
|
||||||
const response = attachment.response as {
|
const response = attachment.response as {
|
||||||
systemMessage?: string | ContentBlockParam[]
|
systemMessage?: string | ContentBlockParam[]
|
||||||
hookSpecificOutput?: { additionalContext?: string | ContentBlockParam[]; [key: string]: unknown }
|
hookSpecificOutput?: {
|
||||||
|
additionalContext?: string | ContentBlockParam[]
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
const messages: UserMessage[] = []
|
const messages: UserMessage[] = []
|
||||||
|
|
@ -4147,7 +4415,9 @@ You have exited auto mode. The user may now want to interact more directly. You
|
||||||
) {
|
) {
|
||||||
messages.push(
|
messages.push(
|
||||||
createUserMessage({
|
createUserMessage({
|
||||||
content: response.hookSpecificOutput.additionalContext as string | ContentBlockParam[],
|
content: response.hookSpecificOutput.additionalContext as
|
||||||
|
| string
|
||||||
|
| ContentBlockParam[],
|
||||||
isMeta: true,
|
isMeta: true,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -4781,7 +5051,7 @@ export function shouldShowUserMessage(
|
||||||
export function isThinkingMessage(message: Message): boolean {
|
export function isThinkingMessage(message: Message): boolean {
|
||||||
if (message.type !== 'assistant') return false
|
if (message.type !== 'assistant') return false
|
||||||
if (!Array.isArray(message.message?.content)) return false
|
if (!Array.isArray(message.message?.content)) return false
|
||||||
return (message.message?.content as Array<{type:string}>).every(
|
return (message.message?.content as Array<{ type: string }>).every(
|
||||||
block => block.type === 'thinking' || block.type === 'redacted_thinking',
|
block => block.type === 'thinking' || block.type === 'redacted_thinking',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -4799,7 +5069,9 @@ export function countToolCalls(
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
if (!msg) continue
|
if (!msg) continue
|
||||||
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
|
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
|
||||||
const hasToolUse = (msg.message?.content as Array<{type:string; name?:string}>).some(
|
const hasToolUse = (
|
||||||
|
msg.message?.content as Array<{ type: string; name?: string }>
|
||||||
|
).some(
|
||||||
(block): block is ToolUseBlock =>
|
(block): block is ToolUseBlock =>
|
||||||
block.type === 'tool_use' && block.name === toolName,
|
block.type === 'tool_use' && block.name === toolName,
|
||||||
)
|
)
|
||||||
|
|
@ -4828,7 +5100,13 @@ export function hasSuccessfulToolCall(
|
||||||
const msg = messages[i]
|
const msg = messages[i]
|
||||||
if (!msg) continue
|
if (!msg) continue
|
||||||
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
|
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
|
||||||
const toolUse = (msg.message?.content as Array<{type:string; name?:string; id?:string}>).find(
|
const toolUse = (
|
||||||
|
msg.message?.content as Array<{
|
||||||
|
type: string
|
||||||
|
name?: string
|
||||||
|
id?: string
|
||||||
|
}>
|
||||||
|
).find(
|
||||||
(block): block is ToolUseBlock =>
|
(block): block is ToolUseBlock =>
|
||||||
block.type === 'tool_use' && block.name === toolName,
|
block.type === 'tool_use' && block.name === toolName,
|
||||||
)
|
)
|
||||||
|
|
@ -4846,7 +5124,13 @@ export function hasSuccessfulToolCall(
|
||||||
const msg = messages[i]
|
const msg = messages[i]
|
||||||
if (!msg) continue
|
if (!msg) continue
|
||||||
if (msg.type === 'user' && Array.isArray(msg.message?.content)) {
|
if (msg.type === 'user' && Array.isArray(msg.message?.content)) {
|
||||||
const toolResult = (msg.message?.content as Array<{type:string; tool_use_id?:string; is_error?:boolean}>).find(
|
const toolResult = (
|
||||||
|
msg.message?.content as Array<{
|
||||||
|
type: string
|
||||||
|
tool_use_id?: string
|
||||||
|
is_error?: boolean
|
||||||
|
}>
|
||||||
|
).find(
|
||||||
(block): block is ToolResultBlockParam =>
|
(block): block is ToolResultBlockParam =>
|
||||||
block.type === 'tool_result' &&
|
block.type === 'tool_result' &&
|
||||||
block.tool_use_id === mostRecentToolUseId,
|
block.tool_use_id === mostRecentToolUseId,
|
||||||
|
|
@ -4892,7 +5176,11 @@ function filterTrailingThinkingFromLastAssistant(
|
||||||
const content = lastMessage.message.content
|
const content = lastMessage.message.content
|
||||||
if (!Array.isArray(content)) return messages
|
if (!Array.isArray(content)) return messages
|
||||||
const lastBlock = content.at(-1)
|
const lastBlock = content.at(-1)
|
||||||
if (!lastBlock || typeof lastBlock === 'string' || !isThinkingBlock(lastBlock)) {
|
if (
|
||||||
|
!lastBlock ||
|
||||||
|
typeof lastBlock === 'string' ||
|
||||||
|
!isThinkingBlock(lastBlock)
|
||||||
|
) {
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5012,7 +5300,10 @@ export function filterWhitespaceOnlyAssistantMessages(
|
||||||
for (const message of filtered) {
|
for (const message of filtered) {
|
||||||
const prev = merged.at(-1)
|
const prev = merged.at(-1)
|
||||||
if (message.type === 'user' && prev?.type === 'user') {
|
if (message.type === 'user' && prev?.type === 'user') {
|
||||||
merged[merged.length - 1] = mergeUserMessages(prev as UserMessage, message as UserMessage) // lvalue
|
merged[merged.length - 1] = mergeUserMessages(
|
||||||
|
prev as UserMessage,
|
||||||
|
message as UserMessage,
|
||||||
|
) // lvalue
|
||||||
} else {
|
} else {
|
||||||
merged.push(message)
|
merged.push(message)
|
||||||
}
|
}
|
||||||
|
|
@ -5108,7 +5399,7 @@ export function filterOrphanedThinkingOnlyMessages(
|
||||||
const content = msg.message?.content
|
const content = msg.message?.content
|
||||||
if (!Array.isArray(content)) continue
|
if (!Array.isArray(content)) continue
|
||||||
|
|
||||||
const hasNonThinking = (content as Array<{type:string}>).some(
|
const hasNonThinking = (content as Array<{ type: string }>).some(
|
||||||
block => block.type !== 'thinking' && block.type !== 'redacted_thinking',
|
block => block.type !== 'thinking' && block.type !== 'redacted_thinking',
|
||||||
)
|
)
|
||||||
if (hasNonThinking && msg.message?.id) {
|
if (hasNonThinking && msg.message?.id) {
|
||||||
|
|
@ -5128,7 +5419,7 @@ export function filterOrphanedThinkingOnlyMessages(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if ALL content blocks are thinking blocks
|
// Check if ALL content blocks are thinking blocks
|
||||||
const allThinking = (content as Array<{type:string}>).every(
|
const allThinking = (content as Array<{ type: string }>).every(
|
||||||
block => block.type === 'thinking' || block.type === 'redacted_thinking',
|
block => block.type === 'thinking' || block.type === 'redacted_thinking',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -5307,8 +5598,15 @@ export function ensureToolResultPairing(
|
||||||
// Collect server-side tool result IDs (*_tool_result blocks have tool_use_id).
|
// Collect server-side tool result IDs (*_tool_result blocks have tool_use_id).
|
||||||
const serverResultIds = new Set<string>()
|
const serverResultIds = new Set<string>()
|
||||||
const aMsg5 = msg as AssistantMessage
|
const aMsg5 = msg as AssistantMessage
|
||||||
for (const c of aMsg5.message.content as (ContentBlockParam | ContentBlock)[]) {
|
for (const c of aMsg5.message.content as (
|
||||||
if (typeof c !== 'string' && 'tool_use_id' in c && typeof (c as { tool_use_id: string }).tool_use_id === 'string') {
|
| ContentBlockParam
|
||||||
|
| ContentBlock
|
||||||
|
)[]) {
|
||||||
|
if (
|
||||||
|
typeof c !== 'string' &&
|
||||||
|
'tool_use_id' in c &&
|
||||||
|
typeof (c as { tool_use_id: string }).tool_use_id === 'string'
|
||||||
|
) {
|
||||||
serverResultIds.add((c as { tool_use_id: string }).tool_use_id)
|
serverResultIds.add((c as { tool_use_id: string }).tool_use_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5326,7 +5624,9 @@ export function ensureToolResultPairing(
|
||||||
// has no matching *_tool_result and the API rejects with e.g. "advisor
|
// has no matching *_tool_result and the API rejects with e.g. "advisor
|
||||||
// tool use without corresponding advisor_tool_result".
|
// tool use without corresponding advisor_tool_result".
|
||||||
const seenToolUseIds = new Set<string>()
|
const seenToolUseIds = new Set<string>()
|
||||||
const assistantContent = Array.isArray(aMsg5.message.content) ? aMsg5.message.content : []
|
const assistantContent = Array.isArray(aMsg5.message.content)
|
||||||
|
? aMsg5.message.content
|
||||||
|
: []
|
||||||
const finalContent = assistantContent.filter(block => {
|
const finalContent = assistantContent.filter(block => {
|
||||||
if (typeof block === 'string') return true
|
if (typeof block === 'string') return true
|
||||||
if (block.type === 'tool_use') {
|
if (block.type === 'tool_use') {
|
||||||
|
|
@ -5338,7 +5638,8 @@ export function ensureToolResultPairing(
|
||||||
seenToolUseIds.add((block as ToolUseBlock).id)
|
seenToolUseIds.add((block as ToolUseBlock).id)
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
((block.type as string) === 'server_tool_use' || (block.type as string) === 'mcp_tool_use') &&
|
((block.type as string) === 'server_tool_use' ||
|
||||||
|
(block.type as string) === 'mcp_tool_use') &&
|
||||||
!serverResultIds.has((block as { id: string }).id)
|
!serverResultIds.has((block as { id: string }).id)
|
||||||
) {
|
) {
|
||||||
repaired = true
|
repaired = true
|
||||||
|
|
@ -5348,7 +5649,8 @@ export function ensureToolResultPairing(
|
||||||
})
|
})
|
||||||
|
|
||||||
const assistantContentChanged =
|
const assistantContentChanged =
|
||||||
finalContent.length !== (aMsg5.message.content as (ContentBlockParam | ContentBlock)[]).length
|
finalContent.length !==
|
||||||
|
(aMsg5.message.content as (ContentBlockParam | ContentBlock)[]).length
|
||||||
|
|
||||||
// If stripping orphaned server tool uses empties the content array,
|
// If stripping orphaned server tool uses empties the content array,
|
||||||
// insert a placeholder so the API doesn't reject empty assistant content.
|
// insert a placeholder so the API doesn't reject empty assistant content.
|
||||||
|
|
@ -5436,8 +5738,13 @@ export function ensureToolResultPairing(
|
||||||
let content: (ContentBlockParam | ContentBlock)[] = Array.isArray(
|
let content: (ContentBlockParam | ContentBlock)[] = Array.isArray(
|
||||||
nextUserMsg.message.content,
|
nextUserMsg.message.content,
|
||||||
)
|
)
|
||||||
? nextUserMsg.message.content as (ContentBlockParam | ContentBlock)[]
|
? (nextUserMsg.message.content as (ContentBlockParam | ContentBlock)[])
|
||||||
: [{ type: 'text' as const, text: (nextUserMsg.message.content as string | undefined) ?? '' }]
|
: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: (nextUserMsg.message.content as string | undefined) ?? '',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
// Strip orphaned tool_results and dedupe duplicate tool_result IDs
|
// Strip orphaned tool_results and dedupe duplicate tool_result IDs
|
||||||
if (orphanedIds.length > 0 || hasDuplicateToolResults) {
|
if (orphanedIds.length > 0 || hasDuplicateToolResults) {
|
||||||
|
|
@ -5513,13 +5820,18 @@ export function ensureToolResultPairing(
|
||||||
// Capture diagnostic info to help identify root cause
|
// Capture diagnostic info to help identify root cause
|
||||||
const messageTypes = messages.map((m, idx) => {
|
const messageTypes = messages.map((m, idx) => {
|
||||||
if (m.type === 'assistant') {
|
if (m.type === 'assistant') {
|
||||||
const contentArr = Array.isArray(m.message.content) ? m.message.content : []
|
const contentArr = Array.isArray(m.message.content)
|
||||||
|
? m.message.content
|
||||||
|
: []
|
||||||
const toolUses = contentArr
|
const toolUses = contentArr
|
||||||
.filter(b => typeof b !== 'string' && b.type === 'tool_use')
|
.filter(b => typeof b !== 'string' && b.type === 'tool_use')
|
||||||
.map(b => (b as ToolUseBlock | ToolUseBlockParam).id)
|
.map(b => (b as ToolUseBlock | ToolUseBlockParam).id)
|
||||||
const serverToolUses = contentArr
|
const serverToolUses = contentArr
|
||||||
.filter(
|
.filter(
|
||||||
b => typeof b !== 'string' && ((b.type as string) === 'server_tool_use' || (b.type as string) === 'mcp_tool_use'),
|
b =>
|
||||||
|
typeof b !== 'string' &&
|
||||||
|
((b.type as string) === 'server_tool_use' ||
|
||||||
|
(b.type as string) === 'mcp_tool_use'),
|
||||||
)
|
)
|
||||||
.map(b => (b as { id: string }).id)
|
.map(b => (b as { id: string }).id)
|
||||||
const parts = [
|
const parts = [
|
||||||
|
|
@ -5580,8 +5892,12 @@ export function stripAdvisorBlocks(
|
||||||
let changed = false
|
let changed = false
|
||||||
const result = messages.map(msg => {
|
const result = messages.map(msg => {
|
||||||
if (msg.type !== 'assistant') return msg
|
if (msg.type !== 'assistant') return msg
|
||||||
const content = Array.isArray(msg.message.content) ? msg.message.content : []
|
const content = Array.isArray(msg.message.content)
|
||||||
const filtered = content.filter(b => typeof b !== 'string' && !isAdvisorBlock(b))
|
? msg.message.content
|
||||||
|
: []
|
||||||
|
const filtered = content.filter(
|
||||||
|
b => typeof b !== 'string' && !isAdvisorBlock(b),
|
||||||
|
)
|
||||||
if (filtered.length === content.length) return msg
|
if (filtered.length === content.length) return msg
|
||||||
changed = true
|
changed = true
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user