diff --git a/apps/desktop/src/main/__tests__/create-session-input.test.ts b/apps/desktop/src/main/__tests__/create-session-input.test.ts index 77c6e91611..fb20348747 100644 --- a/apps/desktop/src/main/__tests__/create-session-input.test.ts +++ b/apps/desktop/src/main/__tests__/create-session-input.test.ts @@ -30,7 +30,7 @@ import { strict as assert } from 'node:assert'; import { describe, it } from 'node:test'; -import { DEEP_RESEARCH_SESSION_LABEL } from '@maka/core/explore-agent'; +import { DEEP_RESEARCH_SESSION_LABEL } from '@maka/core/deep-research'; import { DEFAULT_SESSION_NAME } from '@maka/core/session-name'; diff --git a/apps/desktop/src/main/create-session-input.ts b/apps/desktop/src/main/create-session-input.ts index c06de46351..b61ea0b931 100644 --- a/apps/desktop/src/main/create-session-input.ts +++ b/apps/desktop/src/main/create-session-input.ts @@ -43,7 +43,7 @@ import type { OrchestrationMode } from '@maka/core/orchestration'; import type { PermissionMode } from '@maka/core/permission'; -import type { SessionStartMode } from '@maka/core/explore-agent'; +import type { SessionStartMode } from '@maka/core/deep-research'; import { DEFAULT_SESSION_NAME } from '@maka/core/session-name'; import { isChatDefaultPermissionMode } from '@maka/core/settings'; @@ -52,7 +52,7 @@ import { isCollaborationMode } from '@maka/core/collaboration'; import { isOrchestrationMode } from '@maka/core/orchestration'; -import { isSessionStartMode } from '@maka/core/explore-agent'; +import { isSessionStartMode } from '@maka/core/deep-research'; /** * `unknown`, because this is an IPC boundary and the renderer's type is a diff --git a/apps/desktop/src/renderer/app-shell-command-actions.ts b/apps/desktop/src/renderer/app-shell-command-actions.ts index aa78127825..0cfcb58143 100644 --- a/apps/desktop/src/renderer/app-shell-command-actions.ts +++ b/apps/desktop/src/renderer/app-shell-command-actions.ts @@ -20,7 +20,7 @@ import { useMemo, useRef } from "react"; import type { LlmConnection } from '@maka/core/llm-connections'; import type { PermissionMode } from '@maka/core/permission'; -import type { SessionStartMode } from '@maka/core/explore-agent'; +import type { SessionStartMode } from '@maka/core/deep-research'; import type { SessionSummary, StoredMessage } from '@maka/core/session'; import type { SettingsSection, ThemePreference } from '@maka/core/settings'; import type { UiLocale } from '@maka/core/ui-locale'; diff --git a/apps/desktop/src/renderer/app-shell-session-start-actions.ts b/apps/desktop/src/renderer/app-shell-session-start-actions.ts index c87538a9cd..344ebbd502 100644 --- a/apps/desktop/src/renderer/app-shell-session-start-actions.ts +++ b/apps/desktop/src/renderer/app-shell-session-start-actions.ts @@ -17,7 +17,7 @@ * under the License. */ -import type { SessionStartMode } from '@maka/core/explore-agent'; +import type { SessionStartMode } from '@maka/core/deep-research'; import type { UiLocale } from '@maka/core/ui-locale'; import type { NavSelection } from '@maka/ui'; import type { DesktopNewTaskTarget } from '../preload/bridge-contract.js'; diff --git a/apps/desktop/src/renderer/chat-message-surface.tsx b/apps/desktop/src/renderer/chat-message-surface.tsx index 88cb5007a2..d993aca8aa 100644 --- a/apps/desktop/src/renderer/chat-message-surface.tsx +++ b/apps/desktop/src/renderer/chat-message-surface.tsx @@ -18,7 +18,7 @@ */ import { useMemo, useState, type ComponentProps, type ReactNode } from 'react'; -import { isDeepResearchSession } from '@maka/core/explore-agent'; +import { isDeepResearchSession } from '@maka/core/deep-research'; import { type LlmConnection, type ProviderType } from '@maka/core/llm-connections'; import { type OnboardingState } from '@maka/core/onboarding'; import { type SettingsSection } from '@maka/core/settings'; diff --git a/docs/archive/expert-team-implementation.md b/docs/archive/expert-team-implementation.md index 00f993adda..74b10018c6 100644 --- a/docs/archive/expert-team-implementation.md +++ b/docs/archive/expert-team-implementation.md @@ -325,7 +325,7 @@ guards: isValidPluginFolderName(无 ../\/前导.); BLOCKED{员工管理,招 2. **磁盘 artifact + JSON 指针 fan-in(两家都用)**:member 把重产物落盘,只回小 JSON 指针,orchestrator 上下文保持极小。maka 实现专家团时必须这样,否则 lead 上下文爆炸。 ## C3. maka 落地要点(详见 [expert-team-plan.md](expert-team-plan.md)) -- **单专家** = `mode:expert:` 标签 + 人格 fragment(照抄 Deep Research 的 `explore-agent.ts` 路径),人格经 `childInstruction` 边界注入(继承权限/隐私/工作区)。`defaultInitPrompt` 学 WorkBuddy 只 pre-fill composer(P0 简单)。 +- **单专家** = `mode:expert:` 标签 + 人格 fragment(照抄 Deep Research 的 `deep-research.ts` 路径),人格经 `childInstruction` 边界注入(继承权限/隐私/工作区)。`defaultInitPrompt` 学 WorkBuddy 只 pre-fill composer(P0 简单)。 - **专家团** = lead 用 `mode:expert:` + orchestrator 人格,member 注册进 `agent-catalog.ts` 的 `BUILTIN_AGENT_DEFINITIONS`(各带窄 `tools`),lead 用现有 `agent_spawn`/`spawnChildAgent` 派生(星型,非隔离,单轮 fan-out + await fan-in,回指针)。**先不做 Teams mailbox**。 - **专家中心** = 克隆 `skills-panel.tsx`(市场/内置/已安装)+ `managed-skill-sources.ts`(加 `sourceType:'remote'` 拉 COS 式 manifest,`contentSha256` 已就绪)。 - **数字同事** = P2 持久化外壳(雇佣专家 + 昵称/头像/仓库/资料库/模型),IM 绑定/云端是最重的 P3+,需评估是否值得(WorkBuddy 靠云后端 + IM CLI 子进程,成本高)。 diff --git a/docs/archive/expert-team-plan.md b/docs/archive/expert-team-plan.md index afb683f1b5..94d24e9189 100644 --- a/docs/archive/expert-team-plan.md +++ b/docs/archive/expert-team-plan.md @@ -102,7 +102,7 @@ QoderWork **没有**命名人格选择器。它的三层值得参考: | 关注点 | 复用/扩展 | 位置 | |---|---|---| -| 专家**人格注入** | `mode:` 标签 + prompt fragment(照抄 Deep Research) | `apps/desktop/src/main/system-prompt-main.ts`(已有 `childInstruction` seam);`packages/core/src/explore-agent.ts`(`mode:deep_research` 先例) | +| 专家**人格注入** | `mode:` 标签 + prompt fragment(照抄 Deep Research) | `apps/desktop/src/main/system-prompt-main.ts`(已有 `childInstruction` seam);`packages/core/src/deep-research.ts`(`mode:deep_research` 先例) | | 专家=**带工具作用域的专业 agent** | 扩展 `BUILTIN_AGENT_DEFINITIONS`(每条已带 `systemPrompt`) | `packages/runtime/src/agent-catalog.ts`、`subagent-tools.ts`(`agent_spawn`/`SessionManager.spawnChildAgent`) | | 专家**目录 + 按需安装** | 克隆 managed/bundled 技能目录 | `apps/desktop/src/main/managed-skill-sources.ts`、`bundled-skill-catalog.generated.ts`、`skills.ts`(install→`skill.lock.json`→baseline→update diff,PR #842) | | 专家**定义文件格式** | 复用 SKILL.md frontmatter 解析 | `packages/runtime/src/skills.ts`(`parseSkillFrontMatter`) | diff --git a/docs/astryx-surface-file-inventory.md b/docs/astryx-surface-file-inventory.md index 962402d4e3..ced83f6d26 100644 --- a/docs/astryx-surface-file-inventory.md +++ b/docs/astryx-surface-file-inventory.md @@ -5,7 +5,7 @@ Each row is one on-disk product surface file. Regenerated inventory must stay in Wiki bar: Design Conventions · API Use-the-System · Theming · Container Padding. -**Totals:** 224 files — blocker 0, polish 1, aligned 223. +**Totals:** 223 files — blocker 0, polish 1, aligned 222. ## Exclusions (explicit) @@ -242,7 +242,6 @@ Wiki bar: Design Conventions · API Use-the-System · Theming · Container Paddi | `packages/ui/src/titlebar-session-identity.tsx` | shell-chrome-or-panel | BreadcrumbItem, Breadcrumbs | aligned — uses Astryx (BreadcrumbItem, Breadcrumbs) | aligned | | `packages/ui/src/toast.tsx` | ui-composition | Button, HStack, Text, VStack | aligned — uses Astryx (Button, HStack, Text, VStack) | aligned | | `packages/ui/src/tool-activity.tsx` | ui-composition | Banner, Button, ChatToolCalls, List, ListItem, Text | aligned — uses Astryx (Banner, Button, ChatToolCalls, List, ListItem, Text) | aligned | -| `packages/ui/src/tool-activity/agent-preview.tsx` | ui-composition | Button | aligned — uses Astryx (Button) | aligned | | `packages/ui/src/tool-activity/diff-code-preview.tsx` | ui-composition | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `packages/ui/src/tool-activity/tool-code-block.tsx` | ui-composition | none | aligned — no raw controls; no Astryx JSX usage | aligned | | `packages/ui/src/tool-activity/tool-result-preview.tsx` | ui-composition | Button | aligned — uses Astryx (Button) | aligned | diff --git a/docs/astryx-surface-file-inventory.paths b/docs/astryx-surface-file-inventory.paths index 6131ae8c61..89c798d6b0 100644 --- a/docs/astryx-surface-file-inventory.paths +++ b/docs/astryx-surface-file-inventory.paths @@ -214,7 +214,6 @@ packages/ui/src/task-ledger-panel.tsx packages/ui/src/titlebar-session-identity.tsx packages/ui/src/toast.tsx packages/ui/src/tool-activity.tsx -packages/ui/src/tool-activity/agent-preview.tsx packages/ui/src/tool-activity/diff-code-preview.tsx packages/ui/src/tool-activity/tool-code-block.tsx packages/ui/src/tool-activity/tool-result-preview.tsx diff --git a/packages/cli/src/__tests__/pi-transcript.test.ts b/packages/cli/src/__tests__/pi-transcript.test.ts index a72fd0bc2d..b74ed7bdd2 100644 --- a/packages/cli/src/__tests__/pi-transcript.test.ts +++ b/packages/cli/src/__tests__/pi-transcript.test.ts @@ -4292,8 +4292,8 @@ describe('Maka Pi TUI transcript', () => { state, event({ type: 'tool_start', - toolUseId: 'explore-intent', - toolName: 'ExploreAgent', + toolUseId: 'grep-intent', + toolName: 'Grep', args: undefined, intent: ' inspect render entry with sk-1234567890abcdef ', }), diff --git a/packages/cli/src/pi-transcript-format.ts b/packages/cli/src/pi-transcript-format.ts index a66edd1d6d..420b6a55b8 100644 --- a/packages/cli/src/pi-transcript-format.ts +++ b/packages/cli/src/pi-transcript-format.ts @@ -88,13 +88,6 @@ export function formatToolResultContent(content: ToolResultContent): string { ].join('\n\n'); case 'web_search_error': return content.message; - case 'explore_agent': - return ( - content.report ?? - content.summary ?? - content.message ?? - `Inspected ${content.filesInspected} files` - ); case 'subagent': return content.summary; case 'agent_swarm': { diff --git a/packages/core/package.json b/packages/core/package.json index 8e979f2e18..bab15ee8f4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -74,7 +74,7 @@ "./deep-research-client-progress": "./dist/deep-research-client-progress.js", "./daily-review": "./dist/daily-review.js", "./work-board": "./dist/work-board.js", - "./explore-agent": "./dist/explore-agent.js", + "./deep-research": "./dist/deep-research.js", "./long-term-memory": "./dist/long-term-memory.js", "./local-memory": "./dist/local-memory.js", "./web-search": "./dist/web-search.js", diff --git a/packages/core/src/__tests__/explore-agent.test.ts b/packages/core/src/__tests__/deep-research-session.test.ts similarity index 100% rename from packages/core/src/__tests__/explore-agent.test.ts rename to packages/core/src/__tests__/deep-research-session.test.ts diff --git a/packages/core/src/__tests__/deep-research.test.ts b/packages/core/src/__tests__/deep-research.test.ts new file mode 100644 index 0000000000..d121adc850 --- /dev/null +++ b/packages/core/src/__tests__/deep-research.test.ts @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { buildDeepResearchSystemPromptFragment } from '../deep-research.js'; + +test('Deep Research directs inspection through the standard read-only tool surface', () => { + const prompt = buildDeepResearchSystemPromptFragment(); + + for (const toolName of ['Read', 'Glob', 'Grep', 'WebSearch']) { + assert.match(prompt, new RegExp(`\\b${toolName}\\b`)); + } + assert.doesNotMatch(prompt, /ExploreAgent/); +}); diff --git a/packages/core/src/__tests__/tool-result-record-schema.test.ts b/packages/core/src/__tests__/tool-result-record-schema.test.ts index dfa8d0f8f5..cb0b4be59d 100644 --- a/packages/core/src/__tests__/tool-result-record-schema.test.ts +++ b/packages/core/src/__tests__/tool-result-record-schema.test.ts @@ -193,6 +193,125 @@ describe('retired permission modes in stored subagent results', () => { }); }); +describe('retired ExploreAgent results in stored transcripts', () => { + const stored = { + kind: 'explore_agent', + ok: true, + mode: 'read_only', + objective: 'Trace the session lifecycle.', + roots: ['packages/runtime'], + queries: ['SessionManager'], + filesInspected: 3, + filesSkipped: 1, + bytesRead: 512, + progress: ['Inspected the runtime entry point.'], + candidateFiles: [], + matches: [], + notes: [], + report: 'SessionManager owns the lifecycle.', + } as const; + + test('folds the legacy result to ordinary historical text at every persisted read boundary', () => { + const expected = { kind: 'text', text: 'SessionManager owns the lifecycle.' }; + + assert.deepEqual( + decodePersistedToolResultContent( + markPersisted(stored as unknown as ToolResultContent), + ), + expected, + ); + assert.deepEqual(toolResultContent(decodePersistedMessage(storedToolResult(stored))), expected); + }); + + test('accepts the earliest persisted shape before progress was added', () => { + const earliest = { + kind: 'explore_agent', + ok: true, + mode: 'read_only', + objective: 'Trace the session lifecycle.', + roots: ['packages/runtime'], + queries: ['SessionManager'], + filesInspected: 3, + filesSkipped: 1, + bytesRead: 512, + candidateFiles: [], + matches: [], + notes: [], + } as const; + const expected = { kind: 'text', text: 'Inspected 3 files' }; + + assert.deepEqual( + decodePersistedToolResultContent( + markPersisted(earliest as unknown as ToolResultContent), + ), + expected, + ); + assert.deepEqual( + toolResultContent(decodePersistedMessage(storedToolResult(earliest))), + expected, + ); + }); + + test('uses a non-empty failure summary when the legacy report is empty', () => { + const failed = { + ...stored, + ok: false, + terminalStatus: 'failed', + report: '', + summary: '未完成:目标无效。', + reason: 'invalid_objective', + message: '目标无效。', + } as const; + + assert.deepEqual( + decodePersistedToolResultContent( + markPersisted(failed as unknown as ToolResultContent), + ), + { kind: 'text', text: '未完成:目标无效。' }, + ); + }); + + test('ignores retired structured detail after establishing legacy provenance', () => { + const malformedDetails = { + ...stored, + report: undefined, + progress: { invalid: true }, + matches: 'not an array', + filesInspected: undefined, + removedInLaterVersions: true, + }; + + assert.deepEqual( + decodePersistedToolResultContent( + markPersisted(malformedDetails as unknown as ToolResultContent), + ), + { kind: 'text', text: 'Historical repository scan result' }, + ); + }); + + test('requires stable legacy provenance at persisted read boundaries', () => { + assert.throws( + () => + decodePersistedToolResultContent( + markPersisted({ + kind: 'explore_agent', + ok: true, + mode: 'write_enabled', + } as unknown as ToolResultContent), + ), + /Invalid tool result content/, + ); + }); + + test('rejects the retired result kind at canonical live boundaries', () => { + assert.throws(() => decodeCanonicalToolResultContent(stored), /Invalid tool result content/); + assert.throws( + () => decodeCanonicalMessage(storedToolResult(stored)), + /Invalid tool result content/, + ); + }); +}); + function storedToolResult(content: unknown) { return { type: 'tool_result', diff --git a/packages/core/src/deep-research-client-progress.ts b/packages/core/src/deep-research-client-progress.ts index cc46a50096..19d628ba6e 100644 --- a/packages/core/src/deep-research-client-progress.ts +++ b/packages/core/src/deep-research-client-progress.ts @@ -17,7 +17,7 @@ * under the License. */ -import { buildDeepResearchImplementationPrompt } from './explore-agent.js'; +import { buildDeepResearchImplementationPrompt } from './deep-research.js'; import { DEEP_RESEARCH_CLIENT_IMPLEMENTATION_PROMPT_MAX_BYTES, DEEP_RESEARCH_CLIENT_OBJECTIVE_MAX_BYTES, diff --git a/packages/core/src/explore-agent.ts b/packages/core/src/deep-research.ts similarity index 90% rename from packages/core/src/explore-agent.ts rename to packages/core/src/deep-research.ts index 2999b6cad2..9f480ed920 100644 --- a/packages/core/src/explore-agent.ts +++ b/packages/core/src/deep-research.ts @@ -17,13 +17,7 @@ * under the License. */ -/** - * Read-only deep research mode (external reference design). - * - * Deep research is visible and bounded. The session profile pins - * permissionMode=explore and may use the read-only ExploreAgent tool for - * scoped local investigation; it is not a hidden autonomous writer. - */ +/** Read-only Deep Research session semantics and prompt contract. */ import type { DeepResearchRun } from './deep-research-run.js'; import type { PermissionMode } from './permission.js'; @@ -231,24 +225,12 @@ export function buildDeepResearchImplementationPrompt(run: DeepResearchRun): str ); } -export function buildDeepResearchSystemPromptFragment( - options: { readonly exploreAgentAvailable?: boolean } = {}, -): string { - const exploreAgentAvailable = options.exploreAgentAvailable ?? true; +export function buildDeepResearchSystemPromptFragment(): string { return [ 'Deep research mode is active for this session.', '', 'Mode contract:', - exploreAgentAvailable - ? '- Inspect first. Prefer Read, Glob, Grep, WebSearch, and ExploreAgent.' - : '- Inspect first. Prefer Read, Glob, Grep, and WebSearch.', - ...(exploreAgentAvailable - ? [ - '- Use ExploreAgent only for a separate, self-contained local investigation that benefits from a bounded read-only worker. Keep synthesis and final judgment in the main thread.', - '- Do not use ExploreAgent just because it is available. If the next step is a known file, a specific symbol, package scripts, test setup, config, or 1-3 obvious files, inspect directly in the main thread.', - '- When using ExploreAgent, bound the prompt with a goal, relevant paths or keywords, what to ignore, a stopping condition, and exactly what evidence the worker should return.', - ] - : []), + '- Inspect first. Prefer Read, Glob, Grep, and WebSearch.', '- Do not write, edit, delete, move, or rename user project files; do not install, run migrations, start services, or send network requests unless the user explicitly leaves research mode.', '- The deep_research_* tools are the one write exception: they only update Maka-owned research artifacts and an append-only workspace ledger, never the user project.', '- If implementation is needed, produce a concrete plan with files, risks, and verification commands instead of modifying files.', diff --git a/packages/core/src/events.ts b/packages/core/src/events.ts index dc9c11c41d..23cd9b451b 100644 --- a/packages/core/src/events.ts +++ b/packages/core/src/events.ts @@ -812,45 +812,6 @@ export type ToolResultContent = message: string; credentialSource?: string; } - | { - kind: 'explore_agent'; - ok: boolean; - partial?: boolean; - terminalStatus?: 'completed' | 'completed_empty' | 'failed' | 'canceled' | 'canceled_partial'; - mode: 'read_only'; - objective: string; - roots: string[]; - queries: string[]; - ignoredPaths?: string[]; - stoppingCondition?: string; - limitReasons?: ReadonlyArray< - 'candidate_budget' | 'file_budget' | 'match_budget' | 'byte_budget' - >; - filesDiscovered?: number; - filesInspected: number; - filesSkipped: number; - sensitiveFilesSkipped?: number; - bytesRead: number; - startedAt?: number; - completedAt?: number; - durationMs?: number; - progress: string[]; - recentEvents?: ReadonlyArray<{ type: string; at: number; message: string }>; - evidence?: ReadonlyArray<{ - type: 'match' | 'candidate'; - path: string; - line?: number; - label: string; - score?: number; - }>; - summary?: string; - report?: string; - candidateFiles: ReadonlyArray<{ path: string; score: number; reasons: string[] }>; - matches: ReadonlyArray<{ path: string; line: number; query: string; snippet: string }>; - notes: string[]; - reason?: 'invalid_objective' | 'invalid_root' | 'no_readable_roots' | 'aborted'; - message?: string; - } | { kind: 'subagent'; childSessionId?: string; diff --git a/packages/core/src/runtime-inputs.ts b/packages/core/src/runtime-inputs.ts index d401076dda..a3ac133ed2 100644 --- a/packages/core/src/runtime-inputs.ts +++ b/packages/core/src/runtime-inputs.ts @@ -34,7 +34,7 @@ import type { PermissionMode } from './permission.js'; import type { ThinkingLevel } from './model-thinking.js'; import type { CollaborationMode } from './collaboration.js'; import type { OrchestrationMode, TurnOrchestration } from './orchestration.js'; -import type { SessionStartMode } from './explore-agent.js'; +import type { SessionStartMode } from './deep-research.js'; import type { SubagentWorkspaceBinding } from './subagent-workspace.js'; import type { ToolMode } from './tool-mode.js'; import type { TurnOrigin } from './turn-origin.js'; diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 51ea84dfb5..686ad8493c 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -50,7 +50,7 @@ import { markPersisted, type PersistedValue } from './persisted-value.js'; import type { SubagentWorkspaceBinding } from './subagent-workspace.js'; import { decodeTurnOrigin, type TurnOrigin } from './turn-origin.js'; -export { DEEP_RESEARCH_SESSION_LABEL, isDeepResearchSession } from './explore-agent.js'; +export { DEEP_RESEARCH_SESSION_LABEL, isDeepResearchSession } from './deep-research.js'; /** Runtime execution states. Archive visibility is represented by `isArchived`. */ export const SESSION_STATUSES = [ diff --git a/packages/core/src/tool-result-record-schema.ts b/packages/core/src/tool-result-record-schema.ts index cf1499dce7..5e74492a9a 100644 --- a/packages/core/src/tool-result-record-schema.ts +++ b/packages/core/src/tool-result-record-schema.ts @@ -36,7 +36,6 @@ import { } from './record-schema.js'; type Result = Extract; -type ExploreResult = Result<'explore_agent'>; type AgentSwarmResult = Result<'agent_swarm'>; type RiveResult = Result<'rive_workflow'>; @@ -86,61 +85,6 @@ const WEB_SEARCH_ERROR_SHAPE = defineObjectShape>()( ['kind', 'ok', 'provider', 'reason', 'message'], ['query', 'credentialSource'], ); -const EXPLORE_SHAPE = defineObjectShape()( - [ - 'kind', - 'ok', - 'mode', - 'objective', - 'roots', - 'queries', - 'filesInspected', - 'filesSkipped', - 'bytesRead', - 'progress', - 'candidateFiles', - 'matches', - 'notes', - ], - [ - 'partial', - 'terminalStatus', - 'ignoredPaths', - 'stoppingCondition', - 'limitReasons', - 'filesDiscovered', - 'sensitiveFilesSkipped', - 'startedAt', - 'completedAt', - 'durationMs', - 'recentEvents', - 'evidence', - 'summary', - 'report', - 'reason', - 'message', - ], -); -type ExploreRecentEvent = NonNullable[number]; -type ExploreEvidence = NonNullable[number]; -type ExploreCandidate = ExploreResult['candidateFiles'][number]; -type ExploreMatch = ExploreResult['matches'][number]; -const EXPLORE_RECENT_EVENT_SHAPE = defineObjectShape()( - ['type', 'at', 'message'], - [], -); -const EXPLORE_EVIDENCE_SHAPE = defineObjectShape()( - ['type', 'path', 'label'], - ['line', 'score'], -); -const EXPLORE_CANDIDATE_SHAPE = defineObjectShape()( - ['path', 'score', 'reasons'], - [], -); -const EXPLORE_MATCH_SHAPE = defineObjectShape()( - ['path', 'line', 'query', 'snippet'], - [], -); const SUBAGENT_SHAPE = defineObjectShape>()( ['kind', 'agentName', 'turnId', 'status', 'permissionMode', 'summary', 'artifactIds'], [ @@ -223,6 +167,24 @@ export function decodePersistedToolResultContent( persisted: PersistedValue, ): ToolResultContent { const value = persisted as unknown; + if (isRecord(value) && value.kind === 'explore_agent') { + if (value.mode !== 'read_only' || typeof value.ok !== 'boolean') { + throw new Error('Invalid tool result content'); + } + const summary = firstNonEmptyString( + typeof value.report === 'string' ? value.report : undefined, + typeof value.summary === 'string' ? value.summary : undefined, + typeof value.message === 'string' ? value.message : undefined, + ); + return { + kind: 'text', + text: + summary ?? + (isFiniteNumber(value.filesInspected) + ? `Inspected ${value.filesInspected} files` + : 'Historical repository scan result'), + }; + } if (!isRecord(value) || value.kind !== 'subagent') { return decodeCanonicalToolResultContent(value); } @@ -230,6 +192,10 @@ export function decodePersistedToolResultContent( return decodeCanonicalToolResultContent({ ...value, permissionMode }); } +function firstNonEmptyString(...values: Array): string | undefined { + return values.find((value) => value !== undefined && value.trim().length > 0); +} + function isNonShellToolResultContent(value: unknown): value is ToolResultContent { if (!isRecord(value) || typeof value.kind !== 'string') return false; switch (value.kind) { @@ -313,8 +279,6 @@ function isNonShellToolResultContent(value: unknown): value is ToolResultContent typeof value.message === 'string' && isOptionalString(value.credentialSource) ); - case 'explore_agent': - return isExploreResult(value); case 'subagent': return ( hasValidSubagentResultFields(value) && @@ -387,97 +351,6 @@ function isAgentSwarmItem(value: unknown): value is AgentSwarmItem { ); } -function isExploreResult(value: Record): value is ExploreResult { - return ( - hasExactShape(value, EXPLORE_SHAPE) && - typeof value.ok === 'boolean' && - (value.partial === undefined || typeof value.partial === 'boolean') && - (value.terminalStatus === undefined || - ['completed', 'completed_empty', 'failed', 'canceled', 'canceled_partial'].includes( - value.terminalStatus as string, - )) && - value.mode === 'read_only' && - typeof value.objective === 'string' && - isStringArray(value.roots) && - isStringArray(value.queries) && - (value.ignoredPaths === undefined || isStringArray(value.ignoredPaths)) && - isOptionalString(value.stoppingCondition) && - (value.limitReasons === undefined || - (Array.isArray(value.limitReasons) && - value.limitReasons.every((reason) => - ['candidate_budget', 'file_budget', 'match_budget', 'byte_budget'].includes(reason), - ))) && - isOptionalFiniteNumber(value.filesDiscovered) && - isFiniteNumber(value.filesInspected) && - isFiniteNumber(value.filesSkipped) && - isOptionalFiniteNumber(value.sensitiveFilesSkipped) && - isFiniteNumber(value.bytesRead) && - isOptionalFiniteNumber(value.startedAt) && - isOptionalFiniteNumber(value.completedAt) && - isOptionalFiniteNumber(value.durationMs) && - isStringArray(value.progress) && - (value.recentEvents === undefined || - (Array.isArray(value.recentEvents) && value.recentEvents.every(isExploreRecentEvent))) && - (value.evidence === undefined || - (Array.isArray(value.evidence) && value.evidence.every(isExploreEvidence))) && - isOptionalString(value.summary) && - isOptionalString(value.report) && - Array.isArray(value.candidateFiles) && - value.candidateFiles.every(isExploreCandidate) && - Array.isArray(value.matches) && - value.matches.every(isExploreMatch) && - isStringArray(value.notes) && - (value.reason === undefined || - ['invalid_objective', 'invalid_root', 'no_readable_roots', 'aborted'].includes( - value.reason as string, - )) && - isOptionalString(value.message) - ); -} - -function isExploreRecentEvent(value: unknown): value is ExploreRecentEvent { - return ( - isRecord(value) && - hasExactShape(value, EXPLORE_RECENT_EVENT_SHAPE) && - typeof value.type === 'string' && - isFiniteNumber(value.at) && - typeof value.message === 'string' - ); -} - -function isExploreEvidence(value: unknown): value is ExploreEvidence { - return ( - isRecord(value) && - hasExactShape(value, EXPLORE_EVIDENCE_SHAPE) && - (value.type === 'match' || value.type === 'candidate') && - typeof value.path === 'string' && - isOptionalFiniteNumber(value.line) && - typeof value.label === 'string' && - isOptionalFiniteNumber(value.score) - ); -} - -function isExploreCandidate(value: unknown): value is ExploreCandidate { - return ( - isRecord(value) && - hasExactShape(value, EXPLORE_CANDIDATE_SHAPE) && - typeof value.path === 'string' && - isFiniteNumber(value.score) && - isStringArray(value.reasons) - ); -} - -function isExploreMatch(value: unknown): value is ExploreMatch { - return ( - isRecord(value) && - hasExactShape(value, EXPLORE_MATCH_SHAPE) && - typeof value.path === 'string' && - isFiniteNumber(value.line) && - typeof value.query === 'string' && - typeof value.snippet === 'string' - ); -} - function isWebSearchRow(value: unknown): value is WebSearchRow { return ( isRecord(value) && diff --git a/packages/core/src/tool-result-status.ts b/packages/core/src/tool-result-status.ts index 1f313bd729..5c06c9404d 100644 --- a/packages/core/src/tool-result-status.ts +++ b/packages/core/src/tool-result-status.ts @@ -73,8 +73,8 @@ export function isCancelledToolResultContent(content: ToolResultContent | undefi * * `isError` is the call-level contract: a successful observation of a * cancelled background task (`StopBackgroundTask` → shell_run cancelled, - * isError:false) is `completed`, not interrupted. Only failed cancels and - * aborted explore agents map to `interrupted`. + * isError:false) is `completed`, not interrupted. Failed cancels map to + * `interrupted`. */ export function toolResultActivityStatus( isError: boolean, @@ -83,8 +83,5 @@ export function toolResultActivityStatus( if (!isError) return 'completed'; // Failed cancel (user stop / kill) — not a tool failure banner. if (isCancelledToolResultContent(content)) return 'interrupted'; - if (content?.kind === 'explore_agent' && content.reason === 'aborted') { - return 'interrupted'; - } return 'errored'; } diff --git a/packages/runtime-host/protocol-compatible-changes/session-catalog-deep-research-import.json b/packages/runtime-host/protocol-compatible-changes/session-catalog-deep-research-import.json new file mode 100644 index 0000000000..1d86c8ae48 --- /dev/null +++ b/packages/runtime-host/protocol-compatible-changes/session-catalog-deep-research-import.json @@ -0,0 +1,8 @@ +{ + "epoch": 65, + "files": [ + "packages/runtime-host/src/protocol/session-catalog.ts", + "packages/runtime-host/src/protocol/session-continuity.ts" + ], + "reason": "Renames an internal Deep Research import and removes a retired tool from a comment without changing any wire codec or message shape" +} diff --git a/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts b/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts new file mode 100644 index 0000000000..6c1338603b --- /dev/null +++ b/packages/runtime-host/src/__tests__/interactive-run-composer.test.ts @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { createDefaultRuntimePolicy } from '@maka/core/runtime-policy'; +import type { TaskLedgerStore } from '@maka/core/task-ledger'; +import type { MakaTool } from '@maka/runtime/tool-runtime'; +import { createInteractiveRunComposer } from '../server/interactive-run-composer.js'; +import type { HostMemoryCoordinator } from '../server/memory-coordinator.js'; +import type { HostSkillCatalogCoordinator } from '../server/skill-catalog-coordinator.js'; + +test('the interactive tool surface does not expose the retired ExploreAgent tool', () => { + const composer = createFixtureComposer(); + + assert.equal( + composer.tools.some(({ name }) => name === 'ExploreAgent'), + false, + ); +}); + +test('Deep Research keeps standard inspection tools and its durable workspace tools', () => { + const tool = (name: string): MakaTool => ({ + name, + description: name, + parameters: {}, + impl: async () => name, + }); + const composer = createFixtureComposer({ + hostTools: [tool('WebSearch')], + deepResearch: { tools: [tool('deep_research_status')] }, + }); + const names = new Set(composer.tools.map(({ name }) => name)); + + for (const name of ['Read', 'Glob', 'Grep', 'WebSearch', 'deep_research_status']) { + assert.equal(names.has(name), true, `expected Deep Research tool ${name}`); + } + for (const name of ['Write', 'Edit', 'Bash', 'ExploreAgent']) { + assert.equal(names.has(name), false, `unexpected Deep Research tool ${name}`); + } +}); + +function createFixtureComposer( + overrides: Partial[0]> = {}, +) { + return createInteractiveRunComposer({ + runtimePolicy: { revision: 0, policy: createDefaultRuntimePolicy() }, + skills: { + readCanonicalModelInventory: async () => ({ inventory: [] }), + } as unknown as HostSkillCatalogCoordinator, + memory: {} as HostMemoryCoordinator, + taskLedger: {} as TaskLedgerStore, + builtinTools: {}, + ...overrides, + }); +} diff --git a/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts index e5f788bac5..c02dde9c3c 100644 --- a/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/session-catalog-coordinator.test.ts @@ -24,7 +24,7 @@ import { join } from 'node:path'; import test from 'node:test'; import { createDefaultRuntimePolicy } from '@maka/core/runtime-policy'; import { createGenesisExecutionBoundary } from '@maka/core/sandbox-boundary'; -import { DEEP_RESEARCH_SESSION_LABEL, DEEP_RESEARCH_SESSION_NAME } from '@maka/core/explore-agent'; +import { DEEP_RESEARCH_SESSION_LABEL, DEEP_RESEARCH_SESSION_NAME } from '@maka/core/deep-research'; import { type RelayModelProfile } from '@maka/core/model-thinking'; import { WORKHUB_COORDINATION_SESSION_ID, diff --git a/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts index 4d32b5c5bc..bb8a64d3e6 100644 --- a/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts +++ b/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts @@ -26,7 +26,7 @@ import { connect, type Socket } from 'node:net'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { test } from 'node:test'; -import { DEEP_RESEARCH_SESSION_LABEL, DEEP_RESEARCH_SESSION_NAME } from '@maka/core/explore-agent'; +import { DEEP_RESEARCH_SESSION_LABEL, DEEP_RESEARCH_SESSION_NAME } from '@maka/core/deep-research'; import { openInteractiveArtifactStoreForWrite } from '@maka/storage/artifact-stores'; import { openInteractiveExecutionStoresForWrite } from '@maka/storage/execution-stores'; import { openInteractiveRuntimePolicyStoresForWrite } from '@maka/storage/runtime-policy-stores'; diff --git a/packages/runtime-host/src/protocol/session-catalog.ts b/packages/runtime-host/src/protocol/session-catalog.ts index 3ea74f4fe2..62c5309a48 100644 --- a/packages/runtime-host/src/protocol/session-catalog.ts +++ b/packages/runtime-host/src/protocol/session-catalog.ts @@ -20,7 +20,7 @@ import { isCollaborationMode, type CollaborationMode } from '@maka/core/collaboration'; import { isOrchestrationMode, type OrchestrationMode } from '@maka/core/orchestration'; import { isPermissionMode, type PermissionMode } from '@maka/core/permission'; -import { isSessionStartMode, type SessionStartMode } from '@maka/core/explore-agent'; +import { isSessionStartMode, type SessionStartMode } from '@maka/core/deep-research'; import { isSessionBlockedReason, isSessionToolProfile, diff --git a/packages/runtime-host/src/protocol/session-continuity.ts b/packages/runtime-host/src/protocol/session-continuity.ts index 5ed3397d5f..116f875cad 100644 --- a/packages/runtime-host/src/protocol/session-continuity.ts +++ b/packages/runtime-host/src/protocol/session-continuity.ts @@ -176,8 +176,8 @@ export type SessionToolEvent = activityKind?: ToolActivityKind; displayName?: string; /** - * Model/runtime-authored call intent (e.g. ExploreAgent's objective). - * Pass-through from the durable event; bounded on the wire. + * Model/runtime-authored call intent. Pass-through from the durable + * event; bounded on the wire. */ intent?: string; /** diff --git a/packages/runtime-host/src/server/deep-research-coordinator.ts b/packages/runtime-host/src/server/deep-research-coordinator.ts index bc13f11489..c27025ea88 100644 --- a/packages/runtime-host/src/server/deep-research-coordinator.ts +++ b/packages/runtime-host/src/server/deep-research-coordinator.ts @@ -19,7 +19,7 @@ import { projectDeepResearchEvents, type DeepResearchRun } from '@maka/core/deep-research-run'; import { projectDeepResearchClientProgress } from '@maka/core/deep-research-client-progress'; -import { isDeepResearchSession } from '@maka/core/explore-agent'; +import { isDeepResearchSession } from '@maka/core/deep-research'; import { buildDeepResearchTools } from '@maka/runtime/deep-research-tools'; import { type MakaTool } from '@maka/runtime/tool-runtime'; import { diff --git a/packages/runtime-host/src/server/interactive-run-composer.ts b/packages/runtime-host/src/server/interactive-run-composer.ts index 047745ac58..fb74349ca1 100644 --- a/packages/runtime-host/src/server/interactive-run-composer.ts +++ b/packages/runtime-host/src/server/interactive-run-composer.ts @@ -25,7 +25,7 @@ import { type RunCompositionSourceRevision } from '@maka/core/run-composition'; import { buildDeepResearchSystemPromptFragment, isDeepResearchSession, -} from '@maka/core/explore-agent'; +} from '@maka/core/deep-research'; import { activePlanExecution, type PlanSessionState, type PlanStore } from '@maka/core/plan'; import type { PermissionMode } from '@maka/core/permission'; import type { RuntimeExecutionConnection } from '@maka/core/llm-connections'; @@ -44,7 +44,6 @@ import { buildSubmitPlanTool, buildUpdatePlanTool, } from '@maka/runtime/plan-tools'; -import { buildExploreAgentTool } from '@maka/runtime/explore-agent-tool'; import { buildParentAgentTools } from '@maka/runtime/subagent-tools'; import { buildPersonalizationPromptFragment } from '@maka/runtime/system-prompt/personalization-prompt'; import { buildRequestSandboxBoundaryTool } from '@maka/runtime/sandbox-boundary-tool'; @@ -247,11 +246,7 @@ export function createInteractiveRunComposer(input: InteractiveRunComposerInput) input.plan?.mode === 'plan' ? renderPlanModePrompt({ fullAccess: input.plan.permissionMode === 'bypass' }) : undefined, - input.deepResearch - ? buildDeepResearchSystemPromptFragment({ - exploreAgentAvailable: tools.some(({ name }) => name === 'ExploreAgent'), - }) - : undefined, + input.deepResearch ? buildDeepResearchSystemPromptFragment() : undefined, input.sideConversation ? buildSideConversationSystemPromptFragment() : undefined, ]); return Object.freeze({ @@ -510,7 +505,6 @@ function buildDefaultHostTools( const builtins = builtinOptions ? buildBuiltinTools(builtinOptions) : []; const question = buildAskUserQuestionTool(); const sandboxBoundary = buildRequestSandboxBoundaryTool(); - const exploreAgent = buildExploreAgentTool(); const taskTools = buildTaskLedgerTools({ store: taskLedger }); const activeExecution = plan ? activePlanExecution(plan.state) : undefined; const interruptedExecution = plan @@ -531,7 +525,6 @@ function buildDefaultHostTools( ...hostTools.map((tool) => tool.name), question.name, sandboxBoundary.name, - exploreAgent.name, 'Skill', 'SkillSearch', ...taskTools.map((tool) => tool.name), @@ -548,7 +541,6 @@ function buildDefaultHostTools( ...hostTools, question, sandboxBoundary, - exploreAgent, buildSkillAgentToolFromInventory(inventoryFor, skillHost, { shadowTracker }), buildSkillSearchAgentToolFromInventory(inventoryFor, skillHost, { shadowTracker }), ...taskTools, diff --git a/packages/runtime-host/src/server/session-catalog-coordinator.ts b/packages/runtime-host/src/server/session-catalog-coordinator.ts index e20e9f6be4..10dd15752b 100644 --- a/packages/runtime-host/src/server/session-catalog-coordinator.ts +++ b/packages/runtime-host/src/server/session-catalog-coordinator.ts @@ -32,7 +32,7 @@ import { DEFAULT_SESSION_NAME, normalizeUserSessionName } from '@maka/core/sessi import { isSessionStartModeLabel as isExecutionSemanticLabel, sessionStartModeSpec, -} from '@maka/core/explore-agent'; +} from '@maka/core/deep-research'; import { isWorkHubCoordinationSessionId, isWorkHubCoordinationSessionTarget, diff --git a/packages/runtime-host/src/server/session-revision-coordinator.ts b/packages/runtime-host/src/server/session-revision-coordinator.ts index 5768558101..328e280676 100644 --- a/packages/runtime-host/src/server/session-revision-coordinator.ts +++ b/packages/runtime-host/src/server/session-revision-coordinator.ts @@ -18,7 +18,7 @@ */ import { createHash, randomUUID } from 'node:crypto'; -import { isDeepResearchSession } from '@maka/core/explore-agent'; +import { isDeepResearchSession } from '@maka/core/deep-research'; import { SIDE_CONVERSATION_SESSION_LABEL } from '@maka/core/side-conversation'; import type { RuntimeEvent } from '@maka/core/runtime-event'; import type { CreateSessionInput } from '@maka/core/runtime-inputs'; diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 241aff59c0..62bb50d5d0 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -52,7 +52,6 @@ "./conversation-copy": "./dist/conversation-copy.js", "./cua-frame-state": "./dist/cua-frame-state.js", "./execution-inspect": "./dist/execution-inspect.js", - "./explore-agent-tool": "./dist/explore-agent-tool.js", "./goal-continuation": "./dist/goal-continuation.js", "./goal-evaluator": "./dist/goal-evaluator.js", "./goal-state": "./dist/goal-state.js", diff --git a/packages/runtime/src/__tests__/ai-sdk-backend.test.ts b/packages/runtime/src/__tests__/ai-sdk-backend.test.ts index 621487e3af..1c555069a8 100644 --- a/packages/runtime/src/__tests__/ai-sdk-backend.test.ts +++ b/packages/runtime/src/__tests__/ai-sdk-backend.test.ts @@ -11302,7 +11302,7 @@ describe('AiSdkBackend tool execution', () => { assert.equal(completion?.type === 'complete' ? completion.stopReason : undefined, 'end_turn'); }); - test('caps concurrent read-only subagent tools in one turn', async () => { + test('caps concurrent subagent tools in one turn', async () => { const messages: unknown[] = []; const events: SessionEvent[] = []; const backend = createTestAiSdkBackend({ @@ -11322,7 +11322,7 @@ describe('AiSdkBackend tool execution', () => { let implStarted = 0; const release: Array<() => void> = []; const tool: MakaTool = { - name: 'ExploreAgent', + name: 'agent_spawn', description: 'read-only worker', parameters: {}, categoryHint: 'subagent', @@ -11351,7 +11351,7 @@ describe('AiSdkBackend tool execution', () => { { toolCallId: 'tool-overflow', abortSignal: new AbortController().signal }, ); assert.deepEqual(rejected, { - error: '只读探索并发过多:同一轮最多 5 个子代理。请等待已有探索完成后再继续。', + error: '子代理并发过多:同一轮最多 5 个子代理。请等待已有任务完成后再继续。', }); assert.equal(implStarted, MAX_ACTIVE_SUBAGENT_TOOLS_PER_TURN); assert.equal( @@ -11367,96 +11367,6 @@ describe('AiSdkBackend tool execution', () => { await Promise.all(pending); }); - test('maps structured subagent terminal states to persisted tool status', async () => { - const messages: unknown[] = []; - const events: SessionEvent[] = []; - const telemetry: Array<{ status: string; toolCallId?: string }> = []; - const backend = createTestAiSdkBackend({ - sessionId: 'session-1', - header: header('explore'), - appendMessage: async (message) => { - messages.push(message); - }, - connection: connection(), - apiKey: 'sk-test', - modelId: 'claude-sonnet-4-5-20250929', - modelFactory: () => ({}), - tools: [], - newId: idGenerator(), - now: () => 1, - recordToolInvocation: (record) => { - telemetry.push({ status: record.status, toolCallId: record.toolCallId }); - }, - }); - const tool: MakaTool = { - name: 'ExploreAgent', - description: 'read-only worker', - parameters: {}, - categoryHint: 'subagent', - impl: async (args: unknown) => { - const input = args as { reason?: string }; - return { - kind: 'explore_agent', - ok: false, - mode: 'read_only', - objective: 'bad scope', - roots: [], - queries: [], - filesInspected: 0, - filesSkipped: 0, - bytesRead: 0, - progress: [], - candidateFiles: [], - matches: [], - notes: [], - reason: input.reason === 'aborted' ? 'aborted' : 'invalid_root', - message: input.reason === 'aborted' ? '只读探索已取消。' : '范围无效。', - }; - }, - }; - const execute = runtimeExecute(backend, tool, 'turn-1', { - push: (event) => events.push(event), - }); - - await execute( - { objective: 'bad scope' }, - { - toolCallId: 'tool-failed', - abortSignal: new AbortController().signal, - }, - ); - await execute( - { objective: 'cancelled', reason: 'aborted' }, - { - toolCallId: 'tool-aborted', - abortSignal: new AbortController().signal, - }, - ); - - assert.equal( - ( - messages.find( - (message) => - (message as { type?: string; toolUseId?: string }).type === 'tool_result' && - (message as { toolUseId?: string }).toolUseId === 'tool-failed', - ) as { isError?: boolean } | undefined - )?.isError, - true, - ); - assert.equal( - ( - events.find( - (event) => event.type === 'tool_result' && event.toolUseId === 'tool-aborted', - ) as { isError?: boolean } | undefined - )?.isError, - true, - ); - assert.deepEqual(telemetry, [ - { status: 'error', toolCallId: 'tool-failed' }, - { status: 'aborted', toolCallId: 'tool-aborted' }, - ]); - }); - test('maps foreground subagent terminal states to persisted tool status', async () => { const messages: unknown[] = []; const events: SessionEvent[] = []; diff --git a/packages/runtime/src/__tests__/deep-research-tools.test.ts b/packages/runtime/src/__tests__/deep-research-tools.test.ts index c5362e7007..fd553a6053 100644 --- a/packages/runtime/src/__tests__/deep-research-tools.test.ts +++ b/packages/runtime/src/__tests__/deep-research-tools.test.ts @@ -128,6 +128,7 @@ async function withTempRoot(fn: (root: string) => Promise): Promise describe('Deep Research runtime tools', () => { it('admits only the explicit Deep Research tool surface', () => { + const standardResearchNames = ['AskUserQuestion', 'Read', 'Glob', 'Grep', 'WebSearch']; const canonicalNames = [ DEEP_RESEARCH_START_TOOL_NAME, DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, @@ -138,7 +139,9 @@ describe('Deep Research runtime tools', () => { DEEP_RESEARCH_STATUS_TOOL_NAME, DEEP_RESEARCH_COMPLETE_TOOL_NAME, ]; + assert.ok(standardResearchNames.every((name) => isDeepResearchToolAllowed({ name }))); assert.ok(canonicalNames.every((name) => isDeepResearchToolAllowed({ name }))); + assert.equal(isDeepResearchToolAllowed({ name: 'ExploreAgent' }), false); assert.equal(isDeepResearchToolAllowed({ name: 'deep_research_unsafe_fixture' }), false); }); diff --git a/packages/runtime/src/__tests__/explore-agent-tool.test.ts b/packages/runtime/src/__tests__/explore-agent-tool.test.ts deleted file mode 100644 index 5f8fea0228..0000000000 --- a/packages/runtime/src/__tests__/explore-agent-tool.test.ts +++ /dev/null @@ -1,578 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises'; -import { join } from 'node:path'; -import { tmpdir } from 'node:os'; -import { buildExploreAgentTool, runReadOnlyExplore } from '../explore-agent-tool.js'; - -describe('ExploreAgent read-only worker', () => { - it('returns source-grounded matches without absolute paths', async () => { - await withWorkspace(async (workspaceRoot) => { - await mkdir(join(workspaceRoot, 'src'), { recursive: true }); - await writeFile( - join(workspaceRoot, 'src', 'permission.ts'), - ['export const policy = {', " explore: 'read-only subagent',", '};'].join('\n'), - ); - await writeFile(join(workspaceRoot, 'README.md'), '# Demo\npermission model overview'); - - const result = await runReadOnlyExplore({ - cwd: workspaceRoot, - objective: 'study permission policy', - roots: ['.'], - queries: ['permission', 'subagent'], - maxFiles: 10, - maxMatches: 10, - }); - - assert.equal(result.ok, true); - assert.equal(result.kind, 'explore_agent'); - assert.equal(result.mode, 'read_only'); - assert.equal(result.terminalStatus, 'completed'); - assert.deepEqual(result.roots, ['.']); - assert.deepEqual(result.ignoredPaths, []); - assert.equal(result.stoppingCondition, ''); - assert.deepEqual(result.limitReasons, []); - assert.equal(typeof result.startedAt, 'number'); - assert.equal(typeof result.completedAt, 'number'); - assert.equal(typeof result.durationMs, 'number'); - assert.ok(result.completedAt >= result.startedAt); - assert.ok(result.durationMs >= 0); - assert.ok(result.filesInspected >= 2); - assert.ok(result.filesDiscovered >= result.filesInspected); - assert.ok( - result.matches.some( - (match) => match.path === 'src/permission.ts' && match.query === 'subagent', - ), - ); - assert.ok(result.candidateFiles.some((file) => file.path === 'src/permission.ts')); - assert.equal(result.sensitiveFilesSkipped, 0); - assert.ok( - result.evidence.some( - (item) => item.type === 'match' && item.path === 'src/permission.ts' && item.line === 2, - ), - ); - assert.match( - result.summary, - /发现 \d+ 个候选 · 读取 \d+ 个文件 · 命中 \d+ 处 · 证据 \d+ 个 · 候选 \d+ 个 · 耗时 /, - ); - assert.ok( - result.recentEvents.some( - (event) => event.type === 'started' && /准备范围/.test(event.message), - ), - ); - assert.ok( - result.recentEvents.some( - (event) => event.type === 'completed' && /完成/.test(event.message), - ), - ); - assert.ok( - result.recentEvents.every( - (event) => typeof event.at === 'number' && !JSON.stringify(event).includes(workspaceRoot), - ), - ); - assert.match(result.report, /目标:study permission policy/); - assert.match(result.report, /状态:完成,已找到可交接证据。/); - assert.match(result.report, /发现\/读取:\d+ \/ \d+ 个文件/); - assert.match(result.report, /证据锚点:/); - assert.match(result.report, /src\/permission\.ts:2/); - assert.match(result.report, /命中片段:/); - assert.match(result.report, /耗时 \d+(?:\.\d)?(?: ms|s|m \d+s)/); - assert.equal(JSON.stringify(result).includes(workspaceRoot), false); - assert.ok(result.notes.some((note) => /不写文件、不联网、不启动进程/.test(note))); - assert.equal( - result.notes.some((note) => /Read-only worker|Search budget/.test(note)), - false, - ); - }); - }); - - it('rejects roots outside cwd and skips symlinked content', async () => { - await withWorkspace(async (workspaceRoot) => { - const outside = await mkdtemp(join(tmpdir(), 'maka-explore-outside-')); - try { - await writeFile(join(outside, 'secret.ts'), 'subscription_token = "secret"'); - await symlink(outside, join(workspaceRoot, 'linked-outside')); - - const invalid = await runReadOnlyExplore({ - cwd: workspaceRoot, - objective: 'inspect secret', - roots: ['../'], - queries: ['secret'], - }); - assert.equal(invalid.ok, false); - assert.equal(invalid.reason, 'invalid_root'); - - const result = await runReadOnlyExplore({ - cwd: workspaceRoot, - objective: 'inspect secret', - roots: ['.'], - queries: ['secret'], - }); - assert.equal(result.ok, true); - assert.equal(result.matches.length, 0); - assert.equal(JSON.stringify(result).includes('subscription_token'), false); - } finally { - await rm(outside, { recursive: true, force: true }); - } - }); - }); - - it('returns a structured failure when the session cwd is unreadable', async () => { - const missingRoot = join( - tmpdir(), - `maka-explore-missing-${Date.now()}-${Math.random().toString(16).slice(2)}`, - ); - const result = await runReadOnlyExplore({ - cwd: missingRoot, - objective: 'inspect missing workspace', - roots: ['.'], - queries: ['workspace'], - }); - - assert.equal(result.ok, false); - assert.equal(result.reason, 'invalid_root'); - assert.equal(result.terminalStatus, 'failed'); - assert.equal(result.filesDiscovered, 0); - assert.equal(result.message, '会话工作目录不可读取。'); - assert.equal(result.summary, '未完成:会话工作目录不可读取。'); - assert.ok( - result.recentEvents.some( - (event) => event.type === 'failed' && /工作目录不可读取/.test(event.message), - ), - ); - assert.equal(result.filesInspected, 0); - assert.equal(result.matches.length, 0); - assert.equal(JSON.stringify(result).includes(missingRoot), false); - assert.equal(typeof result.durationMs, 'number'); - }); - - it('skips sensitive local credential files even when they match the query', async () => { - await withWorkspace(async (workspaceRoot) => { - await mkdir(join(workspaceRoot, 'src'), { recursive: true }); - await writeFile(join(workspaceRoot, '.env'), 'ANTHROPIC_API_KEY=sk-ant-secret'); - await writeFile(join(workspaceRoot, '.npmrc'), '//registry.example/:_authToken=npm_secret'); - await writeFile( - join(workspaceRoot, 'credentials.json'), - '{"refresh_token":"secret-refresh"}', - ); - await writeFile( - join(workspaceRoot, 'src', 'config.ts'), - 'export const secretBoundary = "redacted in docs";', - ); - - const result = await runReadOnlyExplore({ - cwd: workspaceRoot, - objective: 'study secret boundary', - roots: ['.'], - queries: ['secret'], - maxFiles: 20, - maxMatches: 20, - }); - - assert.equal(result.ok, true); - assert.ok(result.matches.some((match) => match.path === 'src/config.ts')); - assert.equal(result.sensitiveFilesSkipped, 3); - assert.ok(result.notes.some((note) => /已跳过 3 个疑似本地凭据\/密钥文件/.test(note))); - assert.match(result.report, /跳过 \d+ 个(含敏感 3 个)/); - assert.equal(JSON.stringify(result).includes('sk-ant-secret'), false); - assert.equal(JSON.stringify(result).includes('npm_secret'), false); - assert.equal(JSON.stringify(result).includes('secret-refresh'), false); - assert.equal( - result.candidateFiles.some( - (file) => - file.path === '.env' || file.path === '.npmrc' || file.path === 'credentials.json', - ), - false, - ); - assert.equal( - result.evidence.some( - (item) => - item.path === '.env' || item.path === '.npmrc' || item.path === 'credentials.json', - ), - false, - ); - }); - }); - - it('honors explicit ignore paths during scoped research', async () => { - await withWorkspace(async (workspaceRoot) => { - await mkdir(join(workspaceRoot, 'src'), { recursive: true }); - await mkdir(join(workspaceRoot, 'vendor'), { recursive: true }); - await mkdir(join(workspaceRoot, 'generated', 'nested'), { recursive: true }); - await writeFile(join(workspaceRoot, 'src', 'alpha.ts'), 'export const alpha = "source";'); - await writeFile(join(workspaceRoot, 'vendor', 'alpha.ts'), 'export const alpha = "vendor";'); - await writeFile( - join(workspaceRoot, 'generated', 'nested', 'alpha.ts'), - 'export const alpha = "generated";', - ); - - const result = await runReadOnlyExplore({ - cwd: workspaceRoot, - objective: 'study alpha source implementation', - roots: ['.'], - queries: ['alpha'], - ignorePaths: ['vendor', './generated/', '../outside', '/tmp/outside', 'src/../escape'], - maxFiles: 20, - maxMatches: 20, - }); - - assert.equal(result.ok, true); - assert.deepEqual(result.ignoredPaths, ['vendor', 'generated']); - assert.ok(result.matches.some((match) => match.path === 'src/alpha.ts')); - assert.equal( - result.matches.some((match) => match.path.startsWith('vendor/')), - false, - ); - assert.equal( - result.matches.some((match) => match.path.startsWith('generated/')), - false, - ); - assert.equal( - result.candidateFiles.some( - (file) => file.path.startsWith('vendor/') || file.path.startsWith('generated/'), - ), - false, - ); - assert.ok(result.notes.some((note) => /已按请求忽略:vendor, generated/.test(note))); - assert.equal(JSON.stringify(result).includes(workspaceRoot), false); - }); - }); - - it('preserves the caller stopping condition in results and reports', async () => { - await withWorkspace(async (workspaceRoot) => { - await mkdir(join(workspaceRoot, 'src'), { recursive: true }); - await writeFile(join(workspaceRoot, 'src', 'alpha.ts'), 'export const alpha = "source";'); - - const result = await runReadOnlyExplore({ - cwd: workspaceRoot, - objective: 'study alpha source implementation', - roots: ['.'], - queries: ['alpha'], - stoppingCondition: 'stop after finding the implementation entry and one evidence line', - maxFiles: 10, - maxMatches: 10, - }); - - assert.equal(result.ok, true); - assert.equal( - result.stoppingCondition, - 'stop after finding the implementation entry and one evidence line', - ); - assert.match( - result.report, - /停止条件:stop after finding the implementation entry and one evidence line/, - ); - assert.ok( - result.notes.some((note) => - /停止条件:stop after finding the implementation entry and one evidence line/.test(note), - ), - ); - assert.equal(JSON.stringify(result).includes(workspaceRoot), false); - }); - }); - - it('surfaces budget boundaries as structured result metadata', async () => { - await withWorkspace(async (workspaceRoot) => { - for (let index = 0; index < 8; index++) { - await writeFile(join(workspaceRoot, `alpha-${index}.md`), `alpha evidence ${index}`); - } - - const result = await runReadOnlyExplore({ - cwd: workspaceRoot, - objective: 'find alpha evidence', - roots: ['.'], - queries: ['alpha'], - maxFiles: 2, - maxMatches: 2, - }); - - assert.equal(result.ok, true); - assert.deepEqual(result.limitReasons, ['file_budget', 'match_budget']); - assert.ok(result.notes.some((note) => /按查询命中和项目结构分读取前 2 个/.test(note))); - assert.ok(result.notes.some((note) => /命中预算已用尽;只返回前 2 处内容命中/.test(note))); - assert.match(result.report, /预算边界:读取文件预算已满、命中预算已满/); - assert.equal(JSON.stringify(result).includes(workspaceRoot), false); - }); - }); - - it('runs through the tool impl with the session cwd only', async () => { - await withWorkspace(async (workspaceRoot) => { - await writeFile(join(workspaceRoot, 'notes.md'), 'reference explore worker notes'); - const tool = buildExploreAgentTool(); - const output: string[] = []; - const result = await tool.impl( - { objective: 'find reference notes', queries: ['reference'] }, - { - sessionId: 's1', - turnId: 't1', - cwd: workspaceRoot, - toolCallId: 'tool-1', - abortSignal: new AbortController().signal, - emitOutput: (_stream, chunk) => output.push(chunk), - }, - ); - assert.equal(result.kind, 'explore_agent'); - assert.equal(result.ok, true); - assert.ok(result.matches.some((match) => match.path === 'notes.md')); - assert.ok(result.progress.some((message) => /准备范围/.test(message))); - assert.ok(result.progress.some((message) => /完成/.test(message))); - assert.equal(result.progress.join('').includes(workspaceRoot), false); - assert.ok(output.some((chunk) => /准备范围/.test(chunk))); - assert.ok(output.some((chunk) => /完成/.test(chunk))); - assert.equal(output.join('').includes(workspaceRoot), false); - }); - }); - - it('honors runtime abort signals before scanning files', async () => { - await withWorkspace(async (workspaceRoot) => { - await writeFile(join(workspaceRoot, 'notes.md'), 'reference explore worker notes'); - const abort = new AbortController(); - abort.abort(); - - const result = await runReadOnlyExplore({ - cwd: workspaceRoot, - objective: 'find reference notes', - queries: ['reference'], - abortSignal: abort.signal, - }); - - assert.equal(result.ok, false); - assert.equal(result.reason, 'aborted'); - assert.equal(result.terminalStatus, 'canceled'); - assert.equal(result.filesDiscovered, 0); - assert.equal(result.message, '只读探索已取消。'); - assert.ok( - result.recentEvents.some( - (event) => event.type === 'aborted' && /已取消/.test(event.message), - ), - ); - assert.equal(result.filesInspected, 0); - assert.equal(result.partial, false); - assert.deepEqual(result.matches, []); - assert.deepEqual(result.evidence, []); - assert.equal(result.report, ''); - assert.equal(JSON.stringify(result).includes('reference explore worker notes'), false); - }); - }); - - it('keeps bounded partial findings when canceled after reading files', async () => { - await withWorkspace(async (workspaceRoot) => { - for (let index = 0; index < 20; index++) { - await writeFile(join(workspaceRoot, `partial-${index}.md`), `alpha evidence ${index}`); - } - const abort = new AbortController(); - - const result = await runReadOnlyExplore({ - cwd: workspaceRoot, - objective: 'find partial alpha evidence', - roots: ['.'], - queries: ['alpha'], - maxFiles: 20, - maxMatches: 20, - abortSignal: abort.signal, - onProgress: (message) => { - if (/已读取 10 个文件/.test(message)) abort.abort(); - }, - }); - - assert.equal(result.ok, false); - assert.equal(result.reason, 'aborted'); - assert.equal(result.partial, true); - assert.equal(result.terminalStatus, 'canceled_partial'); - assert.equal(result.message, '只读探索已取消,已保留取消前的部分结果。'); - assert.equal(result.filesInspected, 10); - assert.ok(result.filesDiscovered >= result.filesInspected); - assert.ok(result.matches.length > 0); - assert.ok(result.evidence.length > 0); - assert.match(result.summary, /^已取消:发现 \d+ 个候选 · 读取 10 个文件/); - assert.match(result.report, /状态:已取消,以下为取消前部分结果。/); - assert.match(result.report, /命中片段:/); - assert.ok(result.notes.some((note) => /取消前已读取的部分结果/.test(note))); - assert.ok( - result.recentEvents.some( - (event) => event.type === 'aborted' && /部分结果/.test(event.message), - ), - ); - assert.equal(JSON.stringify(result).includes(workspaceRoot), false); - }); - }); - - it('forwards the runtime abort signal through the tool impl', async () => { - await withWorkspace(async (workspaceRoot) => { - await writeFile(join(workspaceRoot, 'notes.md'), 'reference explore worker notes'); - const tool = buildExploreAgentTool(); - const abort = new AbortController(); - abort.abort(); - - const result = await tool.impl( - { objective: 'find reference notes', queries: ['reference'] }, - { - sessionId: 's1', - turnId: 't1', - cwd: workspaceRoot, - toolCallId: 'tool-1', - abortSignal: abort.signal, - emitOutput: () => undefined, - }, - ); - - assert.equal(result.ok, false); - assert.equal(result.reason, 'aborted'); - assert.equal(result.message, '只读探索已取消。'); - assert.equal(result.filesInspected, 0); - assert.equal(result.partial, false); - }); - }); - - it('emits bounded progress checkpoints for long scans', async () => { - await withWorkspace(async (workspaceRoot) => { - for (let index = 0; index < 25; index++) { - await writeFile(join(workspaceRoot, `file-${index}.md`), `alpha reference ${index}`); - } - const progress: string[] = []; - const result = await runReadOnlyExplore({ - cwd: workspaceRoot, - objective: 'find alpha references', - queries: ['alpha'], - maxFiles: 25, - maxMatches: 25, - onProgress: (message) => progress.push(message), - }); - - assert.equal(result.ok, true); - assert.deepEqual(result.progress, progress); - assert.ok(result.recentEvents.length >= result.progress.length); - assert.ok(result.recentEvents.length <= 20); - assert.ok( - result.recentEvents.some( - (event) => event.type === 'checkpoint' && /已读取 10 个文件/.test(event.message), - ), - ); - assert.ok(progress.length >= 5); - assert.ok(progress.length <= 12); - assert.ok(progress.some((message) => /已读取 10 个文件/.test(message))); - assert.ok(progress.some((message) => /完成,读取/.test(message))); - assert.equal(progress.join('\n').includes(workspaceRoot), false); - }); - }); - - it('prioritizes project landmarks during broad research scans', async () => { - await withWorkspace(async (workspaceRoot) => { - await mkdir(join(workspaceRoot, 'src'), { recursive: true }); - await mkdir(join(workspaceRoot, 'tests'), { recursive: true }); - for (let index = 0; index < 20; index++) { - await writeFile(join(workspaceRoot, `aaa-filler-${index}.md`), `filler ${index}`); - } - await writeFile(join(workspaceRoot, 'package.json'), '{"scripts":{"test":"node --test"}}'); - await writeFile(join(workspaceRoot, 'README.md'), '# Landmark project'); - await writeFile(join(workspaceRoot, 'src', 'main.ts'), 'export function boot() {}'); - await writeFile( - join(workspaceRoot, 'tests', 'boot.test.ts'), - 'test("boot", () => undefined)', - ); - - const result = await runReadOnlyExplore({ - cwd: workspaceRoot, - objective: 'map this project architecture', - roots: ['.'], - queries: ['unlikely-query'], - maxFiles: 6, - maxMatches: 6, - }); - - assert.equal(result.ok, true); - assert.ok( - result.candidateFiles.some( - (file) => file.path === 'package.json' && file.reasons.includes('project manifest'), - ), - ); - assert.ok( - result.candidateFiles.some( - (file) => file.path === 'README.md' && file.reasons.includes('project documentation'), - ), - ); - assert.ok( - result.candidateFiles.some( - (file) => file.path === 'src/main.ts' && file.reasons.includes('project entrypoint'), - ), - ); - assert.ok( - result.candidateFiles.some( - (file) => - file.path === 'tests/boot.test.ts' && file.reasons.includes('project test surface'), - ), - ); - assert.ok( - result.evidence.some( - (item) => - item.type === 'candidate' && - item.path === 'package.json' && - item.label === '项目配置锚点', - ), - ); - assert.ok( - result.evidence.some( - (item) => - item.type === 'candidate' && item.path === 'README.md' && item.label === '项目文档锚点', - ), - ); - assert.ok(result.notes.some((note) => /优先读取项目配置、文档、入口和测试线索/.test(note))); - assert.ok(result.notes.some((note) => /按查询命中和项目结构分/.test(note))); - assert.equal(JSON.stringify(result).includes(workspaceRoot), false); - }); - }); - - it('keeps the generated research report bounded and source-grounded', async () => { - await withWorkspace(async (workspaceRoot) => { - for (let index = 0; index < 20; index++) { - await writeFile( - join(workspaceRoot, `report-${index}.md`), - `alpha line ${index}\nalpha detail ${index}`, - ); - } - - const result = await runReadOnlyExplore({ - cwd: workspaceRoot, - objective: 'summarize alpha report evidence', - roots: ['.'], - queries: ['alpha'], - maxFiles: 20, - maxMatches: 60, - }); - - assert.equal(result.ok, true); - assert.ok(result.report.length <= 6000); - assert.match(result.report, /目标:summarize alpha report evidence/); - assert.match(result.report, /下一步阅读:/); - assert.equal(result.report.includes(workspaceRoot), false); - }); - }); -}); - -async function withWorkspace(fn: (workspaceRoot: string) => Promise): Promise { - const workspaceRoot = await mkdtemp(join(tmpdir(), 'maka-explore-agent-')); - try { - await fn(workspaceRoot); - } finally { - await rm(workspaceRoot, { recursive: true, force: true }); - } -} diff --git a/packages/runtime/src/__tests__/plan-mode.test.ts b/packages/runtime/src/__tests__/plan-mode.test.ts index ee17fd271c..deebb84f31 100644 --- a/packages/runtime/src/__tests__/plan-mode.test.ts +++ b/packages/runtime/src/__tests__/plan-mode.test.ts @@ -119,7 +119,7 @@ describe('Plan Mode tool surface', () => { tool('Read', 'read'), tool('WebSearch', 'web_read'), tool('Write', 'file_write'), - tool('ExploreAgent', 'subagent'), + tool('agent_spawn', 'subagent'), tool('AskUserQuestion'), tool('SubmitPlan'), tool('update_plan'), @@ -144,7 +144,7 @@ describe('Plan Mode tool surface', () => { tool('CustomTool'), tool('ScheduledTask'), tool('GoalSet'), - tool('ExploreAgent', 'subagent'), + tool('agent_spawn', 'subagent'), tool('AskUserQuestion'), tool('SubmitPlan'), tool('update_plan'), @@ -167,7 +167,7 @@ describe('Plan Mode tool surface', () => { hasActiveExecution: true, tools: [ tool('Write', 'file_write'), - tool('ExploreAgent', 'subagent'), + tool('agent_spawn', 'subagent'), tool('SubmitPlan'), tool('update_plan'), tool('cancel_plan'), diff --git a/packages/runtime/src/__tests__/runtime-event-read-model.test.ts b/packages/runtime/src/__tests__/runtime-event-read-model.test.ts index b0dd3d815d..321631eba3 100644 --- a/packages/runtime/src/__tests__/runtime-event-read-model.test.ts +++ b/packages/runtime/src/__tests__/runtime-event-read-model.test.ts @@ -688,6 +688,59 @@ describe('projectRuntimeEventsToStoredMessages', () => { expect(out.diagnostics).toEqual([]); }); + test('folds retired ExploreAgent results before model replay', () => { + const result = { + kind: 'explore_agent', + ok: false, + terminalStatus: 'failed', + mode: 'read_only', + objective: 'Trace the session lifecycle.', + roots: ['packages/runtime'], + queries: ['SessionManager'], + filesInspected: 0, + filesSkipped: 0, + bytesRead: 0, + candidateFiles: [], + matches: [], + notes: [], + summary: '未完成:目标无效。', + report: '', + reason: 'invalid_objective', + message: '目标无效。', + } as const; + const events = [ + ev({ + id: 'evt-explore-call', + role: 'model', + author: 'agent', + content: { + kind: 'function_call', + id: 'explore-1', + name: 'ExploreAgent', + args: { objective: result.objective }, + }, + }), + ev({ + id: 'evt-explore-result', + role: 'tool', + author: 'tool', + content: { + kind: 'function_response', + id: 'explore-1', + name: 'ExploreAgent', + result: result as never, + }, + }), + ]; + + const replay = buildRuntimeEventModelReplayPlan(events); + + expect(replay.items.find((item) => item.kind === 'tool_result')).toMatchObject({ + output: { kind: 'text', text: '未完成:目标无效。' }, + }); + expect(replay.diagnostics).toEqual([]); + }); + test('restores a settled Agent Swarm function response', () => { const result = { kind: 'agent_swarm' as const, diff --git a/packages/runtime/src/__tests__/session-manager.test.ts b/packages/runtime/src/__tests__/session-manager.test.ts index 50f3a9df05..880a44922e 100644 --- a/packages/runtime/src/__tests__/session-manager.test.ts +++ b/packages/runtime/src/__tests__/session-manager.test.ts @@ -27,7 +27,7 @@ import { isSandboxBoundaryRestartClosure, } from '@maka/core/sandbox-boundary'; import { createWorkspaceWritePermissionProfile } from '@maka/core/permission-profile'; -import { DEEP_RESEARCH_SESSION_LABEL } from '@maka/core/explore-agent'; +import { DEEP_RESEARCH_SESSION_LABEL } from '@maka/core/deep-research'; import { SIDE_CONVERSATION_SESSION_LABEL } from '@maka/core/side-conversation'; import { RUNTIME_CONTINUATION_AUTHORITY_V1 } from '@maka/core/runtime-event-store'; import { deriveTurnRecords } from '@maka/core/session'; diff --git a/packages/runtime/src/__tests__/subagent-tools.test.ts b/packages/runtime/src/__tests__/subagent-tools.test.ts index 243ac1557b..7391d4009d 100644 --- a/packages/runtime/src/__tests__/subagent-tools.test.ts +++ b/packages/runtime/src/__tests__/subagent-tools.test.ts @@ -321,13 +321,6 @@ describe('subagent tools', () => { categoryHint: 'web_read', impl: async () => ({}), }, - { - name: 'ExploreAgent', - description: 'deterministic exploration', - parameters: {}, - categoryHint: 'subagent', - impl: async () => ({}), - }, ]); expect(tools.map((tool) => tool.name)).toEqual([ diff --git a/packages/runtime/src/deep-research-tools.ts b/packages/runtime/src/deep-research-tools.ts index cc35cd78f1..7d06374c27 100644 --- a/packages/runtime/src/deep-research-tools.ts +++ b/packages/runtime/src/deep-research-tools.ts @@ -63,7 +63,6 @@ const DEEP_RESEARCH_ALLOWED_TOOL_NAMES = new Set([ 'Read', 'Glob', 'Grep', - 'ExploreAgent', 'WebSearch', DEEP_RESEARCH_START_TOOL_NAME, DEEP_RESEARCH_SAVE_ARTIFACT_TOOL_NAME, diff --git a/packages/runtime/src/explore-agent-tool.ts b/packages/runtime/src/explore-agent-tool.ts deleted file mode 100644 index bf5717d489..0000000000 --- a/packages/runtime/src/explore-agent-tool.ts +++ /dev/null @@ -1,1353 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { lstat, readdir, readFile, realpath, stat } from 'node:fs/promises'; -import { basename, extname, join, resolve } from 'node:path'; -import { z } from 'zod'; -import { isPathInside, toRelative } from './path-containment.js'; -import type { MakaTool } from './tool-runtime.js'; - -export const EXPLORE_AGENT_TOOL_NAME = 'ExploreAgent'; - -const DEFAULT_MAX_FILES = 30; -const DEFAULT_MAX_MATCHES = 60; -const MAX_ROOTS = 5; -const MAX_QUERIES = 8; -const MAX_IGNORE_PATHS = 20; -const MAX_DISCOVERED_FILES = 250; -const MAX_FILE_BYTES = 512 * 1024; -const MAX_TOTAL_BYTES = 2 * 1024 * 1024; -const MATCH_CONTEXT_CHARS = 220; - -const PROJECT_MANIFEST_FILES = new Set([ - 'package.json', - 'pnpm-workspace.yaml', - 'turbo.json', - 'vite.config.ts', - 'vite.config.js', - 'tsconfig.json', - 'Cargo.toml', - 'go.mod', - 'pyproject.toml', - 'Package.swift', -]); - -const DOCUMENTATION_FILES = new Set([ - 'README.md', - 'AGENTS.md', - 'CLAUDE.md', - 'GEMINI.md', - 'CONTRIBUTING.md', - 'ARCHITECTURE.md', -]); - -const ENTRYPOINT_NAMES = new Set([ - 'main.ts', - 'main.tsx', - 'main.js', - 'main.jsx', - 'index.ts', - 'index.tsx', - 'index.js', - 'index.jsx', - 'server.ts', - 'server.js', - 'app.ts', - 'app.tsx', - 'app.js', - 'app.jsx', -]); - -const IGNORED_DIRS = new Set([ - '.git', - '.hg', - '.svn', - 'node_modules', - 'dist', - 'build', - 'coverage', - '.next', - '.turbo', - '.vite', - '.cache', - '.svelte-kit', - 'DerivedData', -]); - -const TEXT_EXTENSIONS = new Set([ - '', - '.c', - '.cc', - '.conf', - '.cpp', - '.css', - '.csv', - '.env', - '.go', - '.h', - '.hpp', - '.html', - '.java', - '.js', - '.json', - '.jsx', - '.lock', - '.log', - '.mjs', - '.md', - '.mdx', - '.py', - '.rb', - '.rs', - '.sh', - '.sql', - '.swift', - '.toml', - '.ts', - '.tsx', - '.txt', - '.vue', - '.xml', - '.yaml', - '.yml', -]); - -const SENSITIVE_TEXT_FILE_NAMES = new Set([ - '.env', - '.env.local', - '.env.development', - '.env.production', - '.env.test', - '.npmrc', - '.pypirc', - '.netrc', - 'credentials.json', - 'secrets.json', - 'id_rsa', - 'id_dsa', - 'id_ecdsa', - 'id_ed25519', -]); - -export interface ExploreAgentResult { - kind: 'explore_agent'; - ok: boolean; - partial: boolean; - terminalStatus: ExploreAgentTerminalStatus; - mode: 'read_only'; - objective: string; - roots: string[]; - queries: string[]; - ignoredPaths: string[]; - stoppingCondition: string; - limitReasons: ExploreAgentLimitReason[]; - filesDiscovered: number; - filesInspected: number; - filesSkipped: number; - sensitiveFilesSkipped: number; - bytesRead: number; - startedAt: number; - completedAt: number; - durationMs: number; - progress: string[]; - recentEvents: ExploreAgentEvent[]; - evidence: Array<{ - type: 'match' | 'candidate'; - path: string; - line?: number; - label: string; - score?: number; - }>; - summary: string; - report: string; - candidateFiles: Array<{ path: string; score: number; reasons: string[] }>; - matches: Array<{ path: string; line: number; query: string; snippet: string }>; - notes: string[]; - reason?: 'invalid_objective' | 'invalid_root' | 'no_readable_roots' | 'aborted'; - message?: string; -} - -type ExploreAgentTerminalStatus = - | 'completed' - | 'completed_empty' - | 'failed' - | 'canceled' - | 'canceled_partial'; -type ExploreAgentLimitReason = 'candidate_budget' | 'file_budget' | 'match_budget' | 'byte_budget'; - -export interface ExploreAgentEvent { - type: - | 'started' - | 'scope_resolved' - | 'scan' - | 'read' - | 'checkpoint' - | 'completed' - | 'failed' - | 'aborted' - | 'progress'; - at: number; - message: string; -} - -interface ProgressState { - messages: string[]; - recentEvents: ExploreAgentEvent[]; -} - -export function buildExploreAgentTool(): MakaTool< - { - objective: string; - roots?: string[]; - queries?: string[]; - ignorePaths?: string[]; - stoppingCondition?: string; - maxFiles?: number; - maxMatches?: number; - }, - ExploreAgentResult -> { - return { - name: EXPLORE_AGENT_TOOL_NAME, - displayName: '只读探索', - description: - 'Run a bounded read-only local exploration worker for a self-contained research question. ' + - 'It inspects filenames and text snippets under the session cwd only, returns candidate files and source-grounded matches, ' + - 'and never writes files, starts services, installs packages, or uses the network. Use it when a separate investigation saves main-thread work. ' + - 'Do not use it for one known file, a specific symbol, package scripts, test setup, config, or 1-3 obvious files; inspect those directly.', - parameters: z.object({ - objective: z - .string() - .min(4) - .max(600) - .describe('Specific research objective for the read-only worker.'), - roots: z - .array(z.string().min(1).max(240)) - .max(MAX_ROOTS) - .optional() - .describe('Optional relative roots under the session cwd. Defaults to the session cwd.'), - queries: z - .array(z.string().min(1).max(120)) - .max(MAX_QUERIES) - .optional() - .describe('Optional search terms. If omitted, terms are derived from the objective.'), - ignorePaths: z - .array(z.string().min(1).max(240)) - .max(MAX_IGNORE_PATHS) - .optional() - .describe( - 'Optional relative files or directories to skip, such as generated output, vendors, or build artifacts.', - ), - stoppingCondition: z - .string() - .min(1) - .max(240) - .optional() - .describe( - 'Optional plain-language condition that tells the worker when this investigation is sufficiently answered.', - ), - maxFiles: z.number().int().min(1).max(80).optional(), - maxMatches: z.number().int().min(1).max(120).optional(), - }), - categoryHint: 'subagent', - impl: async ( - { objective, roots, queries, ignorePaths, stoppingCondition, maxFiles, maxMatches }, - { cwd, abortSignal, emitOutput }, - ) => { - return runReadOnlyExplore({ - cwd, - objective, - roots, - queries, - ignorePaths, - stoppingCondition, - maxFiles, - maxMatches, - abortSignal, - onProgress: (message) => emitOutput('stdout', `${message}\n`), - }); - }, - }; -} - -export async function runReadOnlyExplore(input: { - cwd: string; - objective: string; - roots?: string[]; - queries?: string[]; - ignorePaths?: string[]; - stoppingCondition?: string; - maxFiles?: number; - maxMatches?: number; - abortSignal?: AbortSignal; - onProgress?: (message: string) => void; -}): Promise { - const startedAt = Date.now(); - const objective = normalizeText(input.objective).slice(0, 600); - if (objective.length < 4) { - return failure( - 'invalid_objective', - objective, - [], - [], - [], - '', - '只读探索需要一个明确的研究目标。', - [], - startedAt, - ); - } - - const roots = normalizeRoots(input.roots); - const queryTerms = normalizeQueries(input.queries, objective); - const ignoredPaths = normalizeIgnorePaths(input.ignorePaths); - const stoppingCondition = normalizeText(input.stoppingCondition).slice(0, 240); - let workspaceRoot: string; - try { - workspaceRoot = await realpath(input.cwd); - } catch { - return failure( - 'invalid_root', - objective, - roots, - queryTerms, - ignoredPaths, - stoppingCondition, - '会话工作目录不可读取。', - [], - startedAt, - ); - } - const maxFiles = clampInteger(input.maxFiles, 1, 80, DEFAULT_MAX_FILES); - const maxMatches = clampInteger(input.maxMatches, 1, 120, DEFAULT_MAX_MATCHES); - const discoveryBudget = Math.min(MAX_DISCOVERED_FILES, Math.max(maxFiles * 4, maxFiles)); - const progress = createProgressReporter(input.onProgress); - if (input.abortSignal?.aborted) { - return abortFailure( - objective, - roots, - queryTerms, - ignoredPaths, - stoppingCondition, - progress, - startedAt, - ); - } - progress.report( - 'started', - `只读探索:准备范围(${roots.length} 个 root,${queryTerms.length} 个查询词)`, - ); - - const resolvedRoots: Array<{ abs: string; rel: string }> = []; - for (const root of roots) { - if (input.abortSignal?.aborted) { - return abortFailure( - objective, - roots, - queryTerms, - ignoredPaths, - stoppingCondition, - progress, - startedAt, - ); - } - const resolved = resolve(workspaceRoot, root); - if (!isPathInside(workspaceRoot, resolved)) { - return failure( - 'invalid_root', - objective, - roots, - queryTerms, - ignoredPaths, - stoppingCondition, - `root 必须位于会话工作目录内:${root}`, - progress, - startedAt, - ); - } - try { - const actual = await realpath(resolved); - if (!isPathInside(workspaceRoot, actual)) { - return failure( - 'invalid_root', - objective, - roots, - queryTerms, - ignoredPaths, - stoppingCondition, - `root 不能穿过符号链接离开工作目录:${root}`, - progress, - startedAt, - ); - } - const rootStat = await stat(actual); - if (!rootStat.isDirectory() && !rootStat.isFile()) continue; - resolvedRoots.push({ abs: actual, rel: toRelative(workspaceRoot, actual) }); - } catch { - // Missing roots are reported through notes instead of failing the whole worker. - } - } - if (resolvedRoots.length === 0) { - return failure( - 'no_readable_roots', - objective, - roots, - queryTerms, - ignoredPaths, - stoppingCondition, - '没有可读取的研究范围。', - progress, - startedAt, - ); - } - progress.report( - 'scope_resolved', - `只读探索:确认 ${resolvedRoots.length} 个可读范围:${resolvedRoots.map((root) => root.rel).join(', ')}`, - ); - - const files: string[] = []; - const notes: string[] = [ - '只读探索边界:不写文件、不联网、不启动进程。', - `搜索预算:最多读取 ${maxFiles} 个文件、返回 ${maxMatches} 处命中、读取 ${Math.round(MAX_TOTAL_BYTES / 1024)} KiB 文本。`, - ]; - if (ignoredPaths.length > 0) { - notes.push(`已按请求忽略:${ignoredPaths.join(', ')}`); - } - if (stoppingCondition) { - notes.push(`停止条件:${stoppingCondition}`); - } - let filesSkipped = 0; - let sensitiveFilesSkipped = 0; - const limitReasons: ExploreAgentLimitReason[] = []; - for (const root of resolvedRoots) { - if (input.abortSignal?.aborted) { - return abortFailure( - objective, - roots, - queryTerms, - ignoredPaths, - stoppingCondition, - progress, - startedAt, - ); - } - const before = files.length; - const skippedBefore = filesSkipped; - const sensitiveBefore = sensitiveFilesSkipped; - const listed = await listTextFiles( - root.abs, - workspaceRoot, - discoveryBudget - files.length, - ignoredPaths, - input.abortSignal, - ); - if (listed.aborted) { - return abortFailure( - objective, - roots, - queryTerms, - ignoredPaths, - stoppingCondition, - progress, - startedAt, - ); - } - files.push(...listed.files); - filesSkipped += listed.skipped; - sensitiveFilesSkipped += listed.sensitiveSkipped; - if (listed.truncated) { - addLimitReason(limitReasons, 'candidate_budget'); - notes.push(`范围 ${root.rel} 已到达候选文件预算,后续文件未继续扫描。`); - } - if (files.length === before) notes.push(`范围 ${root.rel} 在预算内没有可读取文本文件。`); - const found = files.length - before; - const skipped = filesSkipped - skippedBefore; - const sensitive = sensitiveFilesSkipped - sensitiveBefore; - progress.report( - 'scan', - `只读探索:扫描 ${root.rel},找到 ${found} 个文本候选,跳过 ${skipped} 项${sensitive > 0 ? `(含 ${sensitive} 个敏感文件)` : ''}`, - ); - if (files.length >= discoveryBudget) break; - } - files.sort((left, right) => { - const leftRel = toRelative(workspaceRoot, left); - const rightRel = toRelative(workspaceRoot, right); - const leftScore = scorePath(leftRel, queryTerms).score; - const rightScore = scorePath(rightRel, queryTerms).score; - return rightScore - leftScore || leftRel.localeCompare(rightRel); - }); - if ( - files.some((file) => - scorePath(toRelative(workspaceRoot, file), queryTerms).reasons.some((reason) => - reason.startsWith('project '), - ), - ) - ) { - notes.push('广泛研究会优先读取项目配置、文档、入口和测试线索。'); - } - const filesToInspect = files.slice(0, maxFiles); - if (files.length > filesToInspect.length) { - addLimitReason(limitReasons, 'file_budget'); - notes.push( - `已发现 ${files.length} 个文本候选;按查询命中和项目结构分读取前 ${filesToInspect.length} 个。`, - ); - } - - const candidates = new Map }>(); - const matches: ExploreAgentResult['matches'] = []; - let bytesRead = 0; - let inspected = 0; - - progress.report('read', `只读探索:开始读取 ${filesToInspect.length} 个候选文件`); - for (const file of filesToInspect) { - if (input.abortSignal?.aborted) { - return partialAbortFailure({ - objective, - roots: resolvedRoots.map((root) => root.rel), - queryTerms, - ignoredPaths, - stoppingCondition, - limitReasons, - filesDiscovered: files.length, - filesInspected: inspected, - filesSkipped, - sensitiveFilesSkipped, - bytesRead, - candidates, - matches, - notes, - progress, - startedAt, - }); - } - const rel = toRelative(workspaceRoot, file); - const filenameScore = scorePath(rel, queryTerms); - if (filenameScore.score > 0) { - candidates.set(rel, { - path: rel, - score: filenameScore.score, - reasons: new Set(filenameScore.reasons), - }); - } - - let fileStat; - try { - fileStat = await stat(file); - } catch { - filesSkipped++; - continue; - } - if (fileStat.size > MAX_FILE_BYTES || bytesRead >= MAX_TOTAL_BYTES) { - filesSkipped++; - continue; - } - let text: string; - try { - text = await readFile(file, 'utf8'); - } catch { - filesSkipped++; - continue; - } - if (input.abortSignal?.aborted) { - return partialAbortFailure({ - objective, - roots: resolvedRoots.map((root) => root.rel), - queryTerms, - ignoredPaths, - stoppingCondition, - limitReasons, - filesDiscovered: files.length, - filesInspected: inspected, - filesSkipped, - sensitiveFilesSkipped, - bytesRead, - candidates, - matches, - notes, - progress, - startedAt, - }); - } - if (looksBinary(text)) { - filesSkipped++; - continue; - } - inspected++; - bytesRead += Buffer.byteLength(text, 'utf8'); - const fileMatches = findMatches(rel, text, queryTerms, maxMatches - matches.length); - if (fileMatches.length > 0) { - matches.push(...fileMatches); - const current = candidates.get(rel) ?? { path: rel, score: 0, reasons: new Set() }; - current.score += fileMatches.length * 3; - current.reasons.add('content match'); - candidates.set(rel, current); - } - if (matches.length >= maxMatches || bytesRead >= MAX_TOTAL_BYTES) break; - if (inspected > 0 && inspected % 10 === 0) { - progress.report( - 'checkpoint', - `只读探索:已读取 ${inspected} 个文件,命中 ${matches.length} 处`, - ); - } - } - - const candidateFiles = Array.from(candidates.values()) - .map((candidate) => ({ - path: candidate.path, - score: candidate.score, - reasons: Array.from(candidate.reasons).sort(), - })) - .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)) - .slice(0, 20); - - const evidence = buildEvidenceAnchors(matches, candidateFiles); - const terminalStatus: ExploreAgentTerminalStatus = - evidence.length > 0 ? 'completed' : 'completed_empty'; - if (matches.length === 0) notes.push('没有找到内容命中;候选文件可作为下一步阅读清单。'); - if (sensitiveFilesSkipped > 0) - notes.push(`已跳过 ${sensitiveFilesSkipped} 个疑似本地凭据/密钥文件,只报告数量不读取内容。`); - if (matches.length >= maxMatches) { - addLimitReason(limitReasons, 'match_budget'); - notes.push(`命中预算已用尽;只返回前 ${maxMatches} 处内容命中。`); - } - if (bytesRead >= MAX_TOTAL_BYTES) { - addLimitReason(limitReasons, 'byte_budget'); - notes.push('总读取预算已用尽,部分候选文件未继续读取。'); - } - progress.report( - 'completed', - `只读探索:完成,读取 ${inspected} 个文件,命中 ${matches.length} 处,候选 ${candidateFiles.length} 个`, - ); - const completedAt = Date.now(); - const durationMs = Math.max(0, completedAt - startedAt); - const summary = buildResultSummary({ - filesDiscovered: files.length, - filesInspected: inspected, - matches: matches.length, - evidence: evidence.length, - candidateFiles: candidateFiles.length, - durationMs, - }); - const report = buildResearchReport({ - statusLine: presentExploreAgentTerminalStatus(terminalStatus), - objective, - roots: resolvedRoots.map((root) => root.rel), - queryTerms, - stoppingCondition, - limitReasons, - filesDiscovered: files.length, - filesInspected: inspected, - filesSkipped, - sensitiveFilesSkipped, - bytesRead, - evidence, - candidateFiles, - matches, - notes, - durationMs, - }); - - return { - kind: 'explore_agent', - ok: true, - partial: false, - terminalStatus, - mode: 'read_only', - objective, - roots: resolvedRoots.map((root) => root.rel), - queries: queryTerms, - ignoredPaths, - stoppingCondition, - limitReasons, - filesDiscovered: files.length, - filesInspected: inspected, - filesSkipped, - sensitiveFilesSkipped, - bytesRead, - startedAt, - completedAt, - durationMs, - progress: progress.messages, - recentEvents: progress.recentEvents, - evidence, - summary, - report, - candidateFiles, - matches, - notes, - }; -} - -function createProgressReporter(onProgress: ((message: string) => void) | undefined): { - messages: string[]; - recentEvents: ExploreAgentEvent[]; - report(type: ExploreAgentEvent['type'], message: string): void; -} { - let emitted = 0; - const messages: string[] = []; - const recentEvents: ExploreAgentEvent[] = []; - return { - messages, - recentEvents, - report(type, message) { - appendExploreEvent(recentEvents, { type, at: Date.now(), message }); - if (emitted < 12) { - emitted++; - messages.push(message); - } - onProgress?.(message); - }, - }; -} - -function appendExploreEvent(events: ExploreAgentEvent[], event: ExploreAgentEvent): void { - events.push(event); - while (events.length > 20) { - const evictIndex = events.findIndex((item) => !isLifecycleExploreEvent(item.type)); - events.splice(evictIndex >= 0 ? evictIndex : 0, 1); - } -} - -function addLimitReason(reasons: ExploreAgentLimitReason[], reason: ExploreAgentLimitReason): void { - if (!reasons.includes(reason)) reasons.push(reason); -} - -function isLifecycleExploreEvent(type: ExploreAgentEvent['type']): boolean { - return type === 'started' || type === 'completed' || type === 'failed' || type === 'aborted'; -} - -async function listTextFiles( - root: string, - workspaceRoot: string, - budget: number, - ignoredPaths: string[], - abortSignal?: AbortSignal, -): Promise<{ - files: string[]; - skipped: number; - sensitiveSkipped: number; - aborted: boolean; - truncated: boolean; -}> { - const files: string[] = []; - let skipped = 0; - let sensitiveSkipped = 0; - let aborted = false; - let truncated = false; - - async function walk(abs: string): Promise { - if (abortSignal?.aborted) { - aborted = true; - return; - } - if (files.length >= budget) { - truncated = true; - return; - } - let entryStat; - try { - entryStat = await lstat(abs); - } catch { - skipped++; - return; - } - if (entryStat.isSymbolicLink()) { - skipped++; - return; - } - const rel = toRelative(workspaceRoot, abs); - if (rel !== '.' && isIgnoredPath(rel, ignoredPaths)) { - skipped++; - return; - } - if (entryStat.isFile()) { - if (isSensitiveTextFile(abs)) { - skipped++; - sensitiveSkipped++; - } else if (isLikelyTextFile(abs)) { - files.push(abs); - } else { - skipped++; - } - return; - } - if (!entryStat.isDirectory()) { - skipped++; - return; - } - if (abs !== root && shouldSkipDir(abs)) { - skipped++; - return; - } - let entries; - try { - entries = await readdir(abs, { withFileTypes: true }); - } catch { - skipped++; - return; - } - entries.sort((a, b) => a.name.localeCompare(b.name)); - for (const entry of entries) { - if (abortSignal?.aborted) { - aborted = true; - return; - } - if (files.length >= budget) { - truncated = true; - return; - } - const child = join(abs, entry.name); - if (!isPathInside(workspaceRoot, child)) { - skipped++; - continue; - } - await walk(child); - } - } - - await walk(root); - return { files, skipped, sensitiveSkipped, aborted, truncated }; -} - -function normalizeRoots(roots: string[] | undefined): string[] { - const normalized = (roots && roots.length > 0 ? roots : ['.']) - .map((root) => root.trim()) - .filter(Boolean) - .slice(0, MAX_ROOTS); - return normalized.length > 0 ? normalized : ['.']; -} - -function normalizeQueries(queries: string[] | undefined, objective: string): string[] { - const explicit = (queries ?? []).map(normalizeText).filter((query) => query.length > 0); - const source = explicit.length > 0 ? explicit : deriveQueries(objective); - return Array.from(new Set(source.map((query) => query.slice(0, 120)))).slice(0, MAX_QUERIES); -} - -function normalizeIgnorePaths(paths: string[] | undefined): string[] { - const normalized: string[] = []; - for (const raw of paths ?? []) { - const value = raw - .trim() - .replace(/\\/g, '/') - .replace(/^\.\/+/, '') - .replace(/\/+$/g, ''); - if (!value || value === '.' || value === '..' || value.startsWith('/') || value.includes('\0')) - continue; - const segments = value.split('/').filter(Boolean); - if (segments.length === 0 || segments.some((segment) => segment === '.' || segment === '..')) - continue; - normalized.push(segments.join('/').slice(0, 160)); - if (normalized.length >= MAX_IGNORE_PATHS) break; - } - return Array.from(new Set(normalized)); -} - -function isIgnoredPath(path: string, ignoredPaths: string[]): boolean { - return ignoredPaths.some((ignored) => path === ignored || path.startsWith(`${ignored}/`)); -} - -function deriveQueries(objective: string): string[] { - const words = objective - .split(/[^\p{L}\p{N}_-]+/u) - .map((word) => word.trim()) - .filter((word) => word.length >= 3 && !COMMON_WORDS.has(word.toLowerCase())); - return words.length > 0 ? words.slice(0, MAX_QUERIES) : [objective.slice(0, 80)]; -} - -const COMMON_WORDS = new Set([ - 'the', - 'and', - 'for', - 'with', - 'this', - 'that', - 'from', - 'project', - 'research', - 'please', -]); - -function normalizeText(value: unknown): string { - return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : ''; -} - -function clampInteger(value: unknown, min: number, max: number, fallback: number): number { - return typeof value === 'number' && Number.isInteger(value) - ? Math.max(min, Math.min(max, value)) - : fallback; -} - -function shouldSkipDir(abs: string): boolean { - return IGNORED_DIRS.has(basename(abs)); -} - -function isLikelyTextFile(abs: string): boolean { - return TEXT_EXTENSIONS.has(extname(abs).toLowerCase()); -} - -function isSensitiveTextFile(abs: string): boolean { - const base = basename(abs).toLowerCase(); - if (SENSITIVE_TEXT_FILE_NAMES.has(base)) return true; - if (base.startsWith('.env.')) return true; - if (/\.(pem|key|p12|pfx|crt|cer)$/i.test(base)) return true; - return false; -} - -function scorePath(path: string, queries: string[]): { score: number; reasons: string[] } { - const lowerPath = path.toLowerCase(); - const base = basename(path); - const lowerBase = base.toLowerCase(); - const reasons: string[] = []; - let score = 0; - if (PROJECT_MANIFEST_FILES.has(base)) { - score += 12; - reasons.push('project manifest'); - } - if (DOCUMENTATION_FILES.has(base)) { - score += 10; - reasons.push('project documentation'); - } - if (ENTRYPOINT_NAMES.has(lowerBase)) { - score += 8; - reasons.push('project entrypoint'); - } - if ( - /\b(__tests__|tests?|specs?|e2e)\b/i.test(path) || - /\.(test|spec)\.[cm]?[jt]sx?$/i.test(path) - ) { - score += 6; - reasons.push('project test surface'); - } - if (/\b(src|app|packages|apps)\b/i.test(path)) { - score += 2; - reasons.push('project source surface'); - } - for (const query of queries) { - const lowerQuery = query.toLowerCase(); - if (lowerPath.includes(lowerQuery)) { - score += 5; - reasons.push(`path contains "${query}"`); - } - } - return { score, reasons }; -} - -function findMatches( - path: string, - text: string, - queries: string[], - remaining: number, -): ExploreAgentResult['matches'] { - if (remaining <= 0) return []; - const matches: ExploreAgentResult['matches'] = []; - const lines = text.split(/\r?\n/); - const lowerQueries = queries.map((query) => ({ raw: query, lower: query.toLowerCase() })); - for (let index = 0; index < lines.length; index++) { - const lowerLine = lines[index]!.toLowerCase(); - const query = lowerQueries.find((item) => lowerLine.includes(item.lower)); - if (!query) continue; - matches.push({ - path, - line: index + 1, - query: query.raw, - snippet: capSnippet(lines[index]!), - }); - if (matches.length >= remaining) break; - } - return matches; -} - -function buildEvidenceAnchors( - matches: ExploreAgentResult['matches'], - candidateFiles: ExploreAgentResult['candidateFiles'], -): ExploreAgentResult['evidence'] { - const anchors: ExploreAgentResult['evidence'] = []; - const seen = new Set(); - - for (const match of matches) { - if (anchors.length >= 10) break; - const key = `${match.path}:${match.line}`; - if (seen.has(key)) continue; - seen.add(key); - anchors.push({ - type: 'match', - path: match.path, - line: match.line, - label: `内容命中:${match.query}`, - }); - } - - for (const candidate of candidateFiles) { - if (anchors.length >= 10) break; - if (seen.has(candidate.path)) continue; - seen.add(candidate.path); - anchors.push({ - type: 'candidate', - path: candidate.path, - label: evidenceLabelForCandidate(candidate.reasons), - score: candidate.score, - }); - } - - return anchors; -} - -function buildResearchReport(input: { - statusLine?: string; - objective: string; - roots: string[]; - queryTerms: string[]; - stoppingCondition: string; - limitReasons: ExploreAgentLimitReason[]; - filesDiscovered: number; - filesInspected: number; - filesSkipped: number; - sensitiveFilesSkipped: number; - bytesRead: number; - evidence: ExploreAgentResult['evidence']; - candidateFiles: ExploreAgentResult['candidateFiles']; - matches: ExploreAgentResult['matches']; - notes: string[]; - durationMs: number; -}): string { - const lines = [ - ...(input.statusLine ? [`状态:${input.statusLine}`] : []), - `目标:${input.objective}`, - `范围:${input.roots.length > 0 ? input.roots.join(', ') : '.'}`, - `查询:${input.queryTerms.length > 0 ? input.queryTerms.join(', ') : '未指定'}`, - ...(input.stoppingCondition ? [`停止条件:${input.stoppingCondition}`] : []), - ...(input.limitReasons.length > 0 - ? [`预算边界:${input.limitReasons.map(presentExploreAgentLimitReason).join('、')}`] - : []), - `发现/读取:${input.filesDiscovered} / ${input.filesInspected} 个文件,跳过 ${input.filesSkipped} 个${input.sensitiveFilesSkipped > 0 ? `(含敏感 ${input.sensitiveFilesSkipped} 个)` : ''},${formatReportBytes(input.bytesRead)},耗时 ${formatReportDuration(input.durationMs)}`, - ]; - - if (input.evidence.length > 0) { - lines.push('', '证据锚点:'); - for (const item of input.evidence.slice(0, 8)) { - lines.push( - `- ${item.path}${typeof item.line === 'number' ? `:${item.line}` : ''} — ${item.label}`, - ); - } - } - - if (input.matches.length > 0) { - lines.push('', '命中片段:'); - for (const match of input.matches.slice(0, 5)) { - lines.push(`- ${match.path}:${match.line} [${match.query}] ${match.snippet}`); - } - } - - if (input.candidateFiles.length > 0) { - lines.push('', '下一步阅读:'); - for (const candidate of input.candidateFiles.slice(0, 5)) { - lines.push(`- ${candidate.path}(分数 ${candidate.score})`); - } - } - - if (input.notes.length > 0) { - lines.push('', '说明:'); - for (const note of input.notes.slice(0, 5)) { - lines.push(`- ${note}`); - } - } - - return capReport(lines.join('\n')); -} - -function presentExploreAgentTerminalStatus(status: ExploreAgentTerminalStatus): string { - switch (status) { - case 'completed': - return '完成,已找到可交接证据。'; - case 'completed_empty': - return '完成,但没有找到可交接证据。'; - case 'failed': - return '失败,未产生结果。'; - case 'canceled': - return '已取消,未产生结果。'; - case 'canceled_partial': - return '已取消,以下为取消前部分结果。'; - } -} - -function buildResultSummary(input: { - filesDiscovered: number; - filesInspected: number; - matches: number; - evidence: number; - candidateFiles: number; - durationMs: number; -}): string { - return [ - `发现 ${input.filesDiscovered} 个候选`, - `读取 ${input.filesInspected} 个文件`, - `命中 ${input.matches} 处`, - `证据 ${input.evidence} 个`, - `候选 ${input.candidateFiles} 个`, - `耗时 ${formatReportDuration(input.durationMs)}`, - ].join(' · '); -} - -function presentExploreAgentLimitReason(reason: ExploreAgentLimitReason): string { - switch (reason) { - case 'candidate_budget': - return '候选文件预算已满'; - case 'file_budget': - return '读取文件预算已满'; - case 'match_budget': - return '命中预算已满'; - case 'byte_budget': - return '读取字节预算已满'; - } -} - -function formatReportBytes(bytes: number): string { - if (bytes < 1024) return `${bytes} B`; - return `${Math.round(bytes / 1024)} KiB`; -} - -function formatReportDuration(ms: number): string { - if (ms < 1000) return `${ms} ms`; - if (ms < 60_000) return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`; - const minutes = Math.floor(ms / 60_000); - const seconds = Math.round((ms % 60_000) / 1000); - return `${minutes}m ${seconds}s`; -} - -function capReport(report: string): string { - return Array.from(report).slice(0, 6000).join(''); -} - -function evidenceLabelForCandidate(reasons: string[]): string { - if (reasons.includes('project manifest')) return '项目配置锚点'; - if (reasons.includes('project documentation')) return '项目文档锚点'; - if (reasons.includes('project entrypoint')) return '入口文件锚点'; - if (reasons.includes('project test surface')) return '测试线索锚点'; - if (reasons.includes('project source surface')) return '源码线索锚点'; - if (reasons.includes('content match')) return '内容命中锚点'; - return '候选阅读锚点'; -} - -function capSnippet(line: string): string { - const cleaned = line.replace(/\s+/g, ' ').trim(); - return Array.from(cleaned).slice(0, MATCH_CONTEXT_CHARS).join(''); -} - -function looksBinary(text: string): boolean { - return text.includes('\u0000'); -} - -function failure( - reason: NonNullable, - objective: string, - roots: string[], - queries: string[], - ignoredPaths: string[], - stoppingCondition: string, - message: string, - progress: string[] | ProgressState = [], - startedAt = Date.now(), -): ExploreAgentResult { - const completedAt = Date.now(); - const progressState = normalizeProgressState(progress, startedAt); - appendExploreEvent(progressState.recentEvents, { - type: reason === 'aborted' ? 'aborted' : 'failed', - at: completedAt, - message, - }); - return { - kind: 'explore_agent', - ok: false, - partial: false, - terminalStatus: reason === 'aborted' ? 'canceled' : 'failed', - mode: 'read_only', - objective, - roots, - queries, - ignoredPaths, - stoppingCondition, - limitReasons: [], - filesDiscovered: 0, - filesInspected: 0, - filesSkipped: 0, - sensitiveFilesSkipped: 0, - bytesRead: 0, - startedAt, - completedAt, - durationMs: Math.max(0, completedAt - startedAt), - progress: progressState.messages, - recentEvents: progressState.recentEvents, - evidence: [], - summary: `未完成:${message}`, - report: '', - candidateFiles: [], - matches: [], - notes: ['只读探索边界:不写文件、不联网、不启动进程。'], - reason, - message, - }; -} - -function partialAbortFailure(input: { - objective: string; - roots: string[]; - queryTerms: string[]; - ignoredPaths: string[]; - stoppingCondition: string; - limitReasons: ExploreAgentLimitReason[]; - filesDiscovered: number; - filesInspected: number; - filesSkipped: number; - sensitiveFilesSkipped: number; - bytesRead: number; - candidates: Map }>; - matches: ExploreAgentResult['matches']; - notes: string[]; - progress: ProgressState; - startedAt: number; -}): ExploreAgentResult { - if (input.filesInspected <= 0 && input.matches.length === 0 && input.candidates.size === 0) { - return abortFailure( - input.objective, - input.roots, - input.queryTerms, - input.ignoredPaths, - input.stoppingCondition, - input.progress, - input.startedAt, - ); - } - const completedAt = Date.now(); - appendExploreEvent(input.progress.recentEvents, { - type: 'aborted', - at: completedAt, - message: '只读探索已取消,已保留取消前的部分结果。', - }); - const candidateFiles = Array.from(input.candidates.values()) - .map((candidate) => ({ - path: candidate.path, - score: candidate.score, - reasons: Array.from(candidate.reasons).sort(), - })) - .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)) - .slice(0, 20); - const evidence = buildEvidenceAnchors(input.matches, candidateFiles); - const terminalStatus: ExploreAgentTerminalStatus = 'canceled_partial'; - const durationMs = Math.max(0, completedAt - input.startedAt); - const notes = [...input.notes, '只读探索已取消;以下为取消前已读取的部分结果,不代表完整结论。']; - const summary = `已取消:${buildResultSummary({ - filesDiscovered: input.filesDiscovered, - filesInspected: input.filesInspected, - matches: input.matches.length, - evidence: evidence.length, - candidateFiles: candidateFiles.length, - durationMs, - })}`; - const report = buildResearchReport({ - statusLine: presentExploreAgentTerminalStatus(terminalStatus), - objective: input.objective, - roots: input.roots, - queryTerms: input.queryTerms, - stoppingCondition: input.stoppingCondition, - filesDiscovered: input.filesDiscovered, - filesInspected: input.filesInspected, - filesSkipped: input.filesSkipped, - sensitiveFilesSkipped: input.sensitiveFilesSkipped, - bytesRead: input.bytesRead, - evidence, - candidateFiles, - matches: input.matches, - notes, - limitReasons: input.limitReasons, - durationMs, - }); - return { - kind: 'explore_agent', - ok: false, - partial: true, - terminalStatus, - mode: 'read_only', - objective: input.objective, - roots: input.roots, - queries: input.queryTerms, - ignoredPaths: input.ignoredPaths, - stoppingCondition: input.stoppingCondition, - limitReasons: [...input.limitReasons], - filesDiscovered: input.filesDiscovered, - filesInspected: input.filesInspected, - filesSkipped: input.filesSkipped, - sensitiveFilesSkipped: input.sensitiveFilesSkipped, - bytesRead: input.bytesRead, - startedAt: input.startedAt, - completedAt, - durationMs, - progress: [...input.progress.messages], - recentEvents: [...input.progress.recentEvents], - evidence, - summary, - report, - candidateFiles, - matches: [...input.matches], - notes, - reason: 'aborted', - message: '只读探索已取消,已保留取消前的部分结果。', - }; -} - -function abortFailure( - objective: string, - roots: string[], - queries: string[], - ignoredPaths: string[], - stoppingCondition: string, - progress: string[] | ProgressState = [], - startedAt = Date.now(), -): ExploreAgentResult { - return failure( - 'aborted', - objective, - roots, - queries, - ignoredPaths, - stoppingCondition, - '只读探索已取消。', - progress, - startedAt, - ); -} - -function normalizeProgressState( - progress: string[] | ProgressState, - startedAt: number, -): ProgressState { - if (!Array.isArray(progress)) { - return { - messages: [...progress.messages], - recentEvents: progress.recentEvents.map((event) => ({ ...event })), - }; - } - return { - messages: [...progress], - recentEvents: progress.map((message, index) => ({ - type: 'progress', - at: startedAt + index, - message, - })), - }; -} diff --git a/packages/runtime/src/model-history.ts b/packages/runtime/src/model-history.ts index 0ead934b5f..53332e4343 100644 --- a/packages/runtime/src/model-history.ts +++ b/packages/runtime/src/model-history.ts @@ -64,6 +64,9 @@ import { import { withToolResultArchiveResourceRef } from './tool-result-archive.js'; import { formatAttachmentResourceRef } from '@maka/core/attachments'; import { decodeCanonicalShellToolResultContent } from '@maka/core/shell-run-result'; +import { markPersisted } from '@maka/core/persisted-value'; +import { decodePersistedToolResultContent } from '@maka/core/tool-result-record-schema'; +import type { ToolResultContent } from '@maka/core/events'; import type { AttachmentRef, QuoteRef } from '@maka/core/events'; import type { ModelMessage, UserContent, UserModelMessage } from './model-protocol.js'; import { projectBashToolResultForModel } from './bash-model-output.js'; @@ -607,16 +610,28 @@ export function buildRuntimeEventModelReplayPlan( ); continue; } - const shellResult = decodeCanonicalShellToolResultContent(event.content.result); + const usesProviderOutput = + event.content.providerExecuted && event.content.providerOutput !== undefined; let invalidResultMessage: string | undefined; - let normalizedResult: unknown = - event.content.providerExecuted && event.content.providerOutput !== undefined - ? event.content.providerOutput - : withToolResultArchiveResourceRef(event.content.result); - if (shellResult.state === 'invalid') { - invalidResultMessage = 'function_response contains an invalid shell tool result'; - } else if (shellResult.state === 'valid') { - normalizedResult = shellResult.content; + let normalizedResult: unknown = usesProviderOutput + ? event.content.providerOutput + : withToolResultArchiveResourceRef(event.content.result); + if (!usesProviderOutput && isRetiredExploreAgentResult(normalizedResult)) { + try { + normalizedResult = decodePersistedToolResultContent( + markPersisted(normalizedResult), + ); + } catch { + invalidResultMessage = 'function_response contains an invalid retired tool result'; + } + } + if (!invalidResultMessage) { + const shellResult = decodeCanonicalShellToolResultContent(normalizedResult); + if (shellResult.state === 'invalid') { + invalidResultMessage = 'function_response contains an invalid shell tool result'; + } else if (shellResult.state === 'valid') { + normalizedResult = shellResult.content; + } } if (!invalidResultMessage && event.content.name === 'Bash') { normalizedResult = projectBashToolResultForModel(normalizedResult); @@ -734,6 +749,12 @@ export function buildRuntimeEventModelReplayPlan( }; } +function isRetiredExploreAgentResult(value: unknown): boolean { + return ( + typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'explore_agent' + ); +} + /** * Convert projected RuntimeEvent history into the current AI SDK text-only * message shape. Tool/function and thinking entries are intentionally skipped. diff --git a/packages/runtime/src/session-manager.ts b/packages/runtime/src/session-manager.ts index 62bdf8f6f2..b284c62328 100644 --- a/packages/runtime/src/session-manager.ts +++ b/packages/runtime/src/session-manager.ts @@ -90,7 +90,7 @@ import { type PlanStore, } from '@maka/core/plan'; import { DEFAULT_SESSION_NAME } from '@maka/core/session-name'; -import { DEEP_RESEARCH_SESSION_LABEL, isDeepResearchSession } from '@maka/core/explore-agent'; +import { DEEP_RESEARCH_SESSION_LABEL, isDeepResearchSession } from '@maka/core/deep-research'; import { SIDE_CONVERSATION_SESSION_LABEL } from '@maka/core/side-conversation'; import { SUBAGENT_SESSION_RUNTIME_SCHEMA_VERSION, diff --git a/packages/runtime/src/tool-runtime.ts b/packages/runtime/src/tool-runtime.ts index ff407b4408..79e1fade0f 100644 --- a/packages/runtime/src/tool-runtime.ts +++ b/packages/runtime/src/tool-runtime.ts @@ -288,7 +288,7 @@ type SandboxBoundaryFailureKind = 'invalid' | 'unresolved'; type SandboxBoundaryFailureDetails = Extract['sandboxFailure']; const SUBAGENT_TOOL_LIMIT_MESSAGE = - '只读探索并发过多:同一轮最多 5 个子代理。请等待已有探索完成后再继续。'; + '子代理并发过多:同一轮最多 5 个子代理。请等待已有任务完成后再继续。'; const CLIENT_CAPABILITY_BOUNDARY_MESSAGE = 'Client Capability tools require the Bypass execution boundary because their client-side effects cannot be sandboxed by the Host. Switch this Session to Bypass and retry.'; @@ -960,7 +960,6 @@ export class ToolRuntime { // records, one is what the model reads — and a divergence would go here. const modelFacingArgs = persistedArgs; const now = this.input.now(); - const toolIntent = describeToolIntent(tool, persistedArgs); const trace = this.input.getRunTrace?.() ?? null; const runId = this.input.runId; const invocationId = this.input.invocationId ?? runId; @@ -1030,7 +1029,6 @@ export class ToolRuntime { ? { providerOptions: structuredClone(ctx.providerOptions) } : {}), ...(tool.displayName ? { displayName: tool.displayName } : {}), - ...(toolIntent ? { intent: toolIntent } : {}), ...(stepId !== undefined ? { stepId } : {}), }; let pushedCallEvent: ToolStartEvent | undefined; @@ -1078,7 +1076,6 @@ export class ToolRuntime { ...activityIdentity, ...(tool.activityKind ? { activityKind: tool.activityKind } : {}), ...(tool.displayName ? { displayName: tool.displayName } : {}), - ...(toolIntent ? { intent: toolIntent } : {}), args: structuredClone(persistedArgs), ...(ctx.providerOptions !== undefined ? { providerOptions: structuredClone(ctx.providerOptions) } @@ -3039,9 +3036,6 @@ function deriveToolResultStatus( (raw as { error: string }).error.length > 0 ) return 'error'; - if (content.kind === 'explore_agent' && content.ok === false) { - return content.reason === 'aborted' ? 'aborted' : 'error'; - } if (content.kind === 'subagent') { if (content.status === 'completed') return 'success'; if (content.status === 'cancelled') return 'aborted'; @@ -3092,12 +3086,6 @@ function summarizeToolResultForTelemetry( if (content.kind === 'terminal' || content.kind === 'shell_run' || content.kind === 'subagent') { return { kind: content.kind, status: content.status }; } - if (content.kind === 'explore_agent') { - return { - kind: content.kind, - status: content.terminalStatus ?? (content.ok ? 'completed' : 'failed'), - }; - } if (content.kind === 'rive_workflow') { return { kind: content.kind, @@ -3155,17 +3143,6 @@ function summarizePersistedArgs(args: unknown): string { return text.length <= 512 ? text : `${text.slice(0, 511)}…`; } -function describeToolIntent(tool: MakaTool, args: unknown): string | undefined { - if (tool.categoryHint !== 'subagent' || tool.name !== 'ExploreAgent') return undefined; - if (!args || typeof args !== 'object') return undefined; - const objective = (args as { objective?: unknown }).objective; - if (typeof objective !== 'string') return undefined; - const normalized = redactSecrets(objective.replace(/\s+/g, ' ').trim()); - if (normalized.length === 0) return undefined; - const capped = normalized.length <= 180 ? normalized : `${normalized.slice(0, 179)}…`; - return `只读探索:${capped}`; -} - function byteLength(value: unknown): number { if (value === undefined) return 0; const text = typeof value === 'string' ? value : JSON.stringify(value ?? null); diff --git a/packages/ui/src/__tests__/tool-activity-presentation.test.ts b/packages/ui/src/__tests__/tool-activity-presentation.test.ts index 2127683cfb..97f234d4d8 100644 --- a/packages/ui/src/__tests__/tool-activity-presentation.test.ts +++ b/packages/ui/src/__tests__/tool-activity-presentation.test.ts @@ -492,12 +492,11 @@ describe('collapsed tool row target', () => { const markup = renderToStaticMarkup(createElement(ToolTrow, { items: [{ ...baseItem, - toolName: 'ExploreAgent', - intent: '只读探索:定位渲染入口', - args: { objective: '定位渲染入口' }, + intent: '检查渲染入口', + args: { command: 'rg renderEntry' }, }], })); - assert.match(markup, /只读探索/); + assert.match(markup, /检查渲染入口/); }); it('names a live call from the wire args preview before full args arrive', async () => { diff --git a/packages/ui/src/chat-view.tsx b/packages/ui/src/chat-view.tsx index 4ed428d35d..2eb8c288d4 100644 --- a/packages/ui/src/chat-view.tsx +++ b/packages/ui/src/chat-view.tsx @@ -40,7 +40,7 @@ import type { QuoteRef, ShellRunUpdate, } from '@maka/core/events'; -import { isDeepResearchSession } from '@maka/core/explore-agent'; +import { isDeepResearchSession } from '@maka/core/deep-research'; import { Button, ButtonGroup, ChatMessageList, EmptyState, HStack, Spinner, Text } from '@astryxdesign/core'; import { useChatLayoutContext } from '@astryxdesign/core/Chat'; import { useLayer } from '@astryxdesign/core/Layer'; diff --git a/packages/ui/src/clipboard-feedback.ts b/packages/ui/src/clipboard-feedback.ts index 8ade2dfda4..3e1d4acd0b 100644 --- a/packages/ui/src/clipboard-feedback.ts +++ b/packages/ui/src/clipboard-feedback.ts @@ -23,7 +23,7 @@ * PR-UI-LIB-EXTRACT-7 (WAWQAQ msg `510fef52`, round 8/10): pulled * out of `components.tsx`. The hook is consumed at three sites * inside `@maka/ui` (message metadata copy, ToolActivity, and the - * explore-agent preview); the `phase` type + * structured preview); the `phase` type * is also referenced by `TurnFooterActions` which keeps its own * inline copy-feedback state. None of these are part of the * public API. diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index e8f2b94e15..d7a4d79cba 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -30,7 +30,7 @@ import { DEEP_RESEARCH_SCOPE_OPTIONS, DEEP_RESEARCH_STARTER_PROMPTS, DEEP_RESEARCH_WORKFLOW_STEPS, -} from '@maka/core/explore-agent'; +} from '@maka/core/deep-research'; export type DayPeriod = 'morning' | 'noon' | 'afternoon' | 'evening'; type ResearchItem = Readonly<{ title: string; body: string }>; diff --git a/packages/ui/src/primitives/chat.tsx b/packages/ui/src/primitives/chat.tsx index 87e237772c..a3ce8b5eaa 100644 --- a/packages/ui/src/primitives/chat.tsx +++ b/packages/ui/src/primitives/chat.tsx @@ -102,7 +102,6 @@ export function Marker({ * Retires the bespoke `OverlayPreview` family shell CSS — the shared * height-bounded `.maka-overlay-preview` base + `.maka-overlay-close`, the * structured cards (`.maka-tool-diff*`, `.maka-tool-terminal*`, - * `.maka-explore-agent-*` / `.maka-subagent-preview`, * `.maka-web-search-*`), and the separate `.maka-load-tool-*` result card — * represented by package-owned semantic classes in `styles.css`. * @@ -113,7 +112,7 @@ export function Marker({ * The kind class follows the shared base and may refine it by normal CSS * source order. * 2. Leaf rules authored as descendant selectors on bare tags (e.g. - * `.maka-explore-agent-section li`, `.maka-web-search-preview > header strong`) + * `.maka-web-search-preview > header strong`) * remain descendants of the stable semantic container class. * * Unlike the other tables, `previewVariants` IS exported on the `@maka/ui` barrel @@ -179,39 +178,6 @@ const PREVIEW_PART_CLASSES = { "terminal-copy": "maka-tool-terminal-copy", - // ── explore agent / subagent (shared shell) ─────────────────────────── - // `.maka-explore-agent-preview, .maka-subagent-preview` (+ the fault - // border, keyed on explore's `[data-ok=false]` or subagent's failed / - // cancelled `[data-status]`). - agent: - "maka-agent-preview", - // `.maka-explore-agent-head` (+ its `strong` title and `small` caption, - // the latter shared with the nested summary-line small). - "agent-head": - "maka-agent-preview-head", - // `.maka-explore-agent-summary-line` (+ its `small` ellipsis, layered over - // the head's caption styling above). - "agent-summary-line": - "maka-agent-preview-summary-line", - // `.maka-explore-agent-actions` - "agent-actions": "maka-agent-preview-actions", - // `.maka-explore-agent-message` - "agent-message": - "maka-agent-preview-message", - // `.maka-explore-agent-meta` (+ its `div` cells, `dt` labels, `dd` values). - "agent-meta": - "maka-agent-preview-meta", - // `.maka-explore-agent-section` (+ its direct `> strong`, list `ul`/`li` - // rows, leading `li` reset, `code` / `small` / `p` / `span` leaves). - "agent-section": - "maka-agent-preview-section", - // `.maka-explore-agent-section-head` (+ its `> strong`). - "agent-section-head": - "maka-agent-preview-section-head", - // `.maka-explore-agent-copy` (UiButton) + the copied / shared copy-state tints. - "agent-copy": - "maka-agent-preview-copy", - // ── web search ──────────────────────────────────────────────────────── // `.maka-web-search-preview` (+ its bare `> header` / list leaves; the // container inherits the overlay base's mono font, never resetting it). diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css index f768fa45d2..00c99f3e46 100644 --- a/packages/ui/src/styles.css +++ b/packages/ui/src/styles.css @@ -972,56 +972,9 @@ font: var(--maka-text-supporting); display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); padding: var(--space-1-5) var(--space-2); border-top: 1px solid var(--border); background: oklch(from var(--warning) l c h / 0.06); color: var(--foreground-secondary); } .maka-tool-terminal-truncated-note > span { min-width: 0; } -.maka-tool-terminal-copy, -.maka-agent-preview-copy { flex: 0 0 auto; } -.maka-tool-terminal-copy[data-pending="true"], -.maka-agent-preview-copy[data-pending="true"] { cursor: progress; } -.maka-tool-terminal-copy[data-copy-error="true"], -.maka-agent-preview-copy[data-copy-error="true"] { border-color: oklch(from var(--destructive) l c h / 0.35); color: var(--destructive); } - -.maka-agent-preview { - font: var(--maka-text-supporting); - display: grid; gap: var(--space-2-5); padding: var(--space-2-5) var(--space-3); border: 1px solid var(--foreground-10); border-radius: var(--radius-surface); background: var(--foreground-3); white-space: normal; } -.maka-agent-preview[data-ok="false"], -.maka-agent-preview[data-status="failed"], -.maka-agent-preview[data-status="cancelled"] { border-color: oklch(from var(--destructive) l c h / 0.22); } -.maka-agent-preview-head { display: grid; gap: var(--space-0-5); padding-bottom: var(--space-1-5); border-bottom: 1px solid var(--foreground-10); } -.maka-agent-preview-head strong { - font: var(--maka-text-heading-4); - min-width: 0; overflow: hidden; color: var(--foreground); text-overflow: ellipsis; white-space: nowrap; } -.maka-agent-preview-head small, -.maka-agent-preview-section small, -.maka-agent-preview-meta dt { - font: var(--maka-text-supporting); - color: var(--muted-foreground); letter-spacing: var(--tracking-wider); text-transform: uppercase; } -.maka-agent-preview-summary-line, -.maka-agent-preview-section-head { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: var(--space-2); } -.maka-agent-preview-summary-line small { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.maka-agent-preview-actions { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-1-5); margin-top: var(--space-1); } -.maka-agent-preview-message { - font: var(--maka-text-supporting); - padding: var(--space-2) var(--space-2-5); border-radius: var(--radius-control); background: oklch(from var(--destructive) l c h / 0.07); color: var(--destructive); } -.maka-agent-preview-meta { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-2); margin: 0; } -.maka-agent-preview-meta > div { display: grid; min-width: 0; gap: var(--space-0-5); } -.maka-agent-preview-meta dd { - font: var(--maka-text-supporting); - min-width: 0; margin: 0; overflow: hidden; color: var(--foreground-secondary); text-overflow: ellipsis; white-space: nowrap; } -.maka-agent-preview-section { display: grid; gap: var(--space-1-5); } -.maka-agent-preview-section > strong, -.maka-agent-preview-section-head > strong { - font: var(--maka-text-heading-5); - min-width: 0; color: var(--foreground); } -.maka-agent-preview-section ul { display: grid; gap: var(--space-1-5); margin: 0; padding: 0; list-style: none; } -.maka-agent-preview-section li { display: grid; min-width: 0; gap: var(--space-0-5); padding-block: var(--space-1-5); border-top: 1px solid var(--foreground-5); } -.maka-agent-preview-section li:first-child { padding-top: 0; border-top: 0; } -.maka-agent-preview-section code { - font: var(--maka-text-supporting); - min-width: 0; overflow: hidden; background: transparent; color: var(--foreground); text-overflow: ellipsis; white-space: nowrap; } -.maka-agent-preview-section p, -.maka-agent-preview-section span { - font: var(--maka-text-supporting); - margin: 0; color: var(--foreground-secondary); white-space: pre-wrap; word-break: break-word; } -.maka-agent-preview-copy[data-copied="true"] { border-color: oklch(from var(--accent) l c h / 0.35); color: var(--link); } +.maka-tool-terminal-copy { flex: 0 0 auto; } +.maka-tool-terminal-copy[data-pending="true"] { cursor: progress; } +.maka-tool-terminal-copy[data-copy-error="true"] { border-color: oklch(from var(--destructive) l c h / 0.35); color: var(--destructive); } .maka-web-search-preview { display: grid; gap: var(--space-2); padding: var(--space-2-5) var(--space-3); border: 1px solid var(--foreground-10); border-radius: var(--radius-surface); background: var(--foreground-3); } .maka-web-search-preview > header { display: flex; flex-direction: column; gap: var(--space-0-5); padding-bottom: var(--space-1-5); border-bottom: 1px solid var(--foreground-10); } diff --git a/packages/ui/src/tool-activity/agent-preview.tsx b/packages/ui/src/tool-activity/agent-preview.tsx deleted file mode 100644 index fcad99f557..0000000000 --- a/packages/ui/src/tool-activity/agent-preview.tsx +++ /dev/null @@ -1,667 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { type ToolResultContent } from '@maka/core/events'; -import { type UiLocale } from '@maka/core/ui-locale'; -import { ICON_SIZE, Check, Copy } from '../icons.js'; -import { useClipboardCopyFeedback } from '../clipboard-feedback.js'; -import { previewVariants } from '../primitives/chat.js'; -import { redactSecrets } from '../redact.js'; -import { Button as UiButton } from '@astryxdesign/core'; -import { cn } from '../ui.js'; -import { formatBytes, formatDuration } from './preview-utils.js'; -import { useUiLocale } from '../locale-context.js'; -import { getToolActivityCopy } from './copy.js'; - -type ExploreAgentResult = Extract; - -export function ExploreAgentPreview(props: { - result: ExploreAgentResult; -}) { - const locale = useUiLocale(); - const copy = getToolActivityCopy(locale).agent; - const { result } = props; - const copyFeedback = useClipboardCopyFeedback(); - const { - candidateFiles, - matches, - progress, - evidence, - resultSummary, - terminalStatus, - status, - reportLines, - notes, - roots, - queries, - ignoredPaths, - stoppingCondition, - limitReasons, - filesDiscovered, - skippedSummary, - duration, - continuationReason, - continuationText, - copyPayloads, - } = buildExploreAgentPreviewModel(result, locale); - - function copyButtonState(key: string, idleLabel: string, copiedAria: string) { - const phase = copyFeedback.phaseFor(key); - return { - phase, - disabled: copyFeedback.isPending, - label: phase === 'pending' - ? copy.copyState.pending - : phase === 'copied' - ? copy.copyState.copied - : phase === 'failed' - ? copy.copyState.failed - : idleLabel, - ariaLabel: phase === 'pending' - ? copy.copyState.pendingAria(idleLabel) - : phase === 'copied' - ? copiedAria - : phase === 'failed' - ? copy.copyState.failedAria(idleLabel) - : idleLabel, - }; - } - - const summaryCopy = copyButtonState('summary', copy.copyButtons.summary.idle, copy.copyButtons.summary.copied); - const continuationCopy = copyButtonState('continuation', copy.copyButtons.continuation.idle, copy.copyButtons.continuation.copied); - const processCopy = copyButtonState('process', copy.copyButtons.process.idle, copy.copyButtons.process.copied); - const evidenceCopy = copyButtonState('evidence', copy.copyButtons.evidence.idle, copy.copyButtons.evidence.copied); - const reportCopy = copyButtonState('report', copy.copyButtons.report.idle, copy.copyButtons.report.copied); - const candidateCopy = copyButtonState('candidate', copy.copyButtons.candidate.idle, copy.copyButtons.candidate.copied); - const matchesCopy = copyButtonState('matches', copy.copyButtons.matches.idle, copy.copyButtons.matches.copied); - - return ( -
-
- {redactSecrets(result.objective || copy.objectiveFallback)} - - {status} · {copy.foundRead(filesDiscovered, result.filesInspected)} · {skippedSummary} · {formatBytes(result.bytesRead)} - {limitReasons ? ` · ${copy.budgetLimited}` : ''} - {continuationReason ? ` · ${copy.continuationSuggested(continuationReason)}` : ''} - {duration ? ` · ${copy.duration(duration)}` : ''} - - {resultSummary.length > 0 && ( -
- {redactSecrets(resultSummary)} - void copyFeedback.copy('summary', copyPayloads.summary)} - isDisabled={summaryCopy.disabled} - aria-label={summaryCopy.ariaLabel} - aria-busy={summaryCopy.phase === 'pending' ? 'true' : undefined} - data-pending={summaryCopy.phase === 'pending' ? 'true' : undefined} - data-copied={summaryCopy.phase === 'copied' ? 'true' : 'false'} - data-copy-error={summaryCopy.phase === 'failed' ? 'true' : undefined} - icon={summaryCopy.phase === 'copied' ?
- )} - {continuationText.length > 0 && ( -
- void copyFeedback.copy('continuation', copyPayloads.continuation)} - isDisabled={continuationCopy.disabled} - aria-label={continuationCopy.ariaLabel} - aria-busy={continuationCopy.phase === 'pending' ? 'true' : undefined} - data-pending={continuationCopy.phase === 'pending' ? 'true' : undefined} - data-copied={continuationCopy.phase === 'copied' ? 'true' : 'false'} - data-copy-error={continuationCopy.phase === 'failed' ? 'true' : undefined} - tooltip={copy.continuationTitle} - icon={continuationCopy.phase === 'copied' ?
- )} -
- {!result.ok && ( -
- {redactSecrets(result.message ?? copy.incompleteFallback)} -
- )} -
-
-
{copy.detail.terminal}
-
{terminalStatus}
-
-
-
{copy.detail.foundRead}
-
{filesDiscovered} / {copy.files(result.filesInspected)}
-
-
-
{copy.detail.scope}
-
{redactSecrets(roots)}
-
-
-
{copy.detail.queries}
-
{redactSecrets(queries)}
-
- {ignoredPaths && ( -
-
{copy.detail.ignored}
-
{redactSecrets(ignoredPaths)}
-
- )} - {stoppingCondition && ( -
-
{copy.detail.stopping}
-
{redactSecrets(stoppingCondition)}
-
- )} - {limitReasons && ( -
-
{copy.detail.boundary}
-
{redactSecrets(limitReasons)}
-
- )} - {continuationReason && ( -
-
{copy.detail.next}
-
{copy.continuationSuggested(redactSecrets(continuationReason))}
-
- )} -
- {progress.length > 0 && ( -
-
- {copy.section.process.title} - void copyFeedback.copy('process', copyPayloads.process)} - isDisabled={processCopy.disabled} - aria-label={processCopy.ariaLabel} - aria-busy={processCopy.phase === 'pending' ? 'true' : undefined} - data-pending={processCopy.phase === 'pending' ? 'true' : undefined} - data-copied={processCopy.phase === 'copied' ? 'true' : 'false'} - data-copy-error={processCopy.phase === 'failed' ? 'true' : undefined} - icon={processCopy.phase === 'copied' ?
-
    - {progress.map((item, index) => ( -
  • - {redactSecrets(item)} -
  • - ))} -
-
- )} - {evidence.length > 0 && ( -
-
- {copy.section.evidence.title} - void copyFeedback.copy('evidence', copyPayloads.evidence)} - isDisabled={evidenceCopy.disabled} - aria-label={evidenceCopy.ariaLabel} - aria-busy={evidenceCopy.phase === 'pending' ? 'true' : undefined} - data-pending={evidenceCopy.phase === 'pending' ? 'true' : undefined} - data-copied={evidenceCopy.phase === 'copied' ? 'true' : 'false'} - data-copy-error={evidenceCopy.phase === 'failed' ? 'true' : undefined} - icon={evidenceCopy.phase === 'copied' ?
-
    - {evidence.map((item, index) => ( -
  • - - {redactSecrets(item.path)} - {typeof item.line === 'number' ? `:${item.line}` : ''} - - - {redactSecrets(item.label)} - {typeof item.score === 'number' ? ` · ${copy.score(item.score)}` : ''} - -
  • - ))} -
-
- )} - {reportLines.length > 0 && ( -
-
- {copy.section.report.title} - void copyFeedback.copy('report', copyPayloads.report)} - isDisabled={reportCopy.disabled} - aria-label={reportCopy.ariaLabel} - aria-busy={reportCopy.phase === 'pending' ? 'true' : undefined} - data-pending={reportCopy.phase === 'pending' ? 'true' : undefined} - data-copied={reportCopy.phase === 'copied' ? 'true' : 'false'} - data-copy-error={reportCopy.phase === 'failed' ? 'true' : undefined} - icon={reportCopy.phase === 'copied' ?
-
    - {reportLines.map((line, index) => ( -
  • - {redactSecrets(line)} -
  • - ))} -
-
- )} - {candidateFiles.length > 0 && ( -
-
- {copy.section.candidates.title} - void copyFeedback.copy('candidate', copyPayloads.candidate)} - isDisabled={candidateCopy.disabled} - aria-label={candidateCopy.ariaLabel} - aria-busy={candidateCopy.phase === 'pending' ? 'true' : undefined} - data-pending={candidateCopy.phase === 'pending' ? 'true' : undefined} - data-copied={candidateCopy.phase === 'copied' ? 'true' : 'false'} - data-copy-error={candidateCopy.phase === 'failed' ? 'true' : undefined} - icon={candidateCopy.phase === 'copied' ?
-
    - {candidateFiles.map((file) => ( -
  • - {redactSecrets(file.path)} - - {copy.score(file.score)} - {file.reasons.length > 0 ? ` · ${presentExploreAgentCandidateReasons(file.reasons, locale)}` : ''} - -
  • - ))} -
-
- )} - {matches.length > 0 && ( -
-
- {copy.section.matches.title} - void copyFeedback.copy('matches', copyPayloads.matches)} - isDisabled={matchesCopy.disabled} - aria-label={matchesCopy.ariaLabel} - aria-busy={matchesCopy.phase === 'pending' ? 'true' : undefined} - data-pending={matchesCopy.phase === 'pending' ? 'true' : undefined} - data-copied={matchesCopy.phase === 'copied' ? 'true' : 'false'} - data-copy-error={matchesCopy.phase === 'failed' ? 'true' : undefined} - icon={matchesCopy.phase === 'copied' ?
-
    - {matches.map((match, index) => ( -
  • - {redactSecrets(match.path)}:{match.line} - {redactSecrets(match.query)} -

    {redactSecrets(match.snippet)}

    -
  • - ))} -
-
- )} - {notes.length > 0 && ( -
- {copy.section.notes.title} -
    - {notes.map((note, index) => ( -
  • - {redactSecrets(note)} -
  • - ))} -
-
- )} -
- ); -} - -/** - * @knipignore Consumed via dynamic `await import(uiModuleUrl)` from the built - * dist in tool-activity-result-preview-contract.test.ts, which knip cannot - * trace through the runtime module URL. - */ -export function buildExploreAgentCopyPayloads(result: ExploreAgentResult, locale: UiLocale = 'zh'): Record<'summary' | 'process' | 'evidence' | 'report' | 'candidate' | 'matches' | 'continuation', string> { - return buildExploreAgentPreviewModel(result, locale).copyPayloads; -} - -function buildExploreAgentPreviewModel(result: ExploreAgentResult, locale: UiLocale) { - const copy = getToolActivityCopy(locale).agent; - const candidateFiles = result.candidateFiles.slice(0, 8); - const matches = result.matches.slice(0, 8); - const processLines = Array.isArray(result.recentEvents) && result.recentEvents.length > 0 - ? result.recentEvents.slice(0, 20).map((event) => formatExploreAgentEvent(event, result.startedAt, locale)) - : (result.progress ?? []).slice(0, 12); - const progress = processLines.slice(0, 6); - const evidence = (result.evidence ?? []).slice(0, 6); - const resultSummary = typeof result.summary === 'string' ? result.summary.trim() : ''; - const reportText = typeof result.report === 'string' ? result.report.trim() : ''; - const terminalStatus = presentExploreAgentTerminalStatus(result.terminalStatus, result.ok, result.partial === true, result.reason, locale); - const status = result.ok - ? copy.complete - : result.reason === 'aborted' && result.partial === true - ? copy.cancelledPartial - : presentExploreAgentReason(result.reason, locale) ?? copy.incomplete; - const reportLines = reportText.split('\n').filter((line) => line.trim().length > 0).slice(0, 8); - const notes = result.notes.slice(0, 4); - const roots = result.roots.length > 0 ? result.roots.join(', ') : '.'; - const queries = result.queries.length > 0 ? result.queries.join(', ') : copy.notSpecified; - const ignoredPaths = Array.isArray(result.ignoredPaths) && result.ignoredPaths.length > 0 - ? result.ignoredPaths.join(', ') - : ''; - const stoppingCondition = typeof result.stoppingCondition === 'string' - ? result.stoppingCondition.trim() - : ''; - const limitReasons = Array.isArray(result.limitReasons) - ? result.limitReasons.map((reason) => presentExploreAgentLimitReason(reason, locale)).filter(Boolean).join(locale === 'en' ? ', ' : '、') - : ''; - const filesDiscovered = typeof result.filesDiscovered === 'number' && Number.isFinite(result.filesDiscovered) - ? Math.max(0, Math.floor(result.filesDiscovered)) - : result.filesInspected; - const skippedSummary = result.sensitiveFilesSkipped && result.sensitiveFilesSkipped > 0 - ? copy.skipped(result.filesSkipped, result.sensitiveFilesSkipped) - : copy.skipped(result.filesSkipped); - const duration = formatDuration(result.durationMs); - const summaryText = resultSummary.length > 0 - ? [ - `${copy.field.status}: ${status}`, - `${copy.field.terminal}: ${terminalStatus}`, - `${copy.field.objective}: ${result.objective || copy.objectiveFallback}`, - `${copy.field.summary}: ${resultSummary}`, - `${copy.field.scope}: ${roots}`, - `${copy.field.queries}: ${queries}`, - `${copy.field.foundRead}: ${filesDiscovered} / ${copy.files(result.filesInspected)}`, - duration ? `${copy.field.duration}: ${duration}` : '', - ignoredPaths ? `${copy.field.ignored}: ${ignoredPaths}` : '', - stoppingCondition ? `${copy.field.stopping}: ${stoppingCondition}` : '', - limitReasons ? `${copy.field.boundary}: ${limitReasons}` : '', - ].filter((line) => line.length > 0).join('\n') - : ''; - const processText = [ - summaryText, - processLines.length > 0 ? `${copy.field.events}: ${processLines.length}` : '', - processLines.join('\n'), - ].filter((line) => line.trim().length > 0).join('\n').trim(); - const evidenceText = evidence.length > 0 - ? [ - `${copy.field.status}: ${status}`, - `${copy.field.terminal}: ${terminalStatus}`, - `${copy.field.objective}: ${result.objective || copy.objectiveFallback}`, - `${copy.field.evidence}: ${evidence.length}`, - ...evidence.map((item) => [ - `- ${item.path}${typeof item.line === 'number' ? `:${item.line}` : ''}`, - item.label, - typeof item.score === 'number' ? copy.score(item.score) : '', - ].filter(Boolean).join(' — ')), - ].join('\n') - : ''; - const candidateText = candidateFiles.length > 0 - ? [ - `${copy.field.status}: ${status}`, - `${copy.field.terminal}: ${terminalStatus}`, - `${copy.field.objective}: ${result.objective || copy.objectiveFallback}`, - `${copy.field.foundRead}: ${filesDiscovered} / ${copy.files(result.filesInspected)}`, - `${copy.field.candidates}: ${candidateFiles.length}`, - ...candidateFiles.map((file) => [ - `- ${file.path}`, - copy.score(file.score), - file.reasons.length > 0 ? presentExploreAgentCandidateReasons(file.reasons, locale) : '', - ].filter(Boolean).join(' — ')), - ].join('\n') - : ''; - const matchesText = matches.length > 0 - ? [ - `${copy.field.status}: ${status}`, - `${copy.field.terminal}: ${terminalStatus}`, - `${copy.field.objective}: ${result.objective || copy.objectiveFallback}`, - `${copy.field.queries}: ${queries}`, - `${copy.field.matches}: ${matches.length}`, - ...matches.map((match) => `- ${match.path}:${match.line} [${match.query}] ${match.snippet}`), - ].join('\n') - : ''; - const needsContinuation = - result.partial === true || - !result.ok || - Boolean(limitReasons) || - result.terminalStatus === 'completed_empty'; - const continuationReason = needsContinuation - ? presentExploreAgentContinuationReason({ - partial: result.partial === true, - ok: result.ok, - hasLimitReasons: Boolean(limitReasons), - terminalStatus: result.terminalStatus, - }, locale) - : ''; - const continuationText = needsContinuation - ? [ - copy.continuationIntro, - continuationReason ? `${copy.field.continuationReason}: ${continuationReason}` : '', - `${copy.field.previousStatus}: ${status}`, - `${copy.field.previousTerminal}: ${terminalStatus}`, - `${copy.field.objective}: ${result.objective || copy.objectiveFallback}`, - `${copy.field.scope}: ${roots}`, - `${copy.field.queries}: ${queries}`, - `${copy.field.foundRead}: ${filesDiscovered} / ${copy.files(result.filesInspected)}`, - duration ? `${copy.field.previousDuration}: ${duration}` : '', - ignoredPaths ? `${copy.field.ignored}: ${ignoredPaths}` : '', - stoppingCondition ? `${copy.field.stopping}: ${stoppingCondition}` : '', - limitReasons ? `${copy.field.previousBoundary}: ${limitReasons}` : '', - resultSummary ? `${copy.field.previousSummary}: ${resultSummary}` : '', - candidateFiles.length > 0 - ? [ - copy.continuationCandidates, - ...candidateFiles.slice(0, 5).map((file) => `- ${file.path} (${copy.score(file.score)})`), - ].join('\n') - : '', - matches.length > 0 - ? [ - copy.continuationMatches, - ...matches.slice(0, 5).map((match) => `- ${match.path}:${match.line} [${match.query}] ${match.snippet}`), - ].join('\n') - : '', - copy.continuationOutro, - ].filter((line) => line.trim().length > 0).join('\n') - : ''; - const copyPayloads = { - summary: redactSecrets(summaryText), - process: redactSecrets(processText), - evidence: redactSecrets(evidenceText), - report: redactSecrets(reportText), - candidate: redactSecrets(candidateText), - matches: redactSecrets(matchesText), - continuation: redactSecrets(continuationText), - }; - - return { - candidateFiles, - matches, - progress, - evidence, - resultSummary, - reportLines, - notes, - roots, - queries, - ignoredPaths, - stoppingCondition, - limitReasons, - filesDiscovered, - skippedSummary, - duration, - terminalStatus, - status, - continuationReason, - continuationText, - copyPayloads, - }; -} - -function presentExploreAgentTerminalStatus( - terminalStatus: Extract['terminalStatus'], - ok: boolean, - partial: boolean, - reason: Extract['reason'], - locale: UiLocale, -): string { - const copy = getToolActivityCopy(locale).agent; - switch (terminalStatus) { - case 'completed': - return copy.terminalStatus.completed; - case 'completed_empty': - return copy.terminalStatus.completed_empty; - case 'failed': - return copy.terminalStatus.failed; - case 'canceled': - return copy.terminalStatus.canceled; - case 'canceled_partial': - return copy.terminalStatus.canceled_partial; - case undefined: - if (reason === 'aborted' && partial) return copy.terminalStatus.canceled_partial; - if (reason === 'aborted') return copy.terminalStatus.canceled; - if (!ok) return copy.terminalStatus.failed; - return copy.complete; - default: - return copy.terminalStatus.unknown; - } -} - -function presentExploreAgentReason( - reason: Extract['reason'], - locale: UiLocale, -): string | undefined { - const copy = getToolActivityCopy(locale).agent.reason; - switch (reason) { - case 'invalid_objective': - return copy.invalid_objective; - case 'invalid_root': - return copy.invalid_root; - case 'no_readable_roots': - return copy.no_readable_roots; - case 'aborted': - return copy.aborted; - case undefined: - return undefined; - default: - return copy.unknown; - } -} - -function presentExploreAgentLimitReason(reason: string, locale: UiLocale): string { - const copy = getToolActivityCopy(locale).agent.limitReason; - switch (reason) { - case 'candidate_budget': - return copy.candidate_budget; - case 'file_budget': - return copy.file_budget; - case 'match_budget': - return copy.match_budget; - case 'byte_budget': - return copy.byte_budget; - default: - return ''; - } -} - -function presentExploreAgentContinuationReason(input: { - partial: boolean; - ok: boolean; - hasLimitReasons: boolean; - terminalStatus: Extract['terminalStatus']; -}, locale: UiLocale): string { - const copy = getToolActivityCopy(locale).agent.continuationReason; - if (input.partial) return copy.partial; - if (!input.ok) return copy.failed; - if (input.hasLimitReasons) return copy.budget; - if (input.terminalStatus === 'completed_empty') return copy.empty; - return copy.missing; -} - -function formatExploreAgentEvent(event: { type: string; message: string; at?: number }, startedAt: number | undefined, locale: UiLocale): string { - const label = presentExploreAgentEventType(event.type, locale); - const message = typeof event.message === 'string' ? event.message.trim() : ''; - const offset = formatExploreAgentEventOffset(event.at, startedAt); - const prefix = [label, offset].filter(Boolean).join(' '); - return prefix ? `${prefix}:${message}` : message; -} - -function formatExploreAgentEventOffset(at: number | undefined, startedAt: number | undefined): string { - if (typeof at !== 'number' || typeof startedAt !== 'number') return ''; - if (!Number.isFinite(at) || !Number.isFinite(startedAt)) return ''; - const delta = Math.max(0, Math.floor(at - startedAt)); - const formatted = formatDuration(delta); - return formatted ? `+${formatted}` : ''; -} - -function presentExploreAgentEventType(type: string, locale: UiLocale): string { - const copy = getToolActivityCopy(locale).agent.eventType; - switch (type) { - case 'started': - return copy.started; - case 'scope_resolved': - return copy.scope_resolved; - case 'scan': - return copy.scan; - case 'read': - return copy.read; - case 'checkpoint': - return copy.checkpoint; - case 'completed': - return copy.completed; - case 'failed': - return copy.failed; - case 'aborted': - return copy.aborted; - default: - return ''; - } -} - -function presentExploreAgentCandidateReasons(reasons: string[], locale: UiLocale): string { - const copy = getToolActivityCopy(locale).agent; - return reasons.map((reason) => { - if (reason in copy.candidateReason && reason !== 'fallback') return copy.candidateReason[reason as Exclude]; - const pathMatch = reason.match(/^path contains "(.+)"$/); - if (pathMatch) return copy.pathMatch(redactSecrets(pathMatch[1] ?? '')); - return copy.candidateReason.fallback; - }).join(', '); -} diff --git a/packages/ui/src/tool-activity/copy.ts b/packages/ui/src/tool-activity/copy.ts index 8dc204d075..bc1fb04be6 100644 --- a/packages/ui/src/tool-activity/copy.ts +++ b/packages/ui/src/tool-activity/copy.ts @@ -174,37 +174,6 @@ export interface ToolActivityCopy { string >; readOnly: string; - duration: (value: string) => string; - copyState: { pending: string; copied: string; failed: string; pendingAria: (label: string) => string; failedAria: (label: string) => string }; - copyButtons: Record<'summary' | 'continuation' | 'process' | 'evidence' | 'report' | 'candidate' | 'matches', { idle: string; copied: string }>; - objectiveFallback: string; - foundRead: (found: number, read: number) => string; - skipped: (count: number, sensitive?: number) => string; - budgetLimited: string; - continuationSuggested: (reason: string) => string; - followupActionsAriaLabel: string; - continuationTitle: string; - incompleteFallback: string; - detail: { terminal: string; foundRead: string; scope: string; queries: string; ignored: string; stopping: string; boundary: string; next: string }; - files: (count: number) => string; - section: Record<'process' | 'evidence' | 'report' | 'candidates' | 'matches' | 'notes', { ariaLabel: string; title: string }>; - score: (score: number) => string; - complete: string; - cancelledPartial: string; - incomplete: string; - notSpecified: string; - field: Record<'status' | 'terminal' | 'objective' | 'summary' | 'scope' | 'queries' | 'foundRead' | 'duration' | 'ignored' | 'stopping' | 'boundary' | 'events' | 'evidence' | 'candidates' | 'matches' | 'continuationReason' | 'previousStatus' | 'previousTerminal' | 'previousDuration' | 'previousBoundary' | 'previousSummary', string>; - terminalStatus: Record<'completed' | 'completed_empty' | 'failed' | 'canceled' | 'canceled_partial' | 'unknown', string>; - reason: Record<'invalid_objective' | 'invalid_root' | 'no_readable_roots' | 'aborted' | 'unknown', string>; - limitReason: Record<'candidate_budget' | 'file_budget' | 'match_budget' | 'byte_budget', string>; - continuationReason: Record<'partial' | 'failed' | 'budget' | 'empty' | 'missing', string>; - continuationIntro: string; - continuationCandidates: string; - continuationMatches: string; - continuationOutro: string; - eventType: Record<'started' | 'scope_resolved' | 'scan' | 'read' | 'checkpoint' | 'completed' | 'failed' | 'aborted', string>; - candidateReason: Record<'content match' | 'project manifest' | 'project documentation' | 'project entrypoint' | 'project test surface' | 'project source surface' | 'fallback', string>; - pathMatch: (path: string) => string; }; } @@ -309,14 +278,8 @@ const TOOL_ACTIVITY_COPY = { fileWritten: (bytes, path) => `已向 ${path} 写入 ${bytes} 字节`, }, agent: { - subagentStatus: { completed: '已完成', failed: '失败', cancelled: '已取消', running: '运行中', waiting_for_user: '等待用户输入' }, readOnly: '只读', duration: (value) => `耗时 ${value}`, - copyState: { pending: '复制中…', copied: '已复制', failed: '复制失败', pendingAria: (label) => `${label}中`, failedAria: (label) => `${label}失败` }, - copyButtons: { summary: { idle: '复制摘要', copied: '已复制探索摘要' }, continuation: { idle: '复制续研提示', copied: '已复制续研提示' }, process: { idle: '复制过程', copied: '已复制探索过程' }, evidence: { idle: '复制证据', copied: '已复制证据锚点' }, report: { idle: '复制报告', copied: '已复制研究报告' }, candidate: { idle: '复制候选', copied: '已复制候选文件' }, matches: { idle: '复制片段', copied: '已复制命中片段' } }, - objectiveFallback: '只读探索', foundRead: (found, read) => `发现/读 ${found} / ${read} 个文件`, skipped: (count, sensitive) => sensitive ? `跳过 ${count} 个(含敏感 ${sensitive} 个)` : `跳过 ${count} 个`, budgetLimited: '受预算限制', continuationSuggested: (reason) => `建议续研:${reason}`, followupActionsAriaLabel: '只读探索后续操作', continuationTitle: '复制一段可继续只读探索的提示', incompleteFallback: '只读探索未完成。', detail: { terminal: '终态', foundRead: '发现/读', scope: '范围', queries: '查询', ignored: '忽略', stopping: '停止', boundary: '边界', next: '后续' }, files: (n) => `${n} 个文件`, - section: { process: { ariaLabel: '探索过程', title: '过程' }, evidence: { ariaLabel: '证据锚点', title: '证据锚点' }, report: { ariaLabel: '研究报告', title: '研究报告' }, candidates: { ariaLabel: '候选文件', title: '候选文件' }, matches: { ariaLabel: '命中片段', title: '命中片段' }, notes: { ariaLabel: '探索说明', title: '说明' } }, score: (score) => `分数 ${score}`, complete: '已完成', cancelledPartial: '已取消 · 保留部分结果', incomplete: '未完成', notSpecified: '未指定', - field: { status: '状态', terminal: '终态', objective: '目标', summary: '摘要', scope: '范围', queries: '查询', foundRead: '发现/读取', duration: '耗时', ignored: '忽略', stopping: '停止条件', boundary: '预算边界', events: '事件', evidence: '证据', candidates: '候选', matches: '命中片段', continuationReason: '续研原因', previousStatus: '上一轮状态', previousTerminal: '上一轮终态', previousDuration: '上一轮耗时', previousBoundary: '上一轮预算边界', previousSummary: '上一轮摘要' }, - terminalStatus: { completed: '完成,有证据', completed_empty: '完成,无证据', failed: '失败', canceled: '已取消', canceled_partial: '已取消,有部分结果', unknown: '未知终态' }, reason: { invalid_objective: '目标无效', invalid_root: '范围无效', no_readable_roots: '没有可读取范围', aborted: '已取消', unknown: '未知诊断' }, limitReason: { candidate_budget: '候选文件预算已满', file_budget: '读取文件预算已满', match_budget: '命中预算已满', byte_budget: '读取字节预算已满' }, continuationReason: { partial: '已有部分结果,仍需补证据', failed: '上一轮未完成', budget: '达到预算边界', empty: '没有找到证据', missing: '仍缺证据' }, - continuationIntro: '继续这次只读探索,不要修改文件。', continuationCandidates: '优先补读候选:', continuationMatches: '已有命中片段:', continuationOutro: '请只读检查仍缺证据的部分,输出新的证据锚点、候选文件、结论和下一步 gate。', eventType: { started: '开始', scope_resolved: '范围', scan: '扫描', read: '读取', checkpoint: '进度', completed: '完成', failed: '失败', aborted: '取消' }, candidateReason: { 'content match': '内容命中', 'project manifest': '项目配置', 'project documentation': '项目文档', 'project entrypoint': '入口文件', 'project test surface': '测试线索', 'project source surface': '源码线索', fallback: '探索线索' }, pathMatch: (path) => `路径命中 ${path}`, + subagentStatus: { completed: '已完成', failed: '失败', cancelled: '已取消', running: '运行中', waiting_for_user: '等待用户输入' }, + readOnly: '只读', }, }, en: { @@ -416,14 +379,8 @@ const TOOL_ACTIVITY_COPY = { fileWritten: (bytes, path) => `Wrote ${bytes} bytes to ${path}`, }, agent: { - subagentStatus: { completed: 'Completed', failed: 'Failed', cancelled: 'Cancelled', running: 'Running', waiting_for_user: 'Waiting for user input' }, readOnly: 'Read only', duration: (value) => `Duration ${value}`, - copyState: { pending: 'Copying…', copied: 'Copied', failed: 'Copy failed', pendingAria: (label) => `Copying ${label}`, failedAria: (label) => `Failed to copy ${label}` }, - copyButtons: { summary: { idle: 'Copy summary', copied: 'Exploration summary copied' }, continuation: { idle: 'Copy continuation prompt', copied: 'Continuation prompt copied' }, process: { idle: 'Copy process', copied: 'Exploration process copied' }, evidence: { idle: 'Copy evidence', copied: 'Evidence anchors copied' }, report: { idle: 'Copy report', copied: 'Research report copied' }, candidate: { idle: 'Copy candidates', copied: 'Candidate files copied' }, matches: { idle: 'Copy matches', copied: 'Matching excerpts copied' } }, - objectiveFallback: 'Read-only exploration', foundRead: (found, read) => `Discovered/read ${found} / ${read} files`, skipped: (count, sensitive) => sensitive ? `Skipped ${count} (${sensitive} sensitive)` : `Skipped ${count}`, budgetLimited: 'Budget limited', continuationSuggested: (reason) => `Continue: ${reason}`, followupActionsAriaLabel: 'Read-only exploration follow-up actions', continuationTitle: 'Copy a prompt that continues the read-only exploration', incompleteFallback: 'Read-only exploration did not complete.', detail: { terminal: 'Terminal state', foundRead: 'Discovered/read', scope: 'Scope', queries: 'Queries', ignored: 'Ignored', stopping: 'Stopped by', boundary: 'Limits', next: 'Next' }, files: (n) => `${n} ${n === 1 ? 'file' : 'files'}`, - section: { process: { ariaLabel: 'Exploration process', title: 'Process' }, evidence: { ariaLabel: 'Evidence anchors', title: 'Evidence anchors' }, report: { ariaLabel: 'Research report', title: 'Research report' }, candidates: { ariaLabel: 'Candidate files', title: 'Candidate files' }, matches: { ariaLabel: 'Matching excerpts', title: 'Matching excerpts' }, notes: { ariaLabel: 'Exploration notes', title: 'Notes' } }, score: (score) => `Score ${score}`, complete: 'Completed', cancelledPartial: 'Cancelled · Partial results retained', incomplete: 'Incomplete', notSpecified: 'Not specified', - field: { status: 'Status', terminal: 'Terminal state', objective: 'Objective', summary: 'Summary', scope: 'Scope', queries: 'Queries', foundRead: 'Discovered/read', duration: 'Duration', ignored: 'Ignored', stopping: 'Stopping condition', boundary: 'Budget limits', events: 'Events', evidence: 'Evidence', candidates: 'Candidates', matches: 'Matching excerpts', continuationReason: 'Continuation reason', previousStatus: 'Previous status', previousTerminal: 'Previous terminal state', previousDuration: 'Previous duration', previousBoundary: 'Previous budget limits', previousSummary: 'Previous summary' }, - terminalStatus: { completed: 'Completed with evidence', completed_empty: 'Completed without evidence', failed: 'Failed', canceled: 'Cancelled', canceled_partial: 'Cancelled with partial results', unknown: 'Unknown terminal state' }, reason: { invalid_objective: 'Invalid objective', invalid_root: 'Invalid scope', no_readable_roots: 'No readable scope', aborted: 'Cancelled', unknown: 'Unknown diagnostic' }, limitReason: { candidate_budget: 'Candidate-file budget reached', file_budget: 'File-read budget reached', match_budget: 'Match budget reached', byte_budget: 'Byte-read budget reached' }, continuationReason: { partial: 'Partial results need more evidence', failed: 'The previous run did not complete', budget: 'A budget limit was reached', empty: 'No evidence was found', missing: 'More evidence is needed' }, - continuationIntro: 'Continue this read-only exploration without changing files.', continuationCandidates: 'Prioritize these candidates:', continuationMatches: 'Existing matching excerpts:', continuationOutro: 'Inspect only the areas that still lack evidence, then return new evidence anchors, candidate files, conclusions, and the next gate.', eventType: { started: 'Started', scope_resolved: 'Scope', scan: 'Scan', read: 'Read', checkpoint: 'Progress', completed: 'Completed', failed: 'Failed', aborted: 'Cancelled' }, candidateReason: { 'content match': 'Content match', 'project manifest': 'Project manifest', 'project documentation': 'Project documentation', 'project entrypoint': 'Entry point', 'project test surface': 'Test signal', 'project source surface': 'Source signal', fallback: 'Exploration signal' }, pathMatch: (path) => `Path match ${path}`, + subagentStatus: { completed: 'Completed', failed: 'Failed', cancelled: 'Cancelled', running: 'Running', waiting_for_user: 'Waiting for user input' }, + readOnly: 'Read only', }, }, } satisfies UiCatalog; diff --git a/packages/ui/src/tool-activity/result-projection.ts b/packages/ui/src/tool-activity/result-projection.ts index 4a47e471d1..8f635efcf8 100644 --- a/packages/ui/src/tool-activity/result-projection.ts +++ b/packages/ui/src/tool-activity/result-projection.ts @@ -75,7 +75,6 @@ export function resultOwnsOwnPanel(item: ToolActivityItem): boolean { switch (result.kind) { case 'terminal': case 'shell_run': - case 'explore_agent': case 'web_search': case 'web_search_error': case 'file_diff': diff --git a/packages/ui/src/tool-activity/tool-result-preview.tsx b/packages/ui/src/tool-activity/tool-result-preview.tsx index a78e88daba..938f7eaf85 100644 --- a/packages/ui/src/tool-activity/tool-result-preview.tsx +++ b/packages/ui/src/tool-activity/tool-result-preview.tsx @@ -29,7 +29,6 @@ import { redactSecrets } from '../redact.js'; import { useClipboardCopyFeedback } from '../clipboard-feedback.js'; import { useUiLocale } from '../locale-context.js'; import { cn } from '../ui.js'; -import { ExploreAgentPreview } from './agent-preview.js'; import { formatQuietJsonValue } from './builtin-preview.js'; import { ToolCodeBlock } from './tool-code-block.js'; import { DiffCodePreview } from './diff-code-preview.js'; @@ -212,10 +211,6 @@ export function ToolResultPreview(props: { ); } - if (content.kind === 'explore_agent') { - return ; - } - if (content.kind === 'rive_workflow') { return ; } diff --git a/packages/ui/stories/tool-activity.fixtures.ts b/packages/ui/stories/tool-activity.fixtures.ts index 0aaa267760..0186063039 100644 --- a/packages/ui/stories/tool-activity.fixtures.ts +++ b/packages/ui/stories/tool-activity.fixtures.ts @@ -168,10 +168,10 @@ export const statusOverviewItems = [ }), toolItem({ toolUseId: 'status-interrupted', - toolName: 'explore', - displayName: 'Explore repository', + toolName: 'Grep', + displayName: 'Search repository', status: 'interrupted', - args: { roots: ['packages/ui/src'], query: 'ToolActivity' }, + args: { pattern: 'ToolActivity', path: 'packages/ui/src' }, result: { kind: 'text', text: 'The turn was interrupted after partial output was retained.' }, durationMs: 9_360, }), diff --git a/packages/ui/stories/tool-activity.stories.tsx b/packages/ui/stories/tool-activity.stories.tsx index 2c77e07804..8dba762673 100644 --- a/packages/ui/stories/tool-activity.stories.tsx +++ b/packages/ui/stories/tool-activity.stories.tsx @@ -235,28 +235,30 @@ export const ContiguousDiffGroup: Story = { const longIntentItems: ToolActivityItem[] = [ { - toolUseId: 'explore-agent-long-intent', - toolName: 'ExploreAgent', - displayName: '只读探索', + toolUseId: 'grep-long-intent', + toolName: 'Grep', + displayName: 'Search repository', activityKind: 'tool', status: 'completed', - intent: '审计模型选择、切换、持久化、event log、replay/resume 路径:当前模型事实由谁持有,何时切换,是否已有事件或消息可表达', - args: {}, + intent: + '审计模型选择、切换、持久化、event log、replay/resume 路径:当前模型事实由谁持有,何时切换,是否已有事件或消息可表达', + args: { pattern: 'model|replay|resume', path: 'packages' }, }, { - toolUseId: 'explore-agent-long-intent-2', - toolName: 'ExploreAgent', - displayName: '只读探索', + toolUseId: 'grep-long-intent-2', + toolName: 'Grep', + displayName: 'Search repository', activityKind: 'tool', status: 'completed', - intent: '审计提示词构筑与缓存路径:追踪 durable system prompt、turnTailPrompt、provider-visible messages 与 request shape', - args: {}, + intent: + '审计提示词构筑与缓存路径:追踪 durable system prompt、turnTailPrompt、provider-visible messages 与 request shape', + args: { pattern: 'systemPrompt|turnTailPrompt', path: 'packages' }, }, ]; -// Regression path: a narrow conversation contains a grouped ExploreAgent run -// whose intent summaries are much wider than the reading column. Expanding the -// group must truncate those rows without widening the turn itself. +// Regression path: a narrow conversation contains a grouped tool run whose +// intent summaries are much wider than the reading column. Expanding the group +// must truncate those rows without widening the turn itself. export const LongIntentGroupNarrow: Story = { args: { items: longIntentItems }, render: (args) => (