Skip to content
Open
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/print-background-steer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Add a print-mode background policy that lets `kimi -p` stay alive across background-task completions so the main agent can be steered into follow-up turns. Set `[background].print_background_mode = "steer"` to enable it.
22 changes: 14 additions & 8 deletions apps/kimi-code/src/cli/run-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ function runPromptTurn(
finish(new Error(`${event.code}: ${event.message}`));
return;
}
if (event.type === 'turn.started' && activeTurnId === undefined) {
if (event.type === 'turn.started') {
if (event.agentId !== PROMPT_MAIN_AGENT_ID) {
return;
}
Expand Down Expand Up @@ -599,7 +599,6 @@ function runPromptTurn(
case 'subagent.started':
case 'subagent.suspended':
case 'tool.list.updated':
case 'turn.started':
case 'turn.step.completed':
case 'warning':
return;
Expand All @@ -611,15 +610,22 @@ function runPromptTurn(
});

async function finishCompletedTurn(): Promise<void> {
// Flush the buffered assistant message before draining background tasks:
// in stream-json mode the final message is only emitted by finish(), so a
// long background wait would otherwise withhold the main turn's result
// until the drain settles.
// Flush the buffered assistant message before the end-of-turn policy
// runs: in stream-json mode the final message is only emitted by
// finish(), so a long drain/steer wait would otherwise withhold the main
// turn's result until the run exits.
outputWriter.flushAssistant();
try {
await session.waitForBackgroundTasksOnPrint();
const action = await session.handlePrintMainTurnCompleted();
if (action === 'continue') {
// Stay alive: a still-pending background task will, on completion,
// steer the main agent into a new turn whose events we keep mapping.
// Do not finish yet.
holdEventLoop();
return;
}
} catch (error) {
log.warn('waitForBackgroundTasksOnPrint failed', { error });
log.warn('handlePrintMainTurnCompleted failed', { error });
}
finish();
}
Expand Down
51 changes: 51 additions & 0 deletions apps/kimi-code/test/cli/run-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const mocks = vi.hoisted(() => {
waitForBackgroundTasksOnPrint: vi.fn(async () => {}),
getGoal: vi.fn(async () => ({ goal: null })),
getCronTasks: vi.fn(async () => ({ tasks: [] })),
handlePrintMainTurnCompleted: vi.fn(async (): Promise<'finish' | 'continue'> => 'finish'),
};

return {
Expand Down Expand Up @@ -752,6 +753,56 @@ describe('runPrompt', () => {
await runPromise;
});

it('follows a background-steered second main turn before finishing in steer mode', async () => {
// First end-of-turn: stay alive (a background task is still pending).
// Second end-of-turn: finish.
mocks.session.handlePrintMainTurnCompleted
.mockResolvedValueOnce('continue')
.mockResolvedValueOnce('finish');

mocks.session.prompt.mockImplementationOnce(async () => {
for (const handler of mocks.eventHandlers) {
handler(mocks.mainEvent({ type: 'turn.started', turnId: 10, origin: { kind: 'user' } }));
handler(mocks.mainEvent({ type: 'assistant.delta', turnId: 10, delta: 'first' }));
handler(mocks.mainEvent({ type: 'turn.ended', turnId: 10, reason: 'completed' }));
}
});

const stdout = writer();
const stderr = writer();
const runPromise = runPrompt(opts({ outputFormat: 'stream-json' }), '1.2.3-test', {
stdout,
stderr,
});

// The first turn's assistant message must be flushed and the end-of-turn
// policy consulted, while the run stays alive (action === 'continue').
await waitForAssertion(() => {
expect(mocks.session.handlePrintMainTurnCompleted).toHaveBeenCalledTimes(1);
expect(stdout.text()).toContain('{"role":"assistant","content":"first"}');
});

// Simulate a background-task completion steering the main agent into a new
// turn (the runtime does this via turn.steer; here we drive the events
// directly to verify the driver follows and finishes only after it).
for (const handler of mocks.eventHandlers) {
handler(
mocks.mainEvent({
type: 'turn.started',
turnId: 11,
origin: { kind: 'background_task' },
}),
);
handler(mocks.mainEvent({ type: 'assistant.delta', turnId: 11, delta: 'second' }));
handler(mocks.mainEvent({ type: 'turn.ended', turnId: 11, reason: 'completed' }));
}

await runPromise;

expect(mocks.session.handlePrintMainTurnCompleted).toHaveBeenCalledTimes(2);
expect(stdout.text()).toContain('{"role":"assistant","content":"second"}');
});

it('resumes a concrete session without a configured default model', async () => {
mocks.harnessGetConfig.mockResolvedValueOnce({ providers: {}, telemetry: true });
mocks.session.getStatus.mockResolvedValueOnce({ permission: 'manual', model: 'saved-model' });
Expand Down
8 changes: 5 additions & 3 deletions docs/en/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,14 @@ You can also switch models temporarily without touching the config file — by s
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `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`), setting this to `true` also makes the process wait for all background tasks to finish before exiting, so background subagents can complete their work |
| `print_wait_ceiling_s` | `integer` | `3600` | In print mode (`kimi -p`) with `keep_alive_on_exit = true`, the maximum number of seconds the process waits for background tasks to finish after the main agent's turn ends. Has no effect outside print mode or when `keep_alive_on_exit` is `false` |
| `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"` |
| `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 |

`keep_alive_on_exit` can be overridden by the `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` environment variable, which takes higher priority than `config.toml`.

In print mode (`kimi -p "<prompt>"`), Kimi Code runs a single non-interactive turn and exits as soon as the main agent finishes. If you launch background tasks (for example, concurrent subagents via `Agent(run_in_background=true)`) and need them to run to completion, set `keep_alive_on_exit = true`: the process then waits for every background task to reach a terminal state before exiting, bounded by `print_wait_ceiling_s`. Without it, the single turn ending tears background tasks down with the process.
In print mode (`kimi -p "<prompt>"`), Kimi Code by default runs a single non-interactive turn and exits as soon as the main agent finishes (`print_background_mode = "exit"`). If you launch background tasks (for example, concurrent subagents via `Agent(run_in_background=true)`, or a long command via `Bash(run_in_background=true)`) and need them to run to completion, set `print_background_mode` to `"drain"` (wait for them to finish, without feeding results back) or `"steer"` (feed each completion back to the main agent, starting a new turn so it can act on the result). `"steer"` is useful when the main agent should keep working based on the outcome of a long background task (e.g. training or evaluation); its total wall-clock is bounded by `print_wait_ceiling_s` and the number of extra turns by `print_max_turns`.

## `image`

Expand Down
1 change: 1 addition & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ This page documents the changes in each Kimi Code CLI release.
- Add a TUI preference to keep rapid multi-line pastes from submitting line by line when bracketed paste is unavailable. Set `disable_paste_burst = true` in `tui.toml` to turn it off.
- Keep subagent cards at a stable height and show a live status spinner with a compact two-row activity window.
- In `kimi -p` runs, wait for background subagents to finish before exiting when `background.keep_alive_on_exit` is enabled. Set `keep_alive_on_exit = true` to let concurrent background subagents complete.
- Add `background.print_background_mode` (`exit`/`drain`/`steer`) for `kimi -p`: in `steer` mode, a completing background task (including `Bash(run_in_background=true)`) behaves like a background subagent — it injects a synthetic user message that steers the main agent into a new turn so it can act on the result. Bounded by `print_wait_ceiling_s` and the new `print_max_turns`.

### Refactors

Expand Down
8 changes: 5 additions & 3 deletions docs/zh/configuration/config-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,14 @@ display_name = "Kimi for Coding (custom)"
| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `max_running_tasks` | `integer` | — | 同时运行的最大后台任务数 |
| `keep_alive_on_exit` | `boolean` | `false` | 会话关闭时是否保留仍在运行的后台任务。默认情况下,Kimi Code 会在进程退出前请求停止所有后台任务;只有希望任务在会话结束后继续运行时才设为 `true`。在 print 模式(`kimi -p`)下,设为 `true` 还会让进程在退出前等待所有后台任务跑完,使后台子代理得以完成工作 |
| `print_wait_ceiling_s` | `integer` | `3600` | 在 print 模式(`kimi -p`)且 `keep_alive_on_exit = true` 时,主 agent 的 turn 结束后进程等待后台任务完成的最长秒数。在非 print 模式或 `keep_alive_on_exit` 为 `false` 时无效 |
| `keep_alive_on_exit` | `boolean` | `false` | 会话关闭时是否保留仍在运行的后台任务。默认情况下,Kimi Code 会在进程退出前请求停止所有后台任务;只有希望任务在会话结束后继续运行时才设为 `true`。在 print 模式(`kimi -p`)下,本字段仅作为 `print_background_mode` 未设置时的兼容回退:`true` 等价于 `print_background_mode = "drain"` |
| `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 循环失控 |

`keep_alive_on_exit` 可被环境变量 `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` 覆盖,优先级高于配置文件。

在 print 模式(`kimi -p "<prompt>"`)下,Kimi Code 只跑一个非交互的单轮 turn,主 agent 一结束就退出。如果你启动了后台任务(例如通过 `Agent(run_in_background=true)` 并发子代理)并希望它们跑完,请设置 `keep_alive_on_exit = true`:进程会在退出前等待所有后台任务进入终态,最长不超过 `print_wait_ceiling_s`。否则,单轮 turn 结束时后台任务会随进程一起被清理
在 print 模式(`kimi -p "<prompt>"`)下,Kimi Code 默认只跑一个非交互的单轮 turn,主 agent 一结束就退出(`print_background_mode = "exit"`)。如果你启动了后台任务(例如通过 `Agent(run_in_background=true)` 并发子代理,或 `Bash(run_in_background=true)` 的长命令)并希望它们跑完,可将 `print_background_mode` 设为 `"drain"`(等任务结束再退出,结果不回馈)或 `"steer"`(任务结束后把结果 steer 给主 agent,触发新 turn 继续处理)。`"steer"` 适合让主 agent 依据后台长任务(如训练、评测)的结果继续做后续步骤;其总耗时受 `print_wait_ceiling_s` 限制、额外 turn 数受 `print_max_turns` 限制

## `image`

Expand Down
1 change: 1 addition & 0 deletions docs/zh/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ outline: 2
- TUI 新增一项偏好设置:当 bracketed paste 不可用时,避免快速多行粘贴被逐行提交。可在 `tui.toml` 中设置 `disable_paste_burst = true` 关闭该行为。
- 优化子 Agent 卡片,使其保持固定高度,并在紧凑的双行活动窗口内显示实时状态 spinner。
- `kimi -p` 运行时,若启用了 `background.keep_alive_on_exit`,退出前会等待后台子 Agent 完成。设置 `keep_alive_on_exit = true` 可让并发的后台子 Agent 执行完毕。
- `kimi -p` 新增 `background.print_background_mode`(`exit`/`drain`/`steer`):`steer` 模式下,后台任务(含 `Bash(run_in_background=true)`)完成时会像后台子 Agent 一样,以合成 user 消息 steer 主 Agent 进入新 turn,使其能依据后台结果继续工作;上限由 `print_wait_ceiling_s` 与新增的 `print_max_turns` 控制。

### 重构

Expand Down
2 changes: 2 additions & 0 deletions packages/agent-core/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ export const BackgroundConfigSchema = z.object({
keepAliveOnExit: z.boolean().optional(),
killGracePeriodMs: z.number().int().min(0).optional(),
printWaitCeilingS: z.number().int().min(1).optional(),
printBackgroundMode: z.enum(['exit', 'drain', 'steer']).optional(),
printMaxTurns: z.number().int().min(1).optional(),
});

export type BackgroundConfig = z.infer<typeof BackgroundConfigSchema>;
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core/src/rpc/core-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ export interface SessionAPI extends AgentAPIWithId {
generateAgentsMd: (payload: EmptyPayload) => void;
getSessionWarnings: (payload: EmptyPayload) => readonly SessionWarning[];
waitForBackgroundTasksOnPrint: (payload: EmptyPayload) => void;
handlePrintMainTurnCompleted: (payload: EmptyPayload) => 'finish' | 'continue';
addAdditionalDir: (payload: AddAdditionalDirPayload) => AddAdditionalDirResult;
}

Expand Down
4 changes: 4 additions & 0 deletions packages/agent-core/src/rpc/core-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,10 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
return this.sessionApi(sessionId).waitForBackgroundTasksOnPrint(payload);
}

handlePrintMainTurnCompleted({ sessionId, ...payload }: SessionScopedPayload<EmptyPayload>): Promise<'finish' | 'continue'> {
return this.sessionApi(sessionId).handlePrintMainTurnCompleted(payload);
}

addAdditionalDir({
sessionId,
...payload
Expand Down
Loading
Loading