mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix: align v2 task observable behavior
This commit is contained in:
parent
157704b82a
commit
f6aa8a8e7b
5 changed files with 69 additions and 40 deletions
|
|
@ -68,7 +68,7 @@ type AgentTaskNotification = Record<string, unknown> & {
|
|||
readonly id: string;
|
||||
readonly category: 'task';
|
||||
readonly type: string;
|
||||
readonly source_kind: 'task';
|
||||
readonly source_kind: 'background_task';
|
||||
readonly source_id: string;
|
||||
readonly agent_id?: string | undefined;
|
||||
readonly title: string;
|
||||
|
|
@ -658,7 +658,7 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
|
|||
if (maxRunningTasks === undefined) return;
|
||||
if (!detached) return;
|
||||
if (this.activeTaskCount() < maxRunningTasks) return;
|
||||
throw new Error('Too many detached tasks are already running.');
|
||||
throw new Error('Too many background tasks are already running.');
|
||||
}
|
||||
|
||||
private taskConfig(): AgentTaskConfig | undefined {
|
||||
|
|
@ -815,16 +815,16 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
|
|||
|
||||
private recordTaskStarted(info: AgentTaskInfo): void {
|
||||
this.wire.dispatch(taskStarted({ info }));
|
||||
this.telemetry.track('task_created', {
|
||||
this.telemetry.track('background_task_created', {
|
||||
kind: info.kind === 'process' ? 'bash' : info.kind,
|
||||
});
|
||||
}
|
||||
|
||||
private recordTaskTerminated(info: AgentTaskInfo): void {
|
||||
this.wire.dispatch(taskTerminated({ info }));
|
||||
this.telemetry.track('task_completed', {
|
||||
this.telemetry.track('background_task_completed', {
|
||||
kind: info.kind,
|
||||
duration: info.endedAt !== null ? info.endedAt - info.startedAt : null,
|
||||
duration_ms: info.endedAt !== null ? info.endedAt - info.startedAt : null,
|
||||
status: info.status,
|
||||
});
|
||||
}
|
||||
|
|
@ -886,10 +886,10 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
|
|||
id: origin.notificationId,
|
||||
category: 'task',
|
||||
type: `task.${info.status}`,
|
||||
source_kind: 'task',
|
||||
source_kind: 'background_task',
|
||||
source_id: info.taskId,
|
||||
agent_id: info.kind === 'agent' ? info.agentId : undefined,
|
||||
title: `Task ${info.kind} ${info.status}`,
|
||||
title: `Background ${info.kind} ${info.status}`,
|
||||
severity: info.status === 'completed' ? 'info' : 'warning',
|
||||
body: buildAgentTaskNotificationBody(info),
|
||||
children: agentTaskNotificationChildren(output),
|
||||
|
|
@ -922,7 +922,7 @@ export class AgentTaskService extends Disposable implements IAgentTaskService {
|
|||
);
|
||||
}
|
||||
|
||||
private markDeliveredNotification(origin: TaskOrigin): void {
|
||||
private markDeliveredNotification(origin: TaskNotificationOrigin): void {
|
||||
this.deliveredNotificationKeys.add(notificationKey(origin));
|
||||
}
|
||||
|
||||
|
|
@ -1042,22 +1042,24 @@ function newerRestoredTask(
|
|||
return loaded;
|
||||
}
|
||||
|
||||
function isTaskOrigin(origin: unknown): origin is TaskOrigin {
|
||||
type TaskNotificationOrigin = Pick<TaskOrigin, 'taskId' | 'status' | 'notificationId'>;
|
||||
|
||||
function isTaskOrigin(origin: unknown): origin is TaskNotificationOrigin {
|
||||
if (typeof origin !== 'object' || origin === null) return false;
|
||||
const value = origin as Record<string, unknown>;
|
||||
return (
|
||||
value['kind'] === 'task' &&
|
||||
(value['kind'] === 'background_task' || value['kind'] === 'task') &&
|
||||
typeof value['taskId'] === 'string' &&
|
||||
typeof value['status'] === 'string' &&
|
||||
typeof value['notificationId'] === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
function notificationKey(origin: TaskOrigin): string {
|
||||
function notificationKey(origin: TaskNotificationOrigin): string {
|
||||
return `${origin.taskId}\0${origin.status}\0${origin.notificationId}`;
|
||||
}
|
||||
|
||||
function taskOriginsFromRecord(record: WireRecord): readonly TaskOrigin[] {
|
||||
function taskOriginsFromRecord(record: WireRecord): readonly TaskNotificationOrigin[] {
|
||||
const raw = record as {
|
||||
readonly type: string;
|
||||
readonly message?: unknown;
|
||||
|
|
@ -1072,7 +1074,7 @@ function taskOriginsFromRecord(record: WireRecord): readonly TaskOrigin[] {
|
|||
return [];
|
||||
}
|
||||
|
||||
function taskOriginFromMessage(message: unknown): readonly TaskOrigin[] {
|
||||
function taskOriginFromMessage(message: unknown): readonly TaskNotificationOrigin[] {
|
||||
if (typeof message !== 'object' || message === null) return [];
|
||||
const origin = (message as { readonly origin?: unknown }).origin;
|
||||
return isTaskOrigin(origin) ? [origin] : [];
|
||||
|
|
@ -1094,7 +1096,7 @@ function buildAgentTaskNotificationBody(info: AgentTaskInfo): string {
|
|||
const recovery = [
|
||||
'',
|
||||
`To recover or continue this subagent, call Agent(resume="${agentId}", prompt="Pick up where you left off; redo the last tool call if its result was never observed.").`,
|
||||
`Use agent_id ("${agentId}"), NOT source_id / task_id ("${info.taskId}") because the two look alike but only agent_id is accepted by the resume parameter.`,
|
||||
`Use agent_id ("${agentId}"), NOT source_id / task_id ("${info.taskId}") — the two look alike but only agent_id is accepted by the resume parameter.`,
|
||||
'Add run_in_background=true to keep it backgrounded, or omit it to take the result inline in the current turn.',
|
||||
'The subagent retains its full prior context across the restart, but any in-flight tool call lost its result and may need to be redone.',
|
||||
].join('\n');
|
||||
|
|
|
|||
|
|
@ -523,7 +523,7 @@ describe('AgentTaskService', () => {
|
|||
|
||||
expect(() => {
|
||||
manager.registerTask(agentTask(new Promise(() => {}), 'second background'));
|
||||
}).toThrow('Too many detached tasks are already running.');
|
||||
}).toThrow('Too many background tasks are already running.');
|
||||
});
|
||||
|
||||
it('does not count foreground tasks detached later against the detached task limit', () => {
|
||||
|
|
@ -539,7 +539,7 @@ describe('AgentTaskService', () => {
|
|||
|
||||
expect(() => {
|
||||
manager.registerTask(agentTask(new Promise(() => {}), 'second background'));
|
||||
}).toThrow('Too many detached tasks are already running.');
|
||||
}).toThrow('Too many background tasks are already running.');
|
||||
});
|
||||
|
||||
it('lists active tasks by default', () => {
|
||||
|
|
@ -603,10 +603,10 @@ describe('AgentTaskService', () => {
|
|||
|
||||
expect(() => {
|
||||
registerProcess(manager, pendingProcess().proc, 'sleep 60', 'second task');
|
||||
}).toThrow('Too many detached tasks are already running.');
|
||||
}).toThrow('Too many background tasks are already running.');
|
||||
expect(() => {
|
||||
manager.registerTask(agentTask(new Promise(() => {}), 'agent task'));
|
||||
}).toThrow('Too many detached tasks are already running.');
|
||||
}).toThrow('Too many background tasks are already running.');
|
||||
});
|
||||
|
||||
it('captures process output', async () => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { join } from 'pathe';
|
|||
import type { IProcess } from '#/session/process/processRunner';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { IAgentTaskService } from '#/agent/task/task';
|
||||
import { TERMINAL_STATUSES } from '#/agent/task/types';
|
||||
import { ProcessTask } from '#/os/backends/node-local/tools/process-task';
|
||||
import { createAgentTaskPersistence, type TaskServiceTestManager } from './stubs';
|
||||
import { taskServices, createTestAgent, homeDirServices, type TestAgentContext } from '../harness';
|
||||
|
|
@ -49,6 +50,31 @@ async function waitForOutput(
|
|||
throw new Error(`Timed out waiting for output: ${expected}`);
|
||||
}
|
||||
|
||||
async function waitForTaskNotifications(
|
||||
ctx: TestAgentContext,
|
||||
manager: TaskServiceTestManager,
|
||||
): Promise<void> {
|
||||
const tasks = manager.list(false).filter(
|
||||
(task) =>
|
||||
TERMINAL_STATUSES.has(task.status) &&
|
||||
task.detached !== false &&
|
||||
task.terminalNotificationSuppressed !== true,
|
||||
);
|
||||
if (tasks.length === 0) return;
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const origins = ctx.context.get().map((message) => message.origin);
|
||||
for (const task of tasks) {
|
||||
expect(origins).toContainEqual({
|
||||
kind: 'task',
|
||||
taskId: task.taskId,
|
||||
status: task.status,
|
||||
notificationId: `task:${task.taskId}:${task.status}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function immediateProcess(exitCode: number, stdoutText = ''): IProcess {
|
||||
return {
|
||||
stdin: { write: vi.fn(), end: vi.fn() } as unknown as Writable,
|
||||
|
|
@ -78,6 +104,7 @@ describe('AgentTaskService — readOutput / getOutputSnapshot', () => {
|
|||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await waitForTaskNotifications(ctx, manager);
|
||||
await ctx.expectResumeMatches();
|
||||
} finally {
|
||||
await ctx.dispose();
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ describe('AgentTaskService — event emission', () => {
|
|||
status: 'running',
|
||||
}),
|
||||
});
|
||||
expect(agent.telemetry.track).toHaveBeenCalledWith('task_created', {
|
||||
expect(agent.telemetry.track).toHaveBeenCalledWith('background_task_created', {
|
||||
kind: 'bash',
|
||||
});
|
||||
});
|
||||
|
|
@ -305,7 +305,7 @@ describe('AgentTaskService — event emission', () => {
|
|||
status: 'running',
|
||||
}),
|
||||
});
|
||||
expect(agent.telemetry.track).toHaveBeenCalledWith('task_created', {
|
||||
expect(agent.telemetry.track).toHaveBeenCalledWith('background_task_created', {
|
||||
kind: 'agent',
|
||||
});
|
||||
});
|
||||
|
|
@ -325,10 +325,10 @@ describe('AgentTaskService — event emission', () => {
|
|||
}),
|
||||
});
|
||||
expect(agent.telemetry.track).toHaveBeenCalledWith(
|
||||
'task_completed',
|
||||
'background_task_completed',
|
||||
expect.objectContaining({
|
||||
kind: 'process',
|
||||
duration: expect.any(Number),
|
||||
duration_ms: expect.any(Number),
|
||||
status: 'completed',
|
||||
}),
|
||||
);
|
||||
|
|
@ -349,11 +349,11 @@ describe('AgentTaskService — event emission', () => {
|
|||
await timedOut;
|
||||
|
||||
expect(agent.telemetry.track).toHaveBeenCalledWith(
|
||||
'task_completed',
|
||||
'background_task_completed',
|
||||
expect.objectContaining({ kind: 'process', status: 'failed' }),
|
||||
);
|
||||
expect(agent.telemetry.track).toHaveBeenCalledWith(
|
||||
'task_completed',
|
||||
'background_task_completed',
|
||||
expect.objectContaining({ kind: 'agent', status: 'timed_out' }),
|
||||
);
|
||||
});
|
||||
|
|
@ -436,7 +436,7 @@ describe('AgentTaskService — notification delivery', () => {
|
|||
});
|
||||
const content = (message as { content: Array<{ text: string }> }).content;
|
||||
const text = content[0]!.text;
|
||||
expect(text).toContain('Task agent completed');
|
||||
expect(text).toContain('Background agent completed');
|
||||
expect(text).toContain('agent task completed.');
|
||||
expect(text).toContain('<output-file');
|
||||
expect(text).not.toContain('final subagent summary');
|
||||
|
|
@ -461,7 +461,7 @@ describe('AgentTaskService — notification delivery', () => {
|
|||
});
|
||||
const content = (message as { content: Array<{ text: string }> }).content;
|
||||
const text = content[0]!.text;
|
||||
expect(text).toContain('Task process completed');
|
||||
expect(text).toContain('Background process completed');
|
||||
expect(text).toContain('shell task completed.');
|
||||
});
|
||||
|
||||
|
|
@ -484,7 +484,7 @@ describe('AgentTaskService — notification delivery', () => {
|
|||
});
|
||||
const content = (message as { content: Array<{ text: string }> }).content;
|
||||
expect(content[0]!.text).toContain(
|
||||
'Task process killed',
|
||||
'Background process killed',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -513,7 +513,7 @@ describe('AgentTaskService — notification delivery', () => {
|
|||
notificationId: 'task:agent-done0000:completed',
|
||||
});
|
||||
const text = message.content[0]!.text;
|
||||
expect(text).toContain('Task agent completed');
|
||||
expect(text).toContain('Background agent completed');
|
||||
expect(text).not.toContain('restored subagent summary');
|
||||
expect(text).toContain('<output-file');
|
||||
expect(text).toContain(persistence.taskOutputFile('agent-done0000'));
|
||||
|
|
@ -547,7 +547,7 @@ describe('AgentTaskService — notification delivery', () => {
|
|||
notificationId: 'task:bash-done0000:completed',
|
||||
});
|
||||
const text = message.content[0]!.text;
|
||||
expect(text).toContain('Task process completed');
|
||||
expect(text).toContain('Background process completed');
|
||||
expect(text).not.toContain('restored shell output');
|
||||
expect(text).toContain('<output-file');
|
||||
expect(text).toContain(persistence.taskOutputFile('bash-done0000'));
|
||||
|
|
@ -654,7 +654,7 @@ describe('AgentTaskService — notification delivery', () => {
|
|||
notificationId: 'task:agent-run00000:lost',
|
||||
});
|
||||
expect(message.content[0]!.text).toContain(
|
||||
'Task agent lost',
|
||||
'Background agent lost',
|
||||
);
|
||||
} finally {
|
||||
await cleanupSessionDir(sessionDir, fixture);
|
||||
|
|
@ -684,10 +684,10 @@ describe('AgentTaskService — notification delivery', () => {
|
|||
inputData: {
|
||||
sink: 'context',
|
||||
notificationType: 'task.completed',
|
||||
title: 'Task agent completed',
|
||||
title: 'Background agent completed',
|
||||
body: 'inspect repository completed.',
|
||||
severity: 'info',
|
||||
sourceKind: 'task',
|
||||
sourceKind: 'background_task',
|
||||
sourceId: taskId,
|
||||
},
|
||||
}));
|
||||
|
|
@ -733,10 +733,10 @@ describe('AgentTaskService — notification delivery', () => {
|
|||
inputData: {
|
||||
sink: 'context',
|
||||
notificationType: 'task.completed',
|
||||
title: 'Task process completed',
|
||||
title: 'Background process completed',
|
||||
body: 'done completed.',
|
||||
severity: 'info',
|
||||
sourceKind: 'task',
|
||||
sourceKind: 'background_task',
|
||||
sourceId: taskId,
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ describe('Agent task notification XML', () => {
|
|||
id: 'n_"1&2',
|
||||
category: 'task',
|
||||
type: 'task.done',
|
||||
source_kind: 'task',
|
||||
source_kind: 'background_task',
|
||||
source_id: 'bg&1',
|
||||
title: 'Task finished',
|
||||
severity: 'info',
|
||||
|
|
@ -158,12 +158,12 @@ describe('Agent task notification XML', () => {
|
|||
id: 'n_lost1',
|
||||
category: 'task',
|
||||
type: 'task.lost',
|
||||
source_kind: 'task',
|
||||
source_kind: 'background_task',
|
||||
source_id: 'agent-w7gq3wwj',
|
||||
agent_id: 'agent-0',
|
||||
title: 'Task agent lost',
|
||||
title: 'Background agent lost',
|
||||
severity: 'warning',
|
||||
body: 'Task agent 1 lost.',
|
||||
body: 'Background agent 1 lost.',
|
||||
});
|
||||
|
||||
expect(text).toContain('source_id="agent-w7gq3wwj"');
|
||||
|
|
@ -175,9 +175,9 @@ describe('Agent task notification XML', () => {
|
|||
id: 'n_bash',
|
||||
category: 'task',
|
||||
type: 'task.completed',
|
||||
source_kind: 'task',
|
||||
source_kind: 'background_task',
|
||||
source_id: 'bash-abcdef00',
|
||||
title: 'Task completed',
|
||||
title: 'Background task completed',
|
||||
severity: 'info',
|
||||
body: 'echo done completed.',
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue