feat: multi-agent progress tracking with per-subagent display
Track each subagent separately via agentId from SDK task events. Single subagent shows detailed view with activity sub-lines, multiple subagents show compact one-line-each format.
This commit is contained in:
@@ -33,6 +33,9 @@ interface ContainerInput {
|
|||||||
interface ContainerOutput {
|
interface ContainerOutput {
|
||||||
status: 'success' | 'error';
|
status: 'success' | 'error';
|
||||||
phase?: 'progress' | 'final' | 'tool-activity' | 'intermediate';
|
phase?: 'progress' | 'final' | 'tool-activity' | 'intermediate';
|
||||||
|
agentId?: string;
|
||||||
|
agentLabel?: string;
|
||||||
|
agentDone?: boolean;
|
||||||
result: string | null;
|
result: string | null;
|
||||||
newSessionId?: string;
|
newSessionId?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -561,10 +564,21 @@ async function runQuery(
|
|||||||
if (message.type === 'system' && (message as { subtype?: string }).subtype === 'task_notification') {
|
if (message.type === 'system' && (message as { subtype?: string }).subtype === 'task_notification') {
|
||||||
const tn = message as { task_id: string; status: string; summary: string };
|
const tn = message as { task_id: string; status: string; summary: string };
|
||||||
log(`Task notification: task=${tn.task_id} status=${tn.status} summary=${tn.summary}`);
|
log(`Task notification: task=${tn.task_id} status=${tn.status} summary=${tn.summary}`);
|
||||||
|
if (tn.status === 'completed' || tn.status === 'error' || tn.status === 'cancelled') {
|
||||||
|
writeOutput({
|
||||||
|
status: 'success',
|
||||||
|
phase: 'progress',
|
||||||
|
agentId: tn.task_id,
|
||||||
|
agentDone: true,
|
||||||
|
result: tn.summary || null,
|
||||||
|
newSessionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === 'system' && (message as { subtype?: string }).subtype === 'task_progress') {
|
if (message.type === 'system' && (message as { subtype?: string }).subtype === 'task_progress') {
|
||||||
const tp = message as Record<string, unknown>;
|
const tp = message as Record<string, unknown>;
|
||||||
|
const taskId = typeof tp.task_id === 'string' ? tp.task_id : undefined;
|
||||||
const summary = typeof tp.summary === 'string' ? tp.summary : '';
|
const summary = typeof tp.summary === 'string' ? tp.summary : '';
|
||||||
const description = typeof tp.description === 'string' ? tp.description : '';
|
const description = typeof tp.description === 'string' ? tp.description : '';
|
||||||
if (description) {
|
if (description) {
|
||||||
@@ -572,6 +586,7 @@ async function runQuery(
|
|||||||
status: 'success',
|
status: 'success',
|
||||||
phase: 'tool-activity',
|
phase: 'tool-activity',
|
||||||
result: description,
|
result: description,
|
||||||
|
agentId: taskId,
|
||||||
newSessionId,
|
newSessionId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -586,6 +601,8 @@ async function runQuery(
|
|||||||
status: 'success',
|
status: 'success',
|
||||||
phase: 'progress',
|
phase: 'progress',
|
||||||
result: `🔄 ${desc}`,
|
result: `🔄 ${desc}`,
|
||||||
|
agentId: ts.task_id,
|
||||||
|
agentLabel: desc,
|
||||||
newSessionId,
|
newSessionId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ export interface AgentOutput {
|
|||||||
status: 'success' | 'error';
|
status: 'success' | 'error';
|
||||||
result: string | null;
|
result: string | null;
|
||||||
phase?: 'progress' | 'final' | 'tool-activity' | 'intermediate';
|
phase?: 'progress' | 'final' | 'tool-activity' | 'intermediate';
|
||||||
|
agentId?: string;
|
||||||
|
agentLabel?: string;
|
||||||
|
agentDone?: boolean;
|
||||||
newSessionId?: string;
|
newSessionId?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import { type Channel, type RegisteredGroup } from './types.js';
|
|||||||
|
|
||||||
export type VisiblePhase = 'silent' | 'progress' | 'final';
|
export type VisiblePhase = 'silent' | 'progress' | 'final';
|
||||||
|
|
||||||
|
interface SubagentTrack {
|
||||||
|
label: string;
|
||||||
|
activities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
interface MessageTurnControllerOptions {
|
interface MessageTurnControllerOptions {
|
||||||
chatJid: string;
|
chatJid: string;
|
||||||
group: RegisteredGroup;
|
group: RegisteredGroup;
|
||||||
@@ -36,6 +41,7 @@ export class MessageTurnController {
|
|||||||
private progressTicker: ReturnType<typeof setInterval> | null = null;
|
private progressTicker: ReturnType<typeof setInterval> | null = null;
|
||||||
private progressEditFailCount = 0;
|
private progressEditFailCount = 0;
|
||||||
private latestProgressTextForFinal: string | null = null;
|
private latestProgressTextForFinal: string | null = null;
|
||||||
|
private subagents = new Map<string, SubagentTrack>();
|
||||||
private poisonedSessionDetected = false;
|
private poisonedSessionDetected = false;
|
||||||
private closeRequested = false;
|
private closeRequested = false;
|
||||||
|
|
||||||
@@ -117,20 +123,29 @@ export class MessageTurnController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.phase === 'tool-activity') {
|
if (result.phase === 'tool-activity') {
|
||||||
// Ensure a progress message exists for tool activity sub-lines
|
if (result.agentId) {
|
||||||
if (!this.progressMessageId && !this.progressCreating) {
|
// Subagent tool activity
|
||||||
this.progressCreating = true;
|
let track = this.subagents.get(result.agentId);
|
||||||
const heading = this.pendingProgressText || '작업 중...';
|
if (!track) {
|
||||||
this.sendProgressMessage(heading).then(() => {
|
track = { label: '작업 중...', activities: [] };
|
||||||
this.progressCreating = false;
|
this.subagents.set(result.agentId, track);
|
||||||
this.ensureProgressTicker();
|
}
|
||||||
// Replay any queued tool activities now that the message exists
|
if (text) {
|
||||||
if (this.toolActivities.length > 0 && this.progressMessageId) {
|
const MAX = 2;
|
||||||
void this.syncTrackedProgressMessage();
|
track.activities.push(text);
|
||||||
|
if (track.activities.length > MAX) {
|
||||||
|
track.activities = track.activities.slice(-MAX);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
this.pendingProgressText = null;
|
this.ensureProgressMessageExists();
|
||||||
|
this.ensureProgressTicker();
|
||||||
|
if (!this.poisonedSessionDetected) {
|
||||||
|
this.resetIdleTimer();
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
// Main agent tool activity
|
||||||
|
this.ensureProgressMessageExists();
|
||||||
if (text) {
|
if (text) {
|
||||||
this.addToolActivity(text);
|
this.addToolActivity(text);
|
||||||
}
|
}
|
||||||
@@ -141,6 +156,39 @@ export class MessageTurnController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.phase === 'progress') {
|
if (result.phase === 'progress') {
|
||||||
|
if (result.agentId) {
|
||||||
|
if (result.agentDone) {
|
||||||
|
this.subagents.delete(result.agentId);
|
||||||
|
} else {
|
||||||
|
const label =
|
||||||
|
text ||
|
||||||
|
(result.agentLabel ? `🔄 ${result.agentLabel}` : '작업 중...');
|
||||||
|
const existing = this.subagents.get(result.agentId);
|
||||||
|
if (existing) {
|
||||||
|
existing.label = label;
|
||||||
|
existing.activities = [];
|
||||||
|
} else {
|
||||||
|
this.subagents.set(result.agentId, { label, activities: [] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.latestProgressText) {
|
||||||
|
this.latestProgressText = '작업 중...';
|
||||||
|
this.latestProgressTextForFinal = '작업 중...';
|
||||||
|
}
|
||||||
|
this.ensureProgressMessageExists();
|
||||||
|
this.ensureProgressTicker();
|
||||||
|
if (this.progressMessageId) {
|
||||||
|
void this.syncTrackedProgressMessage();
|
||||||
|
}
|
||||||
|
if (!this.poisonedSessionDetected) {
|
||||||
|
this.resetIdleTimer();
|
||||||
|
}
|
||||||
|
if (result.status === 'error') {
|
||||||
|
this.hadError = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Main agent progress
|
||||||
if (text) {
|
if (text) {
|
||||||
if (this.progressMessageId) {
|
if (this.progressMessageId) {
|
||||||
// Progress message already visible — update heading directly
|
// Progress message already visible — update heading directly
|
||||||
@@ -262,27 +310,48 @@ export class MessageTurnController {
|
|||||||
if (minutes > 0) elapsedParts.push(`${minutes}분`);
|
if (minutes > 0) elapsedParts.push(`${minutes}분`);
|
||||||
elapsedParts.push(`${seconds}초`);
|
elapsedParts.push(`${seconds}초`);
|
||||||
|
|
||||||
const activityLines =
|
|
||||||
this.toolActivities.length > 0
|
|
||||||
? '\n' +
|
|
||||||
this.toolActivities
|
|
||||||
.map((a, i) => {
|
|
||||||
const isLast = i === this.toolActivities.length - 1;
|
|
||||||
const connector = isLast ? '└' : '├';
|
|
||||||
const isSummary = a.startsWith('📋');
|
|
||||||
return isSummary ? `${connector} ${a}` : `${connector} ${a}`;
|
|
||||||
})
|
|
||||||
.join('\n')
|
|
||||||
: '';
|
|
||||||
const suffix = `\n\n${elapsedParts.join(' ')}`;
|
const suffix = `\n\n${elapsedParts.join(' ')}`;
|
||||||
const maxText =
|
let body: string;
|
||||||
2000 -
|
|
||||||
TASK_STATUS_MESSAGE_PREFIX.length -
|
if (this.subagents.size > 1) {
|
||||||
activityLines.length -
|
// Compact: one line per subagent with latest activity
|
||||||
suffix.length;
|
const lines: string[] = [];
|
||||||
|
for (const [, track] of this.subagents) {
|
||||||
|
const latest = track.activities[track.activities.length - 1];
|
||||||
|
lines.push(latest ? `${track.label} · ${latest}` : track.label);
|
||||||
|
}
|
||||||
|
body = lines.join('\n');
|
||||||
|
} else if (this.subagents.size === 1) {
|
||||||
|
// Single subagent: detailed view with activity sub-lines
|
||||||
|
const [, track] = this.subagents.entries().next().value!;
|
||||||
|
const lines: string[] = [track.label];
|
||||||
|
for (let i = 0; i < track.activities.length; i++) {
|
||||||
|
const isLast = i === track.activities.length - 1;
|
||||||
|
lines.push(`${isLast ? '└' : '├'} ${track.activities[i]}`);
|
||||||
|
}
|
||||||
|
body = lines.join('\n');
|
||||||
|
} else {
|
||||||
|
// Single agent rendering
|
||||||
|
const activityLines =
|
||||||
|
this.toolActivities.length > 0
|
||||||
|
? '\n' +
|
||||||
|
this.toolActivities
|
||||||
|
.map((a, i) => {
|
||||||
|
const isLast = i === this.toolActivities.length - 1;
|
||||||
|
const connector = isLast ? '└' : '├';
|
||||||
|
const isSummary = a.startsWith('📋');
|
||||||
|
return isSummary ? `${connector} ${a}` : `${connector} ${a}`;
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
: '';
|
||||||
|
body = text + activityLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxBody =
|
||||||
|
2000 - TASK_STATUS_MESSAGE_PREFIX.length - suffix.length;
|
||||||
const truncated =
|
const truncated =
|
||||||
text.length > maxText ? text.slice(0, maxText - 1) + '…' : text;
|
body.length > maxBody ? body.slice(0, maxBody - 1) + '…' : body;
|
||||||
return `${TASK_STATUS_MESSAGE_PREFIX}${truncated}${activityLines}${suffix}`;
|
return `${TASK_STATUS_MESSAGE_PREFIX}${truncated}${suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearProgressTicker(): void {
|
private clearProgressTicker(): void {
|
||||||
@@ -296,6 +365,7 @@ export class MessageTurnController {
|
|||||||
this.pendingProgressText = null;
|
this.pendingProgressText = null;
|
||||||
this.progressCreating = false;
|
this.progressCreating = false;
|
||||||
this.toolActivities = [];
|
this.toolActivities = [];
|
||||||
|
this.subagents.clear();
|
||||||
this.latestProgressText = null;
|
this.latestProgressText = null;
|
||||||
this.previousProgressText = null;
|
this.previousProgressText = null;
|
||||||
this.latestProgressRendered = null;
|
this.latestProgressRendered = null;
|
||||||
@@ -304,6 +374,32 @@ export class MessageTurnController {
|
|||||||
this.progressEditFailCount = 0;
|
this.progressEditFailCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure a progress message exists in Discord.
|
||||||
|
* Creates one if needed, using pending or default text.
|
||||||
|
*/
|
||||||
|
private ensureProgressMessageExists(): void {
|
||||||
|
if (this.progressMessageId || this.progressCreating) return;
|
||||||
|
this.progressCreating = true;
|
||||||
|
const heading =
|
||||||
|
this.pendingProgressText || this.latestProgressText || '작업 중...';
|
||||||
|
if (!this.latestProgressText) {
|
||||||
|
this.latestProgressText = heading;
|
||||||
|
this.latestProgressTextForFinal = heading;
|
||||||
|
}
|
||||||
|
void this.sendProgressMessage(heading).then(() => {
|
||||||
|
this.progressCreating = false;
|
||||||
|
this.ensureProgressTicker();
|
||||||
|
if (
|
||||||
|
(this.toolActivities.length > 0 || this.subagents.size > 0) &&
|
||||||
|
this.progressMessageId
|
||||||
|
) {
|
||||||
|
void this.syncTrackedProgressMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.pendingProgressText = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buffer a progress update. The previous pending text gets flushed
|
* Buffer a progress update. The previous pending text gets flushed
|
||||||
* immediately, and the new text waits until the next event arrives.
|
* immediately, and the new text waits until the next event arrives.
|
||||||
|
|||||||
Reference in New Issue
Block a user