runner: narrow Claude task progress subagent mapping
This commit is contained in:
154
docs/checklist-continuation-mode-plan.md
Normal file
154
docs/checklist-continuation-mode-plan.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# Checklist Continuation Mode Plan
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
- 장기 리팩토링/장기 과제가 중간 `DONE`에서 끊기지 않게 한다.
|
||||||
|
- `DONE`의 의미는 유지한다.
|
||||||
|
- `DONE` = 이번 step 승인
|
||||||
|
- task 종료 = 마지막 step 완료
|
||||||
|
- `DONE_WITH_CONCERNS`를 continuation 용도로 남용하지 않게 한다.
|
||||||
|
|
||||||
|
## 원칙
|
||||||
|
|
||||||
|
- 새 상태값/새 테이블/마이그레이션부터 늘리지 않는다.
|
||||||
|
- 기존 `paired_tasks.plan_notes`를 versioned JSON으로 재사용한다.
|
||||||
|
- split point는 reviewer `DONE`이 아니라 owner finalize 직전이다.
|
||||||
|
- Arbiter/deadlock은 step 단위로 보고, 장기 stagnation은 plan 단위로 본다.
|
||||||
|
|
||||||
|
## 단계 전략
|
||||||
|
|
||||||
|
### 1. Lite Plan Mode
|
||||||
|
|
||||||
|
복잡도를 크게 늘리지 않고 플랜모드의 UX만 먼저 도입한다.
|
||||||
|
|
||||||
|
- `plan_notes`에 체크리스트 저장
|
||||||
|
- 명령:
|
||||||
|
- `플랜 시작: 1) ... 2) ...`
|
||||||
|
- `플랜 상태`
|
||||||
|
- `플랜 중지`
|
||||||
|
- 현재 step / 남은 step / 직전 요약만 표시
|
||||||
|
- 다음 step은 사용자가 수동으로 진행
|
||||||
|
|
||||||
|
장점:
|
||||||
|
|
||||||
|
- 상태 머신을 거의 건드리지 않는다
|
||||||
|
- 기존 owner/reviewer/arbiter 루프와 충돌이 적다
|
||||||
|
- checklist 기반 장기 작업 UX를 바로 제공할 수 있다
|
||||||
|
|
||||||
|
제한:
|
||||||
|
|
||||||
|
- `DONE` 후 자동 다음 step은 없다
|
||||||
|
- fresh session 자동 절단도 없다
|
||||||
|
|
||||||
|
### 2. Real Continuation Mode (MVP)
|
||||||
|
|
||||||
|
자동 continuation이 필요한 경우에만 확장한다.
|
||||||
|
|
||||||
|
- `plan_notes` JSON 스키마 + 파서
|
||||||
|
- 플랜 시작 / 상태 / 중지 명령
|
||||||
|
- owner finalize continuation 분기
|
||||||
|
- `round_trip_count` step 단위 리셋
|
||||||
|
- fresh session prompt wiring
|
||||||
|
- 테스트
|
||||||
|
|
||||||
|
## `plan_notes` JSON 스키마
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"mode": "planned",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "step-1",
|
||||||
|
"title": "A 리팩토링",
|
||||||
|
"status": "done",
|
||||||
|
"summary": "직전 step 요약",
|
||||||
|
"changedFiles": ["src/a.ts"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "step-2",
|
||||||
|
"title": "B 리팩토링",
|
||||||
|
"status": "in_progress"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "step-3",
|
||||||
|
"title": "C 리팩토링",
|
||||||
|
"status": "pending"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"currentIndex": 1,
|
||||||
|
"autoContinueOnDone": true,
|
||||||
|
"maxAutoTurns": 6,
|
||||||
|
"autoTurnsUsed": 2,
|
||||||
|
"lastStepSummary": "A 완료"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
판별 규칙:
|
||||||
|
|
||||||
|
- `plan_notes`가 versioned JSON이고 `mode: "planned"`일 때만 continuation mode로 해석
|
||||||
|
- 그 외 값은 legacy/freeform note로 유지
|
||||||
|
|
||||||
|
## 구현 포인트
|
||||||
|
|
||||||
|
### owner finalize 분기
|
||||||
|
|
||||||
|
- 위치: `src/paired-execution-context-owner.ts`
|
||||||
|
- reviewer 승인 후 owner finalize가 `DONE`이고 남은 checklist item이 있으면:
|
||||||
|
- 현재 item -> `done`
|
||||||
|
- 다음 item -> `in_progress`
|
||||||
|
- `round_trip_count = 0`
|
||||||
|
- `autoTurnsUsed += 1`
|
||||||
|
- `lastStepSummary`, `changedFiles` 저장
|
||||||
|
- task status는 `completed`가 아니라 continuation 가능한 상태로 유지
|
||||||
|
- 마지막 item일 때만 `completed`
|
||||||
|
|
||||||
|
### 다음 owner step 자동 시작
|
||||||
|
|
||||||
|
- 위치: `src/message-runtime-rules.ts`
|
||||||
|
- planned task는 `active + lastTurnOutputRole=owner`라도 `owner-follow-up`으로 이어질 수 있어야 한다
|
||||||
|
|
||||||
|
### compact owner prompt
|
||||||
|
|
||||||
|
- 위치: `src/message-runtime-prompts.ts`
|
||||||
|
- planned continuation에서는 기존 pending prompt를 그대로 재사용하지 않는다
|
||||||
|
- 다음 step prompt에는 최소한 아래만 전달한다
|
||||||
|
- 현재 checklist item
|
||||||
|
- 직전 step 요약
|
||||||
|
- 변경 파일 목록
|
||||||
|
- 남은 item 목록
|
||||||
|
|
||||||
|
## 외부 프로젝트에서 차용할 것
|
||||||
|
|
||||||
|
### Archon
|
||||||
|
|
||||||
|
- checklist step 진행
|
||||||
|
- fresh context
|
||||||
|
- approval gate 발상
|
||||||
|
|
||||||
|
### OpenHarness
|
||||||
|
|
||||||
|
- permission mode 분리
|
||||||
|
- auto-compaction 같은 운영 아이디어
|
||||||
|
|
||||||
|
## 외부 프로젝트에서 차용하지 않을 것
|
||||||
|
|
||||||
|
- Archon의 YAML DAG/workflow 엔진 전체
|
||||||
|
- OpenHarness의 `plan mode` 자체
|
||||||
|
- 이쪽은 continuation 엔진이 아니라 permission toggle에 가깝다
|
||||||
|
- 프레임워크/런타임 통째 교체
|
||||||
|
|
||||||
|
## 구현 순서 제안
|
||||||
|
|
||||||
|
1. Lite Plan Mode
|
||||||
|
2. 실제 사용 패턴 확인
|
||||||
|
3. Real Continuation Mode MVP
|
||||||
|
4. 이후 compaction / permission polish는 별도 enhancement
|
||||||
|
|
||||||
|
## out of scope
|
||||||
|
|
||||||
|
- step 순서 변경
|
||||||
|
- step 분할/병합
|
||||||
|
- 중간 삽입/삭제
|
||||||
|
- 새 테이블 도입
|
||||||
|
- auto-compaction 본체 구현
|
||||||
@@ -48,6 +48,12 @@ import {
|
|||||||
type RunnerOutput,
|
type RunnerOutput,
|
||||||
writeOutput,
|
writeOutput,
|
||||||
} from './output-protocol.js';
|
} from './output-protocol.js';
|
||||||
|
import {
|
||||||
|
TopLevelAgentTaskTracker,
|
||||||
|
buildTaskNotificationOutput,
|
||||||
|
buildTaskProgressOutput,
|
||||||
|
buildTaskStartedOutput,
|
||||||
|
} from './task-progress-mapping.js';
|
||||||
import {
|
import {
|
||||||
createPreCompactHook,
|
createPreCompactHook,
|
||||||
createReviewerBashGuardHook,
|
createReviewerBashGuardHook,
|
||||||
@@ -153,6 +159,7 @@ async function runQuery(
|
|||||||
let resultCount = 0;
|
let resultCount = 0;
|
||||||
let terminalResultObserved = false;
|
let terminalResultObserved = false;
|
||||||
let pendingProgressText: string | null = null;
|
let pendingProgressText: string | null = null;
|
||||||
|
const trackedAgentTasks = new TopLevelAgentTaskTracker();
|
||||||
|
|
||||||
// Discover additional directories
|
// Discover additional directories
|
||||||
const extraDirs: string[] = [];
|
const extraDirs: string[] = [];
|
||||||
@@ -366,25 +373,20 @@ async function runQuery(
|
|||||||
) {
|
) {
|
||||||
const tn = message as {
|
const tn = message as {
|
||||||
task_id: string;
|
task_id: string;
|
||||||
|
tool_use_id?: string;
|
||||||
status: string;
|
status: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
};
|
};
|
||||||
log(
|
log(
|
||||||
`Task notification: task=${tn.task_id} status=${tn.status} summary=${tn.summary}`,
|
`Task notification: task=${tn.task_id} status=${tn.status} summary=${tn.summary}`,
|
||||||
);
|
);
|
||||||
if (
|
const mapped = buildTaskNotificationOutput(
|
||||||
tn.status === 'completed' ||
|
trackedAgentTasks,
|
||||||
tn.status === 'error' ||
|
tn,
|
||||||
tn.status === 'cancelled'
|
|
||||||
) {
|
|
||||||
writeOutput({
|
|
||||||
status: 'success',
|
|
||||||
phase: 'progress',
|
|
||||||
agentId: tn.task_id,
|
|
||||||
agentDone: true,
|
|
||||||
result: tn.summary || null,
|
|
||||||
newSessionId,
|
newSessionId,
|
||||||
});
|
);
|
||||||
|
if (mapped) {
|
||||||
|
writeOutput(mapped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,20 +395,16 @@ async function runQuery(
|
|||||||
(message as { subtype?: string }).subtype === 'task_progress'
|
(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 =
|
const description =
|
||||||
typeof tp.description === 'string' ? tp.description : '';
|
typeof tp.description === 'string' ? tp.description : '';
|
||||||
if (description && description.length <= 80) {
|
const mapped = buildTaskProgressOutput(
|
||||||
// Short tool description → show as sub-line in progress
|
trackedAgentTasks,
|
||||||
const normalized = normalizeStructuredOutput(description);
|
tp,
|
||||||
writeOutput({
|
|
||||||
status: 'success',
|
|
||||||
phase: 'tool-activity',
|
|
||||||
...normalized,
|
|
||||||
agentId: taskId,
|
|
||||||
newSessionId,
|
newSessionId,
|
||||||
});
|
);
|
||||||
|
if (mapped) {
|
||||||
|
writeOutput(mapped);
|
||||||
} else if (description) {
|
} else if (description) {
|
||||||
// Long AI summary → skip (too long for progress sub-line)
|
// Long AI summary → skip (too long for progress sub-line)
|
||||||
log(
|
log(
|
||||||
@@ -422,16 +420,13 @@ async function runQuery(
|
|||||||
const ts = message as { task_id: string; description?: string };
|
const ts = message as { task_id: string; description?: string };
|
||||||
const desc = ts.description || '';
|
const desc = ts.description || '';
|
||||||
log(`Subagent started: task=${ts.task_id} desc=${desc.slice(0, 200)}`);
|
log(`Subagent started: task=${ts.task_id} desc=${desc.slice(0, 200)}`);
|
||||||
if (desc) {
|
const mapped = buildTaskStartedOutput(
|
||||||
const normalized = normalizeStructuredOutput(`🔄 ${desc}`);
|
trackedAgentTasks,
|
||||||
writeOutput({
|
ts,
|
||||||
status: 'success',
|
|
||||||
phase: 'progress',
|
|
||||||
...normalized,
|
|
||||||
agentId: ts.task_id,
|
|
||||||
agentLabel: desc,
|
|
||||||
newSessionId,
|
newSessionId,
|
||||||
});
|
);
|
||||||
|
if (mapped) {
|
||||||
|
writeOutput(mapped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,6 +537,7 @@ async function runQuery(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === 'assistant') {
|
if (message.type === 'assistant') {
|
||||||
|
trackedAgentTasks.rememberAssistantMessage(message);
|
||||||
const stopReason = (message as { stop_reason?: string }).stop_reason;
|
const stopReason = (message as { stop_reason?: string }).stop_reason;
|
||||||
const textResult = extractAssistantText(message);
|
const textResult = extractAssistantText(message);
|
||||||
// Only log when there's something interesting (text or terminal)
|
// Only log when there's something interesting (text or terminal)
|
||||||
|
|||||||
188
runners/agent-runner/src/task-progress-mapping.ts
Normal file
188
runners/agent-runner/src/task-progress-mapping.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
128
runners/agent-runner/test/task-progress-mapping.test.ts
Normal file
128
runners/agent-runner/test/task-progress-mapping.test.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TopLevelAgentTaskTracker,
|
||||||
|
buildTaskNotificationOutput,
|
||||||
|
buildTaskProgressOutput,
|
||||||
|
buildTaskStartedOutput,
|
||||||
|
} from '../src/task-progress-mapping.js';
|
||||||
|
|
||||||
|
describe('task progress mapping', () => {
|
||||||
|
it('tracks only top-level Agent tool uses as subagents', () => {
|
||||||
|
const tracker = new TopLevelAgentTaskTracker();
|
||||||
|
|
||||||
|
tracker.rememberAssistantMessage({
|
||||||
|
message: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tool_use',
|
||||||
|
id: 'toolu_top',
|
||||||
|
name: 'Agent',
|
||||||
|
caller: { type: 'direct' },
|
||||||
|
input: { description: 'Find subagent enabling mechanism' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'tool_use',
|
||||||
|
id: 'toolu_nested',
|
||||||
|
name: 'Agent',
|
||||||
|
caller: { type: 'subagent' },
|
||||||
|
input: { description: 'Nested helper task' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const started = buildTaskStartedOutput(
|
||||||
|
tracker,
|
||||||
|
{
|
||||||
|
task_id: 'task-top',
|
||||||
|
tool_use_id: 'toolu_top',
|
||||||
|
description: 'Find subagent enabling mechanism',
|
||||||
|
},
|
||||||
|
'session-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
const nested = buildTaskStartedOutput(
|
||||||
|
tracker,
|
||||||
|
{
|
||||||
|
task_id: 'task-nested',
|
||||||
|
tool_use_id: 'toolu_nested',
|
||||||
|
description: 'Nested helper task',
|
||||||
|
},
|
||||||
|
'session-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(started?.agentId).toBe('task-top');
|
||||||
|
expect(started?.agentLabel).toBe('Find subagent enabling mechanism');
|
||||||
|
expect(nested?.agentId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps task progress attached to the tracked top-level subagent', () => {
|
||||||
|
const tracker = new TopLevelAgentTaskTracker();
|
||||||
|
|
||||||
|
tracker.rememberAssistantMessage({
|
||||||
|
message: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'tool_use',
|
||||||
|
id: 'toolu_top',
|
||||||
|
name: 'Agent',
|
||||||
|
caller: { type: 'direct' },
|
||||||
|
input: { description: 'Find codex config.toml location' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
buildTaskStartedOutput(tracker, {
|
||||||
|
task_id: 'task-top',
|
||||||
|
tool_use_id: 'toolu_top',
|
||||||
|
description: 'Find codex config.toml location',
|
||||||
|
});
|
||||||
|
|
||||||
|
const progress = buildTaskProgressOutput(tracker, {
|
||||||
|
task_id: 'task-top',
|
||||||
|
description: 'Extract exact hide_spawn_agent field names',
|
||||||
|
});
|
||||||
|
|
||||||
|
const notification = buildTaskNotificationOutput(tracker, {
|
||||||
|
task_id: 'task-top',
|
||||||
|
status: 'failed',
|
||||||
|
summary: 'Task failed',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(progress?.agentId).toBe('task-top');
|
||||||
|
expect(progress?.result).toBe('Extract exact hide_spawn_agent field names');
|
||||||
|
expect(notification?.agentId).toBe('task-top');
|
||||||
|
expect(notification?.agentDone).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('flattens untracked internal task progress into generic progress lines', () => {
|
||||||
|
const tracker = new TopLevelAgentTaskTracker();
|
||||||
|
|
||||||
|
const started = buildTaskStartedOutput(tracker, {
|
||||||
|
task_id: 'task-internal',
|
||||||
|
tool_use_id: 'toolu_internal',
|
||||||
|
description: 'Search Codex package for hide_spawn_agent config',
|
||||||
|
});
|
||||||
|
|
||||||
|
const progress = buildTaskProgressOutput(tracker, {
|
||||||
|
task_id: 'task-internal',
|
||||||
|
tool_use_id: 'toolu_internal',
|
||||||
|
description: 'Extract exact hide_spawn_agent field names',
|
||||||
|
});
|
||||||
|
|
||||||
|
const notification = buildTaskNotificationOutput(tracker, {
|
||||||
|
task_id: 'task-internal',
|
||||||
|
tool_use_id: 'toolu_internal',
|
||||||
|
status: 'completed',
|
||||||
|
summary: 'Finished searching config',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(started?.agentId).toBeUndefined();
|
||||||
|
expect(progress?.agentId).toBeUndefined();
|
||||||
|
expect(progress?.result).toBe('Extract exact hide_spawn_agent field names');
|
||||||
|
expect(notification?.agentId).toBeUndefined();
|
||||||
|
expect(notification?.agentDone).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user