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
8 changes: 8 additions & 0 deletions .changeset/foreground-task-detach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kimi-code-sdk": patch
"@moonshot-ai/protocol": patch
"@moonshot-ai/kimi-code": patch
---

Allow foreground shell and subagent tasks to be detached into background tasks.
1 change: 0 additions & 1 deletion docs/zh/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,4 +501,3 @@ outline: 2
### 其他

- 当未配置模型时,`/model` 和欢迎面板现在会引导用户使用 `/login`(针对 Kimi)和 `/connect`(针对其他供应商)。

52 changes: 15 additions & 37 deletions packages/agent-core/src/agent/background/agent-task.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { sleep } from '@antfu/utils';

import { errorMessage, isAbortError } from '../../loop/errors';
import {
type BackgroundTask,
type BackgroundTaskInfoBase,
type BackgroundTaskSink,
} from './task';
import type { SessionSubagentHost, SubagentHandle } from '../../session/subagent-host';

export interface AgentBackgroundTaskInfo extends BackgroundTaskInfoBase {
readonly kind: 'agent';
Expand All @@ -15,63 +14,38 @@ export interface AgentBackgroundTaskInfo extends BackgroundTaskInfoBase {
readonly subagentType?: string;
}

export interface AgentBackgroundTaskOptions {
readonly timeoutMs?: number;
readonly abort?: () => void;
readonly agentId?: string;
readonly subagentType?: string;
}

export class AgentBackgroundTask implements BackgroundTask {
readonly kind = 'agent' as const;
readonly idPrefix: string = 'agent';
readonly timeoutMs?: number;
readonly agentId?: string;
readonly subagentType?: string;
private readonly abort?: () => void;
readonly agentId: string;
readonly subagentType: string;

constructor(
private readonly completion: Promise<{ result: string }>,
private readonly handle: SubagentHandle,
readonly description: string,
options: AgentBackgroundTaskOptions = {},
private readonly subagentHost: Pick<SessionSubagentHost, 'markActiveChildDetached'>,
private readonly abortController: AbortController,
) {
this.timeoutMs = options.timeoutMs;
this.abort = options.abort;
this.agentId = options.agentId;
this.subagentType = options.subagentType;
this.agentId = handle.agentId;
this.subagentType = handle.profileName;
}

async start(sink: BackgroundTaskSink): Promise<void> {
const requestAbort = (): void => {
this.abort?.();
this.abortController.abort(sink.signal.reason);
};
if (sink.signal.aborted) {
requestAbort();
} else {
sink.signal.addEventListener('abort', requestAbort, { once: true });
}

const deadlineTimeout: unique symbol = Symbol('background-agent-deadline');
const raceInputs: Array<Promise<{ result: string } | typeof deadlineTimeout>> = [
this.completion,
];
const timeoutMs = this.timeoutMs;

if (timeoutMs !== undefined && timeoutMs > 0) {
raceInputs.push(sleep(timeoutMs).then(() => deadlineTimeout));
}

try {
const outcome = await Promise.race(raceInputs);
if (outcome === deadlineTimeout) {
this.abort?.();
await sink.settle({ status: 'timed_out' });
return;
}
const outcome = await this.handle.completion;
sink.appendOutput(outcome.result);
await sink.settle({ status: 'completed' });
} catch (error: unknown) {
if (sink.signal.aborted && isAbortError(error)) {
if (sink.signal.aborted && (isAbortError(error) || error === sink.signal.reason)) {
await sink.settle({ status: 'killed' });
return;
}
Expand All @@ -81,6 +55,10 @@ export class AgentBackgroundTask implements BackgroundTask {
}
}

onDetach(): void {
this.subagentHost.markActiveChildDetached(this.agentId);
}

toInfo(base: BackgroundTaskInfoBase): AgentBackgroundTaskInfo {
return {
...base,
Expand Down
Loading
Loading