Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/task-output-nonblocking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
Comment thread
RealKai42 marked this conversation as resolved.
---

Remove the blocking `block`/`timeout` wait from the TaskOutput tool so checking a background task can no longer stall the conversation; it now always returns an immediate snapshot, and completion still arrives via automatic notification.
2 changes: 1 addition & 1 deletion docs/en/reference/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Background task tools manage tasks started via `Bash`, `Agent`, or `AskUserQuest

**`TaskList`** returns the list of background tasks. Optional parameters: `active_only` (defaults to true; lists only running tasks) and `limit` (defaults to 20; range 1–100).

**`TaskOutput`** returns the status and output of a task given its `task_id`. The inline preview includes at most the most recent 32 KB of content; the full log is saved to disk, and the tool also returns an `output_path` with a suggestion to use `Read` for paginated access. Optional `block` (defaults to false) and `timeout` (seconds to wait; defaults to 30; range 0–3600) parameters allow waiting for the task to complete before returning.
**`TaskOutput`** returns the status and output of a task given its `task_id`. The inline preview includes at most the most recent 32 KB of content; the full log is saved to disk, and the tool also returns an `output_path` with a suggestion to use `Read` for paginated access. The call is always non-blocking — it returns the current snapshot immediately, and task completion is delivered via automatic notification.

**`TaskStop`** accepts a `task_id` and optional `reason` (defaults to `Stopped by TaskStop`). Safe to call on tasks that are already in a terminal state.

Expand Down
2 changes: 1 addition & 1 deletion docs/zh/reference/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ Plan 模式是一种受约束的工作状态:进入后 `Write` 与 `Edit` 只

**`TaskList`** 返回后台任务列表。可选参数 `active_only`(默认 true,仅列出运行中的任务)和 `limit`(默认 20,取值范围 1–100)。

**`TaskOutput`** 根据 `task_id` 返回任务状态与输出。内联预览最多包含最近 32 KB 的内容;完整日志保存在磁盘上,工具会一并返回 `output_path` 并提示通过 `Read` 分页读取。可选 `block`(默认 false)和 `timeout`(等待秒数,默认 30,取值范围 0–3600)参数可用于等待任务完成后再返回
**`TaskOutput`** 根据 `task_id` 返回任务状态与输出。内联预览最多包含最近 32 KB 的内容;完整日志保存在磁盘上,工具会一并返回 `output_path` 并提示通过 `Read` 分页读取。该调用始终是非阻塞的——立即返回当前快照,任务完成会通过自动通知送达

**`TaskStop`** 接受 `task_id` 和可选的 `reason`(默认 `Stopped by TaskStop`)。对已处于终止状态的任务也能安全调用。

Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/agent/task/taskService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ const NOTIFICATION_FALLBACK_PREVIEW_BYTES = 3_000;
const ACTIVE_BACKGROUND_TASK_INJECTION_VARIANT = 'background_task_status';
const ACTIVE_BACKGROUND_TASK_GUIDANCE = [
'The conversation was compacted, so the earlier messages that started these background tasks are gone — but the tasks are still running from before.',
'Do not start duplicates. Use TaskOutput to fetch a task’s result, TaskList to list them, and TaskStop to cancel one.',
'Do not start duplicates. Use TaskList to list them, TaskOutput for a non-blocking status/output snapshot, and TaskStop to cancel one — completion arrives via automatic notification.',
].join(' ');

export function isAgentTaskTerminal(status: AgentTaskStatus): boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
When `run_in_background=true`, the subagent runs detached from this turn. The completion arrives in a later turn as a synthetic user-role message containing its result — you do not need to poll, sleep, or check on its progress. Continue with other work or respond to the user. Never fabricate or predict what the result will say.

Default to a foreground subagent (omit `run_in_background`) when your next step needs its result — foreground hands the result straight back. Reach for `run_in_background=true` only when you have other work to do while it runs and do not need its result to proceed. Never launch in the background and then immediately wait on it (with `TaskOutput block=true`, sleeping, or otherwise): that just blocks the turn for no benefit — run it in the foreground instead.
Default to a foreground subagent (omit `run_in_background`) when your next step needs its result — foreground hands the result straight back. Reach for `run_in_background=true` only when you have other work to do while it runs and do not need its result to proceed. Never launch in the background and then immediately wait on it (by polling `TaskOutput`, sleeping, or otherwise): that just blocks the turn for no benefit — run it in the foreground instead.
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/agent/tools/os/bash/bash.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The dedicated tools render in the per-tool permission UI and keep raw stdout out
**Output:**
The stdout and stderr will be combined and returned as a string. The output may be truncated if it is too long. If the command exits non-zero, the output ends with a `Command failed with exit code: N` line; a command killed by its timeout or interrupted by the user ends with its own message instead.

If `run_in_background=true`, the command will be started as a background task and this tool will return a task ID instead of waiting for command completion. When doing that, you must provide a short `description`. Background commands default to a ${DEFAULT_BACKGROUND_TIMEOUT_S}s timeout and `timeout` is capped at ${MAX_BACKGROUND_TIMEOUT_S}s; set `disable_timeout=true` only when the task should run without a timeout. You will be automatically notified when the task completes. After starting one, default to returning control to the user instead of immediately waiting on it. Use `TaskOutput` only for a non-blocking status/output snapshot — do not set `block=true` to wait for a task you just launched, since its completion arrives automatically; reserve `block=true` for when the user explicitly asked you to wait. Use `TaskStop` only if the task must be cancelled. If a human user wants to inspect background tasks themselves, point them to the `/tasks` command, which opens an interactive panel; it has no subcommands.
If `run_in_background=true`, the command will be started as a background task and this tool will return a task ID instead of waiting for command completion. When doing that, you must provide a short `description`. Background commands default to a ${DEFAULT_BACKGROUND_TIMEOUT_S}s timeout and `timeout` is capped at ${MAX_BACKGROUND_TIMEOUT_S}s; set `disable_timeout=true` only when the task should run without a timeout. You will be automatically notified when the task completes. After starting one, default to returning control to the user instead of immediately waiting on it. Use `TaskOutput` only for a non-blocking status/output snapshot — do not wait on a task you just launched, since its completion arrives automatically. Use `TaskStop` only if the task must be cancelled. If a human user wants to inspect background tasks themselves, point them to the `/tasks` command, which opens an interactive panel; it has no subcommands.

**Guidelines for safety and security:**
- Each shell tool call will be executed in a fresh shell environment. The shell variables, current working directory changes, and the shell history is not preserved between calls. To run a command in a particular directory, pass the `cwd` argument (or use absolute paths) rather than relying on a `cd` from an earlier call.
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/agent/tools/os/bash/bashTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ export class BashTool implements IBashTool {
if (!output.fullOutputAvailable || output.outputPath === undefined) return result;

const taskOutputHint = this.allowBackground()
? `, or TaskOutput(task_id="${taskId}", block=false)`
? `, or TaskOutput(task_id="${taskId}")`
: '';
const reference =
`\n\n[Full output saved]\n` +
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
Retrieve a snapshot of a running or completed background task.

Use this after `Bash(run_in_background=true)` or `Agent(run_in_background=true)` to check progress, or to read the output of a task that has already completed.
Use this after `Bash(run_in_background=true)`, `Agent(run_in_background=true)`, or `AskUserQuestion(background=true)` to check progress, or to read the output of a task that has already completed.

Guidelines:
- Prefer relying on automatic completion notifications. Use this tool only when you need task output before the automatic notification arrives.
- By default this tool is non-blocking and returns a current status/output snapshot — that is the normal way to use it.
- This tool is always non-blocking: it returns the current status/output snapshot immediately and never waits for the task to finish.
- Do not use TaskOutput to wait for a result you need before continuing — if your next step depends on the task's result, run that task in the foreground instead. TaskOutput is for a deliberate progress check you will act on without blocking, not a way to sit and wait for a background task you just launched.
- Use block=true only when the user explicitly asked you to wait for the task. Never block on a task you launched in the current turn — if you need its result right away, it should have been a foreground call.
- If a block=true call returns `retrieval_status: timeout` (the task is still running), do not block on the same task again. Continue with other work or hand back to the user — the completion notification arrives on its own.
- This tool returns structured task metadata, a fixed-size output preview, and an output_path for the full log.
- For a terminal task, the metadata also explains why it ended. A shell command that runs to completion reports `status: completed` on a zero exit, or `status: failed` with its non-zero `exit_code` — judge that failure from the `exit_code`, because a plain command failure carries no `stop_reason` and no `terminal_reason`. `terminal_reason` is a categorical label emitted only when the end is not an ordinary exit: `timed_out` when the deadline aborted it, `stopped` when it was explicitly stopped, or `failed` when it errored without producing an exit code; the `stopped` and `failed` cases also carry a human-readable `stop_reason`. A task that finished on its own with a clean exit carries neither `stop_reason` nor `terminal_reason`.
- The full, never-truncated log is always available at output_path; use the `Read` tool with that path to page through it, whether or not the preview was truncated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,6 @@ import { type AgentTool } from '#/tool/toolContract';

export const TaskOutputInputSchema = z.object({
task_id: z.string().describe('The background task ID to inspect.'),
block: z
.boolean()
.default(false)
.describe(
'Whether to wait for the task to finish before returning. Discouraged — background tasks notify automatically on completion; use only when the user explicitly asked you to wait.',
)
.optional(),
timeout: z
.number()
.int()
.min(0)
.max(3600)
.default(30)
.describe('Maximum number of seconds to wait when block=true.')
.optional(),
});

export type TaskOutputInput = z.infer<typeof TaskOutputInputSchema>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,8 @@ const OUTPUT_PREVIEW_BYTES = 32 * 1024;
const PAGING_HINT_LINES = 300;


function retrievalStatus(
status: AgentTaskStatus,
block: boolean | undefined,
): 'success' | 'timeout' | 'not_ready' {
if (TERMINAL_STATUSES.has(status)) return 'success';
return block ? 'timeout' : 'not_ready';
function retrievalStatus(status: AgentTaskStatus): 'success' | 'not_ready' {
return TERMINAL_STATUSES.has(status) ? 'success' : 'not_ready';
}

function terminalReason(info: AgentTaskInfo): 'timed_out' | 'stopped' | 'failed' | undefined {
Expand Down Expand Up @@ -85,23 +81,11 @@ export class TaskOutputTool implements ITaskOutputTool {
description: `Reading output of task ${args.task_id}`,
approvalRule: this.name,
matchesRule: (ruleArgs) => matchesGlobRuleSubject(ruleArgs, args.task_id),
execute: ({ signal }) => this.execute(args, signal),
execute: () => this.execute(args),
};
}

private async execute(
args: TaskOutputInput,
signal: AbortSignal,
): Promise<ExecutableToolResult> {
const info = this.tasks.getTask(args.task_id);
if (!info) {
return { isError: true, output: `Task not found: ${args.task_id}` };
}

if (args.block && !TERMINAL_STATUSES.has(info.status)) {
await this.tasks.wait(args.task_id, (args.timeout ?? 30) * 1000, signal);
}

private async execute(args: TaskOutputInput): Promise<ExecutableToolResult> {
const current = this.tasks.getTask(args.task_id);
if (!current) {
return { isError: true, output: `Task not found: ${args.task_id}` };
Expand All @@ -111,7 +95,7 @@ export class TaskOutputTool implements ITaskOutputTool {

const lines = [
formatPlainObject({
retrievalStatus: retrievalStatus(current.status, args.block),
retrievalStatus: retrievalStatus(current.status),
...current,
outputPath: output.outputPath,
terminalReason: terminalReason(current),
Expand All @@ -122,10 +106,6 @@ export class TaskOutputTool implements ITaskOutputTool {
fullOutputTool:
output.fullOutputAvailable && output.outputPath !== undefined ? 'Read' : undefined,
fullOutputHint: fullOutputHint(output),
nextStep:
args.block === true && !TERMINAL_STATUSES.has(current.status)
? 'The task is still running after waiting. Do not block on it again — continue with other work or hand back to the user; you will be notified automatically when it completes.'
: undefined,
}),
'',
];
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-core-v2/test/agent/loop/loop.test.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/agent-core-v2/test/agent/task/taskService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ describe('AgentTaskService', () => {
const reminder = await backgroundTaskReminder();
expect(reminder).toContain('The conversation was compacted');
expect(reminder).toContain(
'gone — but the tasks are still running from before. Do not start duplicates. Use TaskOutput to fetch a task’s result',
'gone — but the tasks are still running from before. Do not start duplicates. Use TaskList to list them, TaskOutput for a non-blocking status/output snapshot',
);
expect(reminder).toContain('active_background_tasks: 1');
expect(reminder).toContain(taskId);
Expand Down
Loading
Loading