From 9cc43332ba2fd18c9bbb0c3e90fb5fbbb9d1fe4b Mon Sep 17 00:00:00 2001 From: liukx0205 <69756503+liukx0205@users.noreply.github.com> Date: Mon, 7 Sep 2026 09:48:43 +0000 Subject: [PATCH 1/4] feat(agent-core-v2): allow read-only tools in /btw side questions The btw child agent previously vetoed every tool call. Allow the read-only tools Read, Grep, and Glob so side questions about the codebase can be answered from current file contents; write and execute tools stay disabled. --- .changeset/btw-readonly-tools.md | 5 ++ docs/en/reference/server-api.md | 2 +- docs/zh/reference/server-api.md | 2 +- .../agent-core-v2/src/features/btw/btw.ts | 17 +++--- .../src/features/btw/btwService.ts | 11 +++- .../test/features/btw/btw.test.ts | 54 +++++++++++++------ 6 files changed, 64 insertions(+), 27 deletions(-) create mode 100644 .changeset/btw-readonly-tools.md diff --git a/.changeset/btw-readonly-tools.md b/.changeset/btw-readonly-tools.md new file mode 100644 index 00000000000..05e0478e050 --- /dev/null +++ b/.changeset/btw-readonly-tools.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": minor +--- + +Add read-only tools to the /btw side agent. It can now read and search files to answer questions about the codebase; write and execute tools stay disabled. diff --git a/docs/en/reference/server-api.md b/docs/en/reference/server-api.md index 33948cd726b..3fbb866d100 100644 --- a/docs/en/reference/server-api.md +++ b/docs/en/reference/server-api.md @@ -751,7 +751,7 @@ On success, `data` is `{ aborted: true }`. #### `POST /api/v1/sessions/{session_id}:btw` -Starts a "by the way" side conversation: forks the main agent into a child agent whose tool calls are disabled, so quick side questions run in isolation without touching the working context. Requires a usable model configuration. +Starts a "by the way" side conversation: forks the main agent into a child agent whose tool calls are limited to the read-only tools Read, Grep, and Glob, so quick side questions run in isolation without touching the working context. Requires a usable model configuration. On success, `data` is `{ agent_id }` — the id of the new child agent. diff --git a/docs/zh/reference/server-api.md b/docs/zh/reference/server-api.md index 2652e0a980b..ee8b250fc8e 100644 --- a/docs/zh/reference/server-api.md +++ b/docs/zh/reference/server-api.md @@ -751,7 +751,7 @@ schema 还接受 `agent_config` 内的 `system_prompt`、`tools`、`mcp_servers` #### `POST /api/v1/sessions/{session_id}:btw` -开启一个 `"by the way"` 旁路对话:把 main agent fork 成一个禁用工具调用的子 Agent,让快速的临时问题在隔离环境中运行,不触碰工作上下文。需要可用的模型配置。 +开启一个 `"by the way"` 旁路对话:把 main agent fork 成一个仅可使用只读工具(Read、Grep、Glob)的子 Agent,让快速的临时问题在隔离环境中运行,不触碰工作上下文。需要可用的模型配置。 成功时,`data` 为 `{ agent_id }`——新子 Agent 的 id。 diff --git a/packages/agent-core-v2/src/features/btw/btw.ts b/packages/agent-core-v2/src/features/btw/btw.ts index 00a09e751bf..7fa91fc3f89 100644 --- a/packages/agent-core-v2/src/features/btw/btw.ts +++ b/packages/agent-core-v2/src/features/btw/btw.ts @@ -1,19 +1,22 @@ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; +export const BTW_READONLY_TOOLS = new Set(['Read', 'Grep', 'Glob']); + export const TOOL_CALL_DISABLED_MESSAGE = - 'Tool calls are disabled for side questions. Answer with text only.'; + 'Only the read-only tools Read, Grep, and Glob are available for side questions. Other tool calls are disabled.'; export const SIDE_QUESTION_SYSTEM_REMINDER = ` -This is a side-channel conversation with the user. You should answer user questions directly based on what you already know. +This is a side-channel conversation with the user. You should answer user questions directly. IMPORTANT: - You are a separate, lightweight instance. - The main agent continues independently; do not reference being interrupted. -- Do not call any tools. All tool calls are disabled and will be rejected. - Even though tool definitions are visible in this request, they exist only - for technical reasons (prompt cache). You must not use them. -- Respond only with text based on what you already know from the conversation - and this side-channel conversation. +- You may use the read-only tools Read, Grep, and Glob to inspect files when + the answer depends on current file contents. All other tools are disabled + and will be rejected, even though their definitions are visible in this + request (they exist only for technical reasons — prompt cache). +- Prefer answering from what you already know from the conversation and this + side-channel conversation; reach for the read-only tools only when needed. - Follow-up turns may happen in this side-channel conversation. - If you do not know the answer, say so directly. `.trim(); diff --git a/packages/agent-core-v2/src/features/btw/btwService.ts b/packages/agent-core-v2/src/features/btw/btwService.ts index 933e5995b98..aca4b12825c 100644 --- a/packages/agent-core-v2/src/features/btw/btwService.ts +++ b/packages/agent-core-v2/src/features/btw/btwService.ts @@ -6,7 +6,12 @@ import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; import { ErrorCodes, Error2 } from '#/errors'; import { IAgentLifecycleService, MAIN_AGENT_ID } from '#/session/agentLifecycle/agentLifecycle'; -import { ISessionBtwService, SIDE_QUESTION_SYSTEM_REMINDER, TOOL_CALL_DISABLED_MESSAGE } from './btw'; +import { + BTW_READONLY_TOOLS, + ISessionBtwService, + SIDE_QUESTION_SYSTEM_REMINDER, + TOOL_CALL_DISABLED_MESSAGE, +} from './btw'; export class SessionBtwService implements ISessionBtwService { declare readonly _serviceBrand: undefined; @@ -32,7 +37,9 @@ export class SessionBtwService implements ISessionBtwService { child.accessor .get(IAgentToolExecutorService) ?.onBeforeExecuteTool((event) => { - event.veto(denyToolExecution(reason)); + if (!BTW_READONLY_TOOLS.has(event.toolCall.name)) { + event.veto(denyToolExecution(reason)); + } }); return childContext.agentId; } diff --git a/packages/agent-core-v2/test/features/btw/btw.test.ts b/packages/agent-core-v2/test/features/btw/btw.test.ts index 8f45dcbc477..c83b098267b 100644 --- a/packages/agent-core-v2/test/features/btw/btw.test.ts +++ b/packages/agent-core-v2/test/features/btw/btw.test.ts @@ -7,6 +7,7 @@ import { TestInstantiationService } from '#/_base/di/test'; import { IAgentToolApprovalService } from '#/agent/toolApproval/toolApproval'; import { IAgentToolExecutorService } from '#/agent/toolExecutor/toolExecutor'; import { + BTW_READONLY_TOOLS, ISessionBtwService, SIDE_QUESTION_SYSTEM_REMINDER, TOOL_CALL_DISABLED_MESSAGE, @@ -86,26 +87,47 @@ describe('SessionBtwService', () => { }); }); - it('vetoes every tool call on the child through the btw deny listener', async () => { + it('vetoes non-read-only tool calls on the child through the btw deny listener', async () => { const svc = ix.get(ISessionBtwService); await svc.start(); - const toolCall: ToolCall = { type: 'function', id: 'call_1', name: 'Bash', arguments: '{}' }; - const decision = await executorEvents.fireBeforeExecute({ - turnId: 0, - signal: new AbortController().signal, - toolCall, - toolCalls: [toolCall], - args: {}, - execution: { approvalRule: 'Bash', execute: async () => ({ output: '' }) }, - }); + for (const name of ['Bash', 'Write', 'Edit']) { + const toolCall: ToolCall = { type: 'function', id: `call_${name}`, name, arguments: '{}' }; + const decision = await executorEvents.fireBeforeExecute({ + turnId: 0, + signal: new AbortController().signal, + toolCall, + toolCalls: [toolCall], + args: {}, + execution: { approvalRule: name, execute: async () => ({ output: '' }) }, + }); - expect(decision).toEqual({ - veto: { - output: `${TOOL_CALL_DISABLED_MESSAGE} [worker guidance]`, - isError: true, - }, - }); + expect(decision).toEqual({ + veto: { + output: `${TOOL_CALL_DISABLED_MESSAGE} [worker guidance]`, + isError: true, + }, + }); + } expect(formatDenyMessage).toHaveBeenCalledWith(TOOL_CALL_DISABLED_MESSAGE); }); + + it('allows read-only tool calls (Read, Grep, Glob) on the child', async () => { + const svc = ix.get(ISessionBtwService); + await svc.start(); + + for (const name of BTW_READONLY_TOOLS) { + const toolCall: ToolCall = { type: 'function', id: `call_${name}`, name, arguments: '{}' }; + const decision = await executorEvents.fireBeforeExecute({ + turnId: 0, + signal: new AbortController().signal, + toolCall, + toolCalls: [toolCall], + args: {}, + execution: { approvalRule: name, execute: async () => ({ output: '' }) }, + }); + + expect(decision).toBeUndefined(); + } + }); }); From ef918b3da831ce0871afdf049187d08308670f8c Mon Sep 17 00:00:00 2001 From: liukx0205 <69756503+liukx0205@users.noreply.github.com> Date: Mon, 7 Sep 2026 09:56:29 +0000 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20address=20review=20=E2=80=94=20sing?= =?UTF-8?q?le-sentence=20changeset,=20inline-code=20tool=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/btw-readonly-tools.md | 2 +- docs/en/reference/server-api.md | 2 +- docs/zh/reference/server-api.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/btw-readonly-tools.md b/.changeset/btw-readonly-tools.md index 05e0478e050..3f85ec8e172 100644 --- a/.changeset/btw-readonly-tools.md +++ b/.changeset/btw-readonly-tools.md @@ -2,4 +2,4 @@ "@moonshot-ai/kimi-code": minor --- -Add read-only tools to the /btw side agent. It can now read and search files to answer questions about the codebase; write and execute tools stay disabled. +Add read-only tools to the /btw side agent so it can answer questions about files in the codebase. diff --git a/docs/en/reference/server-api.md b/docs/en/reference/server-api.md index 3fbb866d100..f38aa633d7a 100644 --- a/docs/en/reference/server-api.md +++ b/docs/en/reference/server-api.md @@ -751,7 +751,7 @@ On success, `data` is `{ aborted: true }`. #### `POST /api/v1/sessions/{session_id}:btw` -Starts a "by the way" side conversation: forks the main agent into a child agent whose tool calls are limited to the read-only tools Read, Grep, and Glob, so quick side questions run in isolation without touching the working context. Requires a usable model configuration. +Starts a "by the way" side conversation: forks the main agent into a child agent whose tool calls are limited to the read-only tools `Read`, `Grep`, and `Glob`, so quick side questions run in isolation without touching the working context. Requires a usable model configuration. On success, `data` is `{ agent_id }` — the id of the new child agent. diff --git a/docs/zh/reference/server-api.md b/docs/zh/reference/server-api.md index ee8b250fc8e..530dfd3cb4b 100644 --- a/docs/zh/reference/server-api.md +++ b/docs/zh/reference/server-api.md @@ -751,7 +751,7 @@ schema 还接受 `agent_config` 内的 `system_prompt`、`tools`、`mcp_servers` #### `POST /api/v1/sessions/{session_id}:btw` -开启一个 `"by the way"` 旁路对话:把 main agent fork 成一个仅可使用只读工具(Read、Grep、Glob)的子 Agent,让快速的临时问题在隔离环境中运行,不触碰工作上下文。需要可用的模型配置。 +开启一个 `"by the way"` 旁路对话:把 main agent fork 成一个仅可使用只读工具(`Read`、`Grep`、`Glob`)的子 Agent,让快速的临时问题在隔离环境中运行,不触碰工作上下文。需要可用的模型配置。 成功时,`data` 为 `{ agent_id }`——新子 Agent 的 id。 From 275a264be47b5cb5c04cd2aeb275c1aefc27a249 Mon Sep 17 00:00:00 2001 From: liukx0205 <69756503+liukx0205@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:12:54 +0000 Subject: [PATCH 3/4] chore: drop the benefit clause from the btw changeset --- .changeset/btw-readonly-tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/btw-readonly-tools.md b/.changeset/btw-readonly-tools.md index 3f85ec8e172..52e43d02e91 100644 --- a/.changeset/btw-readonly-tools.md +++ b/.changeset/btw-readonly-tools.md @@ -2,4 +2,4 @@ "@moonshot-ai/kimi-code": minor --- -Add read-only tools to the /btw side agent so it can answer questions about files in the codebase. +Add read-only tools to the /btw side agent. From 8e513195a1a7e1ac365e910c259286eae0957ca2 Mon Sep 17 00:00:00 2001 From: liukx0205 <69756503+liukx0205@users.noreply.github.com> Date: Mon, 7 Sep 2026 11:14:27 +0000 Subject: [PATCH 4/4] feat(cli): send pasted images to the /btw side agent The btw panel's input path was text-only: pasted image placeholders went to the side agent as literal text and the bytes never left the image store. Expand media placeholders into daemon file-ref parts exactly like the main send path (ingestion wait, extraction, capability validation, staged-media lease with an exact-binding submission id), allow the read-only ReadMediaFile tool in the btw child, and mention it in the side-question reminder. Stacked on #3613 (feat/btw-readonly-tools), which introduced the btw read-only tool allowlist this change extends. --- .changeset/btw-image-support.md | 5 ++ .../src/tui/controllers/btw-panel.ts | 79 +++++++++++++++---- apps/kimi-code/src/tui/kimi-tui.ts | 62 ++++++++++++++- .../test/tui/kimi-tui-message-flow.test.ts | 50 ++++++++++++ docs/en/reference/server-api.md | 2 +- docs/zh/reference/server-api.md | 2 +- .../agent-core-v2/src/features/btw/btw.ts | 12 +-- .../test/features/btw/btw.test.ts | 3 +- 8 files changed, 192 insertions(+), 23 deletions(-) create mode 100644 .changeset/btw-image-support.md diff --git a/.changeset/btw-image-support.md b/.changeset/btw-image-support.md new file mode 100644 index 00000000000..cd9647cec96 --- /dev/null +++ b/.changeset/btw-image-support.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": minor +--- + +Add image support to /btw: pasted images now reach the side agent, which can also view image and video files. diff --git a/apps/kimi-code/src/tui/controllers/btw-panel.ts b/apps/kimi-code/src/tui/controllers/btw-panel.ts index 2a46f0e9d7e..9e16caca714 100644 --- a/apps/kimi-code/src/tui/controllers/btw-panel.ts +++ b/apps/kimi-code/src/tui/controllers/btw-panel.ts @@ -2,6 +2,7 @@ import { Spacer } from '@moonshot-ai/pi-tui'; import type { Event, KimiHarness, + PromptInput, Session, TurnEndedEvent, } from '@moonshot-ai/kimi-code-sdk'; @@ -14,14 +15,40 @@ import { createMarkdownTheme } from '../theme/pi-tui-theme'; import type { InlineSkillActivation } from '../types'; import type { TUIState } from '../tui-state'; +import type { StagingLease } from './staging-leases'; + const BTW_BUSY_NOTICE = 'Wait for /btw to finish before sending another question.'; +export interface BtwPreparedPrompt { + /** Media-expanded RPC input; undefined means send the plain text prompt. */ + readonly input?: PromptInput; + /** Staged-media lease and its exact-binding submission id (plain prompts only). */ + readonly lease?: StagingLease; + readonly submissionId?: string; +} + export interface BtwPanelHost { state: TUIState; session: Session | undefined; readonly harness: KimiHarness; showError(msg: string): void; + /** + * Expand pasted image/video placeholders into daemon file-ref parts for the + * side agent (the /btw counterpart of the main send path's media + * preparation). Returns undefined when preparation failed — the error was + * already shown. + */ + prepareBtwPrompt( + text: string, + opts: { readonly stage: boolean }, + ): Promise; + /** Track a prompt dispatch carrying staged media; see StagingLeaseTracker.trackDispatch. */ + trackBtwDispatch( + lease: StagingLease | undefined, + request: Promise, + onError: (error: unknown) => void, + ): void; } export class BtwPanelController { @@ -180,27 +207,51 @@ export class BtwPanelController { panel: BtwPanelComponent, inlineSkillActivations?: readonly InlineSkillActivation[], ): void { + void this.prepareAndPrompt(agentId, prompt, panel, inlineSkillActivations); + } + + private async prepareAndPrompt( + agentId: string, + prompt: string, + panel: BtwPanelComponent, + inlineSkillActivations?: readonly InlineSkillActivation[], + ): Promise { const session = this.host.session; if (session === undefined) { panel.markFailed(NO_ACTIVE_SESSION_MESSAGE); this.host.state.ui.requestRender(); return; } - const send = - inlineSkillActivations !== undefined && inlineSkillActivations.length > 0 - ? () => - session.promptWithSkills( - prompt, - inlineSkillActivations.map((activation) => ({ - name: activation.skillName, - args: activation.args, - })), - ) - : () => session.prompt(prompt); - void this.withInteractiveAgent(agentId, send).catch((error: unknown) => { - panel.markFailed(`Failed to send /btw prompt: ${formatErrorMessage(error)}`); + const useSkills = inlineSkillActivations !== undefined && inlineSkillActivations.length > 0; + // Skill bundles have no prompt-id channel, so they match the main turn's + // inline-skill path: media rides along without a staged lease. + const prepared = await this.host.prepareBtwPrompt(prompt, { stage: !useSkills }); + if (prepared === undefined) { + panel.markFailed('Failed to prepare the media attachment.'); this.host.state.ui.requestRender(); - }); + return; + } + const input = prepared.input ?? prompt; + const send = useSkills + ? () => + session.promptWithSkills( + input, + inlineSkillActivations.map((activation) => ({ + name: activation.skillName, + args: activation.args, + })), + ) + : prepared.submissionId !== undefined + ? () => session.prompt(input, { promptId: prepared.submissionId }) + : () => session.prompt(input); + this.host.trackBtwDispatch( + prepared.lease, + this.withInteractiveAgent(agentId, send), + (error: unknown) => { + panel.markFailed(`Failed to send /btw prompt: ${formatErrorMessage(error)}`); + this.host.state.ui.requestRender(); + }, + ); } private async cancelAgent(agentId: string): Promise { diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 6b2ab9d606d..d7497e99ad4 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -120,7 +120,7 @@ import { MEDIA_INGESTION_SUBMIT_WAIT_MS } from './constant/media'; import { CHROME_GUTTER } from './constant/rendering'; import { MAX_TERMINAL_TITLE_LENGTH } from './constant/terminal'; import { AuthFlowController } from './controllers/auth-flow'; -import { BtwPanelController } from './controllers/btw-panel'; +import { BtwPanelController, type BtwPreparedPrompt } from './controllers/btw-panel'; import { ClipboardImageHintController } from './controllers/clipboard-image-hint'; import { EditorKeyboardController } from './controllers/editor-keyboard'; import { SessionEventHandler } from './controllers/session-event-handler'; @@ -1521,6 +1521,66 @@ export class KimiTUI { }); } + /** + * /btw counterpart of the send path's media preparation (see + * sendNormalUserInput): expands pasted image/video placeholders into daemon + * file-ref parts for the side agent. The side panel has no queue or + * cache-hint interception, so this stops at extraction + validation; staged + * prompts also get a media lease with an exact-binding submission id, while + * unstaged ones (skill bundles) match the main inline-skill path. + */ + async prepareBtwPrompt( + text: string, + opts: { readonly stage: boolean }, + ): Promise { + const ingestionWait = pendingMediaIngestions( + text, + this.imageStore, + MEDIA_INGESTION_SUBMIT_WAIT_MS, + ); + if (ingestionWait !== undefined) await ingestionWait; + let extraction: ReturnType; + try { + extraction = extractMediaAttachments(text, this.imageStore); + } catch (error) { + this.showError(`Failed to prepare media attachment: ${formatErrorMessage(error)}`); + return undefined; + } + if (!extraction.hasMedia) return {}; + const stagingLease = opts.stage + ? this.staging.create( + // One retain per unique id per extraction, exactly like the main + // send path's lease (see sendNormalUserInput). + [...new Set([...extraction.imageAttachmentIds, ...extraction.videoAttachmentIds])], + [], + 'user', + randomUUID(), + ) + : undefined; + if (!this.validateMediaCapabilities(extraction)) { + this.staging.release(stagingLease); + return undefined; + } + return { + input: resolveOriginalCaptions( + extraction.parts, + extraction.imageAttachmentIds, + this.imageStore, + originalsDirForSession(this.session), + ), + lease: stagingLease, + submissionId: stagingLease?.submissionId, + }; + } + + trackBtwDispatch( + lease: StagingLease | undefined, + request: Promise, + onError: (error: unknown) => void, + ): void { + this.staging.trackDispatch(lease, request, onError); + } + validateMediaCapabilities(extraction: { hasMedia: boolean; imageAttachmentIds: readonly number[]; diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts index f9c2120492b..c76d61f1ffa 100644 --- a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -4708,6 +4708,56 @@ command = "vim" expect(stripSgr(renderBtwPanel(driver))).toContain('Q: What are you working on right now?'); }); + it('sends a pasted image in the initial /btw prompt as daemon file-ref parts', async () => { + const session = makeSession(); + const { driver, harness } = await makeDriver(session); + const imageStore = (driver as unknown as { imageStore: ImageAttachmentStore }).imageStore; + const attachment = stagedImage(imageStore, 'file-btw'); + + driver.handleUserInput(`/btw describe ${attachment.placeholder}`); + + await vi.waitFor(() => { + expect(session.prompt).toHaveBeenCalledWith( + [ + { type: 'text', text: 'describe ' }, + { type: 'image_url', imageUrl: { url: 'kimi-file://file-btw' } }, + ], + { promptId: expect.any(String) }, + ); + }); + expect(harness.deleteFile).not.toHaveBeenCalled(); + emitTurn(driver, 1, () => { + expect(harness.deleteFile).not.toHaveBeenCalled(); + }); + await vi.waitFor(() => { + expect(harness.deleteFile).toHaveBeenCalledWith('file-btw'); + }); + }); + + it('sends a pasted image in follow-up /btw panel input as daemon file-ref parts', async () => { + const session = makeSession(); + const { driver } = await makeDriver(session); + driver.handleUserInput('/btw'); + await vi.waitFor(() => { + expect(session.startBtw).toHaveBeenCalled(); + expect(driver.state.btwPanelContainer.children).toHaveLength(2); + }); + const imageStore = (driver as unknown as { imageStore: ImageAttachmentStore }).imageStore; + const attachment = stagedImage(imageStore, 'file-btw-2'); + + driver.handleUserInput(`look at ${attachment.placeholder}`); + + await vi.waitFor(() => { + expect(session.prompt).toHaveBeenCalledWith( + [ + { type: 'text', text: 'look at ' }, + { type: 'image_url', imageUrl: { url: 'kimi-file://file-btw-2' } }, + ], + { promptId: expect.any(String) }, + ); + }); + }); + it('sends /btw panel input with inline skills via promptWithSkills (v2 engine)', async () => { const session = makeSession({ id: 'ses-lazy', diff --git a/docs/en/reference/server-api.md b/docs/en/reference/server-api.md index f38aa633d7a..d0a60fb7906 100644 --- a/docs/en/reference/server-api.md +++ b/docs/en/reference/server-api.md @@ -751,7 +751,7 @@ On success, `data` is `{ aborted: true }`. #### `POST /api/v1/sessions/{session_id}:btw` -Starts a "by the way" side conversation: forks the main agent into a child agent whose tool calls are limited to the read-only tools `Read`, `Grep`, and `Glob`, so quick side questions run in isolation without touching the working context. Requires a usable model configuration. +Starts a "by the way" side conversation: forks the main agent into a child agent whose tool calls are limited to the read-only tools `Read`, `Grep`, `Glob`, and `ReadMediaFile`, so quick side questions run in isolation without touching the working context. Requires a usable model configuration. On success, `data` is `{ agent_id }` — the id of the new child agent. diff --git a/docs/zh/reference/server-api.md b/docs/zh/reference/server-api.md index 530dfd3cb4b..10ecc56f38d 100644 --- a/docs/zh/reference/server-api.md +++ b/docs/zh/reference/server-api.md @@ -751,7 +751,7 @@ schema 还接受 `agent_config` 内的 `system_prompt`、`tools`、`mcp_servers` #### `POST /api/v1/sessions/{session_id}:btw` -开启一个 `"by the way"` 旁路对话:把 main agent fork 成一个仅可使用只读工具(`Read`、`Grep`、`Glob`)的子 Agent,让快速的临时问题在隔离环境中运行,不触碰工作上下文。需要可用的模型配置。 +开启一个 `"by the way"` 旁路对话:把 main agent fork 成一个仅可使用只读工具(`Read`、`Grep`、`Glob`、`ReadMediaFile`)的子 Agent,让快速的临时问题在隔离环境中运行,不触碰工作上下文。需要可用的模型配置。 成功时,`data` 为 `{ agent_id }`——新子 Agent 的 id。 diff --git a/packages/agent-core-v2/src/features/btw/btw.ts b/packages/agent-core-v2/src/features/btw/btw.ts index 7fa91fc3f89..21fbad74cb7 100644 --- a/packages/agent-core-v2/src/features/btw/btw.ts +++ b/packages/agent-core-v2/src/features/btw/btw.ts @@ -1,9 +1,9 @@ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; -export const BTW_READONLY_TOOLS = new Set(['Read', 'Grep', 'Glob']); +export const BTW_READONLY_TOOLS = new Set(['Read', 'Grep', 'Glob', 'ReadMediaFile']); export const TOOL_CALL_DISABLED_MESSAGE = - 'Only the read-only tools Read, Grep, and Glob are available for side questions. Other tool calls are disabled.'; + 'Only the read-only tools Read, Grep, Glob, and ReadMediaFile are available for side questions. Other tool calls are disabled.'; export const SIDE_QUESTION_SYSTEM_REMINDER = ` This is a side-channel conversation with the user. You should answer user questions directly. @@ -12,9 +12,11 @@ IMPORTANT: - You are a separate, lightweight instance. - The main agent continues independently; do not reference being interrupted. - You may use the read-only tools Read, Grep, and Glob to inspect files when - the answer depends on current file contents. All other tools are disabled - and will be rejected, even though their definitions are visible in this - request (they exist only for technical reasons — prompt cache). + the answer depends on current file contents, and ReadMediaFile to view an + image or video file (including images the user attached to their message). + All other tools are disabled and will be rejected, even though their + definitions are visible in this request (they exist only for technical + reasons — prompt cache). - Prefer answering from what you already know from the conversation and this side-channel conversation; reach for the read-only tools only when needed. - Follow-up turns may happen in this side-channel conversation. diff --git a/packages/agent-core-v2/test/features/btw/btw.test.ts b/packages/agent-core-v2/test/features/btw/btw.test.ts index c83b098267b..a8a9e789788 100644 --- a/packages/agent-core-v2/test/features/btw/btw.test.ts +++ b/packages/agent-core-v2/test/features/btw/btw.test.ts @@ -112,10 +112,11 @@ describe('SessionBtwService', () => { expect(formatDenyMessage).toHaveBeenCalledWith(TOOL_CALL_DISABLED_MESSAGE); }); - it('allows read-only tool calls (Read, Grep, Glob) on the child', async () => { + it('allows read-only tool calls (Read, Grep, Glob, ReadMediaFile) on the child', async () => { const svc = ix.get(ISessionBtwService); await svc.start(); + expect([...BTW_READONLY_TOOLS].toSorted()).toEqual(['Glob', 'Grep', 'Read', 'ReadMediaFile']); for (const name of BTW_READONLY_TOOLS) { const toolCall: ToolCall = { type: 'function', id: `call_${name}`, name, arguments: '{}' }; const decision = await executorEvents.fireBeforeExecute({