Files
EJClaw/runners/agent-runner/src/task-progress-mapping.ts

189 lines
4.6 KiB
TypeScript

import {
normalizeStructuredOutput,
type RunnerOutput,
} from './output-protocol.js';
type AssistantToolUseBlock = {
type?: unknown;
id?: unknown;
name?: unknown;
caller?: { type?: unknown } | null;
input?: { description?: unknown } | null;
};
type AssistantMessageLike = {
type?: unknown;
message?: {
content?: unknown;
};
};
type TaskLike = {
task_id?: unknown;
tool_use_id?: unknown;
description?: unknown;
status?: unknown;
summary?: unknown;
};
function toNonEmptyString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim().length > 0
? value.trim()
: undefined;
}
function getTaskText(message: TaskLike): {
taskId?: string;
toolUseId?: string;
description?: string;
summary?: string;
} {
return {
taskId: toNonEmptyString(message.task_id),
toolUseId: toNonEmptyString(message.tool_use_id),
description: toNonEmptyString(message.description),
summary: toNonEmptyString(message.summary),
};
}
export class TopLevelAgentTaskTracker {
private readonly topLevelToolUses = new Map<string, string>();
private readonly taskLabels = new Map<string, string>();
rememberAssistantMessage(message: unknown): void {
const assistant = message as AssistantMessageLike;
const blocks = assistant.message?.content;
if (!Array.isArray(blocks)) return;
for (const block of blocks) {
const toolUse = block as AssistantToolUseBlock;
if (toolUse.type !== 'tool_use' || toolUse.name !== 'Agent') continue;
const toolUseId = toNonEmptyString(toolUse.id);
if (!toolUseId) continue;
const callerType = toolUse.caller?.type;
if (callerType !== undefined && callerType !== 'direct') continue;
const description =
toNonEmptyString(toolUse.input?.description) || '서브에이전트 작업';
this.topLevelToolUses.set(toolUseId, description);
}
}
resolveTask(message: TaskLike): {
tracked: boolean;
taskId?: string;
label?: string;
} {
const { taskId, toolUseId, description } = getTaskText(message);
if (!taskId) {
return { tracked: false };
}
const existing = this.taskLabels.get(taskId);
if (existing) {
return { tracked: true, taskId, label: existing };
}
if (toolUseId) {
const topLevelLabel = this.topLevelToolUses.get(toolUseId);
if (topLevelLabel) {
const label = topLevelLabel || description || '서브에이전트 작업';
this.taskLabels.set(taskId, label);
return { tracked: true, taskId, label };
}
}
return { tracked: false, taskId, label: description };
}
}
export function buildTaskStartedOutput(
tracker: TopLevelAgentTaskTracker,
message: TaskLike,
newSessionId?: string,
): RunnerOutput | null {
const { description } = getTaskText(message);
if (!description) return null;
const resolved = tracker.resolveTask(message);
const label = resolved.label || description;
const normalized = normalizeStructuredOutput(`🔄 ${label}`);
return {
status: 'success',
phase: 'progress',
...normalized,
...(resolved.tracked && resolved.taskId
? {
agentId: resolved.taskId,
agentLabel: label,
}
: {}),
newSessionId,
};
}
export function buildTaskProgressOutput(
tracker: TopLevelAgentTaskTracker,
message: TaskLike,
newSessionId?: string,
): RunnerOutput | null {
const { description } = getTaskText(message);
if (!description || description.length > 80) return null;
const resolved = tracker.resolveTask(message);
const normalized = normalizeStructuredOutput(description);
return {
status: 'success',
phase: 'tool-activity',
...normalized,
...(resolved.tracked && resolved.taskId
? {
agentId: resolved.taskId,
}
: {}),
newSessionId,
};
}
export function buildTaskNotificationOutput(
tracker: TopLevelAgentTaskTracker,
message: TaskLike,
newSessionId?: string,
): RunnerOutput | null {
const { summary } = getTaskText(message);
const status = toNonEmptyString(message.status);
if (!status) return null;
if (
status !== 'completed' &&
status !== 'failed' &&
status !== 'stopped' &&
status !== 'error' &&
status !== 'cancelled'
) {
return null;
}
const resolved = tracker.resolveTask(message);
return {
status: 'success',
phase: 'progress',
...(summary !== undefined
? {
...normalizeStructuredOutput(summary),
}
: { result: null }),
...(resolved.tracked && resolved.taskId
? {
agentId: resolved.taskId,
agentDone: true,
}
: {}),
newSessionId,
};
}