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/bash-timeout-auto-background.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---

Move foreground Bash commands that hit their timeout to the background instead of killing them, so long-running commands survive the timeout and report back on completion. Set `bash_auto_background_on_timeout = false` under `[background]` in config.toml to restore the kill-on-timeout behavior.
5 changes: 5 additions & 0 deletions .changeset/taskoutput-discourage-blocking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Optimize the TaskOutput tool prompts to discourage blocking waits on background tasks.
1 change: 1 addition & 0 deletions docs/en/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ You can also switch models temporarily without touching the config file — by s
| --- | --- | --- | --- |
| `max_running_tasks` | `integer` | — | Maximum number of background tasks running concurrently |
| `keep_alive_on_exit` | `boolean` | `false` | Whether to keep still-running background tasks when the session closes. By default, Kimi Code requests that all background tasks stop before the process exits; set this to `true` only when you want tasks to outlive the session. In print mode (`kimi -p`), this is only a legacy fallback used when `print_background_mode` is unset: `true` is equivalent to `print_background_mode = "drain"` |
| `bash_auto_background_on_timeout` | `boolean` | `true` | When a foreground `Bash` command hits its timeout, move it to a background task instead of killing it — the agent is notified when it completes, and the backgrounded command is bounded by the 600s default background timeout. Set to `false` to kill timed-out foreground commands instead |
| `print_background_mode` | `"exit" \| "drain" \| "steer"` | `"exit"` | Print mode (`kimi -p`) only. Governs how pending background tasks are handled once the main agent's turn ends: `"exit"` exits immediately; `"drain"` waits for every background task to reach a terminal state before exiting (results are not fed back to the main agent); `"steer"` stays alive so a completing background task — like a background subagent — injects a synthetic user message that steers the main agent into a new turn, looping until a turn ends with no pending background tasks or a limit is hit. Takes precedence over the `keep_alive_on_exit` print fallback |
| `print_wait_ceiling_s` | `integer` | `3600` | In print mode (`kimi -p`), the wall-clock ceiling (seconds) for the wait/steer loop when `print_background_mode` is `"drain"` or `"steer"`. Has no effect outside print mode or when it is `"exit"` |
| `print_max_turns` | `integer` | `50` | In print mode (`kimi -p`) with `print_background_mode = "steer"`, the maximum number of new turns that may be triggered by background-task completions, to keep the steering loop bounded |
Expand Down
1 change: 1 addition & 0 deletions docs/zh/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ display_name = "Kimi for Coding (custom)"
| --- | --- | --- | --- |
| `max_running_tasks` | `integer` | — | 同时运行的最大后台任务数 |
| `keep_alive_on_exit` | `boolean` | `false` | 会话关闭时是否保留仍在运行的后台任务。默认情况下,Kimi Code 会在进程退出前请求停止所有后台任务;只有希望任务在会话结束后继续运行时才设为 `true`。在 print 模式(`kimi -p`)下,本字段仅作为 `print_background_mode` 未设置时的兼容回退:`true` 等价于 `print_background_mode = "drain"` |
| `bash_auto_background_on_timeout` | `boolean` | `true` | 前台 `Bash` 命令触及超时时间时,将其转为后台任务而不是直接终止:命令完成时 agent 会收到通知,转入后台的命令受 600s 默认后台超时约束。设为 `false` 则恢复超时即终止的行为 |
| `print_background_mode` | `"exit" \| "drain" \| "steer"` | `"exit"` | 仅 print 模式(`kimi -p`)生效,决定主 agent 的 turn 结束后如何处理未返回的后台任务:`"exit"` 立即退出;`"drain"` 退出前等待所有后台任务进入终态(结果不回馈给主 agent);`"steer"` 不退出,让后台任务完成时像后台子代理一样以合成 user 消息 steer 主 agent 进入新 turn,直到某 turn 结束时无未决后台任务或触及上限。设置后优先级高于 `keep_alive_on_exit` 的 print 回退 |
| `print_wait_ceiling_s` | `integer` | `3600` | print 模式(`kimi -p`)下,`print_background_mode` 为 `"drain"` 或 `"steer"` 时,等待/steer 循环的墙钟上限(秒)。在非 print 模式或 `"exit"` 时无效 |
| `print_max_turns` | `integer` | `50` | print 模式(`kimi -p`)且 `print_background_mode = "steer"` 时,允许由后台任务完成触发的新 turn 的最大数量,防止 steer 循环失控 |
Expand Down
17 changes: 17 additions & 0 deletions packages/agent-core-v2/src/agent/task/configSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import { z } from 'zod';

import type { IConfigService } from '#/app/config/config';
import { registerConfigSection } from '#/app/config/configSectionContributions';

export const TASK_SECTION = 'task';
Expand All @@ -18,11 +19,27 @@ export const LEGACY_BACKGROUND_SECTION = 'background';
export const AgentTaskConfigSchema = z.object({
maxRunningTasks: z.number().int().min(1).optional(),
keepAliveOnExit: z.boolean().optional(),
/**
* When a foreground Bash command times out, move it to the background
* instead of killing it. Defaults to true when unset.
*/
bashAutoBackgroundOnTimeout: z.boolean().optional(),
killGracePeriodMs: z.number().int().min(0).optional(),
printWaitCeilingS: z.number().int().min(1).optional(),
});

export type AgentTaskConfig = z.infer<typeof AgentTaskConfigSchema>;

/**
* Read the effective task config, falling back to the legacy `[background]`
* section when `[task]` is unset.
*/
export function resolveAgentTaskConfig(config: IConfigService): AgentTaskConfig | undefined {
return (
config.get<AgentTaskConfig | undefined>(TASK_SECTION) ??
config.get<AgentTaskConfig | undefined>(LEGACY_BACKGROUND_SECTION)
);
}

registerConfigSection(TASK_SECTION, AgentTaskConfigSchema);
registerConfigSection(LEGACY_BACKGROUND_SECTION, AgentTaskConfigSchema);
8 changes: 7 additions & 1 deletion packages/agent-core-v2/src/agent/task/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,17 @@ export interface RegisterAgentTaskOptions {
readonly timeoutMs?: number;
/** Deadline to apply if a foreground task is detached. `0` and `undefined` do not arm a timer. */
readonly detachTimeoutMs?: number;
/**
* When true, a foreground task whose deadline fires is detached to the
* background (re-armed to `detachTimeoutMs`) instead of being killed.
* Only meaningful for non-detached registrations.
*/
readonly autoBackgroundOnTimeout?: boolean;
/** Foreground caller signal. Ignored for tasks created already detached. */
readonly signal?: AbortSignal;
}

export type ForegroundTaskReleaseReason = 'detached' | 'terminal';
export type ForegroundTaskReleaseReason = 'detached' | 'timeout_detached' | 'terminal';

/**
* Options for tracking a TaskHandle with the Agent task service.
Expand Down
76 changes: 45 additions & 31 deletions packages/agent-core-v2/src/agent/task/taskService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import {
type IAgentTaskEntry,
type RegisterAgentTaskOptions,
} from './task';
import { LEGACY_BACKGROUND_SECTION, TASK_SECTION, type AgentTaskConfig } from './configSection';
import { resolveAgentTaskConfig } from './configSection';
import { AgentTaskPersistence } from './persist';
import { TaskModel, taskStarted, taskTerminated } from './taskOps';
import { formatTaskList } from '#/agent/task/tools/task-list';
Expand Down Expand Up @@ -304,6 +304,7 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
detached,
timeoutMs,
detachTimeoutMs: options.detachTimeoutMs,
autoBackgroundOnTimeout: options.autoBackgroundOnTimeout,
signal: detached ? undefined : options.signal,
};
this.assertCanRegister(detached);
Expand Down Expand Up @@ -335,13 +336,7 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
this.ghosts.delete(entry.taskId);

if (timeoutMs !== undefined && timeoutMs > 0) {
entry.timeoutHandle = setTimeout(() => {
void this.terminateWithGrace(entry, {
abortReason: 'Timed out',
finalStatus: 'timed_out',
});
}, timeoutMs);
entry.timeoutHandle.unref?.();
this.armManagerTimeout(entry, timeoutMs);
}

entry.lifecyclePromise = Promise.resolve()
Expand Down Expand Up @@ -417,13 +412,7 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
this.ghosts.delete(taskId);

if (timeoutMs !== undefined && timeoutMs > 0) {
entry.timeoutHandle = setTimeout(() => {
void this.terminateWithGrace(entry, {
abortReason: 'Timed out',
finalStatus: 'timed_out',
});
}, timeoutMs);
entry.timeoutHandle.unref?.();
this.armManagerTimeout(entry, timeoutMs);
}

const outputSub = handle.onDidOutput((chunk) => {
Expand Down Expand Up @@ -578,6 +567,15 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
detach(taskId: string): AgentTaskInfo | undefined {
const entry = this.tasks.get(taskId);
if (entry === undefined) return this.ghosts.get(taskId);
return this.detachEntry(entry, false);
}

/**
* Move a foreground task to the background, releasing its tool-call waiter.
* `viaTimeout` marks an automatic detach triggered by the task deadline (vs.
* an explicit user/RPC detach) so the waiter can word its result.
*/
private detachEntry(entry: ManagedTask, viaTimeout: boolean): AgentTaskInfo | undefined {
if (TERMINAL_STATUSES.has(entry.status)) return this.toInfo(entry);

const foregroundRelease = entry.foregroundRelease;
Expand All @@ -598,7 +596,7 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
this.startOutputPersist(entry);
void this.persistLive(entry);
this.recordTaskStarted(this.toInfo(entry));
foregroundRelease.resolve('detached');
foregroundRelease.resolve(viaTimeout ? 'timeout_detached' : 'detached');
return this.toInfo(entry);
}

Expand All @@ -611,16 +609,39 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
entry.timeoutHandle = undefined;
}
if (timeoutMs > 0) {
entry.timeoutHandle = setTimeout(() => {
void this.terminateWithGrace(entry, {
abortReason: 'Timed out',
finalStatus: 'timed_out',
});
}, timeoutMs);
entry.timeoutHandle.unref?.();
this.armManagerTimeout(entry, timeoutMs);
}
}

/**
* Arm a manager-owned deadline (`timeoutMs` / `detachTimeoutMs`). A
* foreground task opted into auto-background survives its first deadline by
* detaching to the background — `detachEntry` re-arms `detachTimeoutMs` —
* instead of being killed; every other deadline terminates with grace.
*/
private armManagerTimeout(entry: ManagedTask, timeoutMs: number): void {
entry.timeoutHandle = setTimeout(() => {
entry.timeoutHandle = undefined;
if (this.canAutoBackgroundOnTimeout(entry)) {
this.detachEntry(entry, true);
return;
}
void this.terminateWithGrace(entry, {
abortReason: 'Timed out',
finalStatus: 'timed_out',
});
}, timeoutMs);
entry.timeoutHandle.unref?.();
}

/**
* Foreground tasks opted into auto-background survive their first deadline
* by detaching to the background instead of being killed.
*/
private canAutoBackgroundOnTimeout(entry: ManagedTask): boolean {
return entry.options.autoBackgroundOnTimeout === true && !this.isDetached(entry);
}

async stop(taskId: string, reason?: string): Promise<AgentTaskInfo | undefined> {
const entry = this.tasks.get(taskId);
if (entry === undefined) return undefined;
Expand Down Expand Up @@ -791,20 +812,13 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
}

private assertCanRegister(detached: boolean): void {
const maxRunningTasks = this.taskConfig()?.maxRunningTasks;
const maxRunningTasks = resolveAgentTaskConfig(this.config)?.maxRunningTasks;
if (maxRunningTasks === undefined) return;
if (!detached) return;
if (this.activeTaskCount() < maxRunningTasks) return;
throw new Error('Too many background tasks are already running.');
}

private taskConfig(): AgentTaskConfig | undefined {
return (
this.config.get<AgentTaskConfig | undefined>(TASK_SECTION) ??
this.config.get<AgentTaskConfig | undefined>(LEGACY_BACKGROUND_SECTION)
);
}

private activeTaskCount(): number {
let count = 0;
for (const entry of this.tasks.values()) {
Expand Down
9 changes: 5 additions & 4 deletions packages/agent-core-v2/src/agent/task/tools/task-output.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
Retrieve output from a running or completed background task.
Retrieve a snapshot of a running or completed background task.

Use this after `Bash(run_in_background=true)` or `Agent(run_in_background=true)` when you need to inspect progress or explicitly wait for completion.
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.

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.
- 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.
- By default this tool is non-blocking and returns a current status/output snapshot.
- Use block=true only when you intentionally want to wait for completion or timeout.
- 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
9 changes: 8 additions & 1 deletion packages/agent-core-v2/src/agent/task/tools/task-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ export const TaskOutputInputSchema = z.object({
block: z
.boolean()
.default(false)
.describe('Whether to wait for the task to finish before returning.')
.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()
Expand Down Expand Up @@ -147,6 +149,11 @@ export class TaskOutputTool implements BuiltinTool<TaskOutputInput> {
fullOutputTool:
output.fullOutputAvailable && output.outputPath !== undefined ? 'Read' : undefined,
fullOutputHint: fullOutputHint(output),
// Nudge at the exact point of misuse: a blocking wait that timed out.
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ 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` for a non-blocking status/output snapshot, and only set `block=true` when you explicitly want to wait for completion. 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 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.

**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.
- The tool call will return after the command is finished. You shall not use this tool to execute an interactive command or a command that may run forever. For possibly long-running foreground commands, set the `timeout` argument in seconds. Foreground commands default to {{ DEFAULT_TIMEOUT_S }}s and allow up to {{ MAX_TIMEOUT_S }}s.
- The tool call will return after the command is finished. You shall not use this tool to execute an interactive command or a command that may run forever. For possibly long-running foreground commands, set the `timeout` argument in seconds. Foreground commands default to {{ DEFAULT_TIMEOUT_S }}s and allow up to {{ MAX_TIMEOUT_S }}s. When a foreground command hits its timeout it is moved to the background instead of being killed, and you will be automatically notified when it completes.
- Avoid using `..` to access files or directories outside of the working directory.
- Avoid modifying files outside of the working directory unless explicitly instructed to do so.
- Never run commands that require superuser privileges unless explicitly instructed to do so.
Expand Down
Loading
Loading