Render sub-agent activity from log

This commit is contained in:
Mohamed Boudra
2026-05-02 17:20:22 +07:00
parent 771f164e26
commit 49e3011a28
21 changed files with 208 additions and 303 deletions
@@ -244,31 +244,62 @@ function resolveSubAgentFallbackHeader(
interface SubAgentDetailProps { interface SubAgentDetailProps {
log: string; log: string;
actions: Extract<ToolCallDetail, { type: "sub_agent" }>["actions"];
childSessionId: string | null | undefined; childSessionId: string | null | undefined;
subAgentType: string | null | undefined; subAgentType: string | null | undefined;
description: string | null | undefined; description: string | null | undefined;
ds: DetailStyles; ds: DetailStyles;
} }
function buildSubAgentActionLog(actions: SubAgentDetailProps["actions"]): string { interface SubAgentActivityRow {
return actions index: number;
.map((action) => toolName: string;
action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`, summary?: string;
)
.join("\n");
} }
function stripSubAgentActionLog(log: string, actions: SubAgentDetailProps["actions"]): string { interface ParsedSubAgentLog {
const actionLog = buildSubAgentActionLog(actions); actions: SubAgentActivityRow[];
const trimmedLog = log.replace(/^\n+/, ""); remainingLog: string;
if (!actionLog || !trimmedLog.startsWith(actionLog)) { }
return trimmedLog;
function parseBracketedSubAgentLine(line: string, index: number): SubAgentActivityRow | null {
const match = line.match(/^\[([^\]]+)\](?:\s+(.*))?$/);
if (!match) {
return null;
} }
return trimmedLog.slice(actionLog.length).replace(/^\n+/, ""); const toolName = match[1]?.trim();
if (!toolName) {
return null;
}
const summary = match[2]?.trim();
return {
index,
toolName,
...(summary ? { summary } : {}),
};
} }
function SubAgentActionRow({ action }: { action: SubAgentDetailProps["actions"][number] }) { function parseSubAgentLog(log: string): ParsedSubAgentLog {
const actions: SubAgentActivityRow[] = [];
const remainingLines: string[] = [];
for (const line of log.replace(/^\n+/, "").split("\n")) {
const normalizedLine = line.trim();
if (!normalizedLine) {
continue;
}
const parsedAction = parseBracketedSubAgentLine(normalizedLine, actions.length + 1);
if (parsedAction) {
actions.push(parsedAction);
} else {
remainingLines.push(line);
}
}
return {
actions,
remainingLog: remainingLines.join("\n").replace(/^\n+/, ""),
};
}
function SubAgentActionRow({ action }: { action: SubAgentActivityRow }) {
return ( return (
<View style={styles.subAgentActionRow}> <View style={styles.subAgentActionRow}>
<Text selectable style={styles.subAgentActionTool}> <Text selectable style={styles.subAgentActionTool}>
@@ -324,13 +355,12 @@ function SubAgentLogText({
function SubAgentDetailSection({ function SubAgentDetailSection({
log, log,
actions,
childSessionId, childSessionId,
subAgentType, subAgentType,
description, description,
ds, ds,
}: SubAgentDetailProps) { }: SubAgentDetailProps) {
const activityLog = stripSubAgentActionLog(log, actions); const { actions, remainingLog } = useMemo(() => parseSubAgentLog(log), [log]);
const fallbackHeader = resolveSubAgentFallbackHeader(subAgentType, description); const fallbackHeader = resolveSubAgentFallbackHeader(subAgentType, description);
const hasActions = actions.length > 0; const hasActions = actions.length > 0;
return ( return (
@@ -363,7 +393,7 @@ function SubAgentDetailSection({
</View> </View>
) : null} ) : null}
<SubAgentLogText <SubAgentLogText
activityLog={activityLog} activityLog={remainingLog}
fallbackHeader={fallbackHeader} fallbackHeader={fallbackHeader}
hasActions={hasActions} hasActions={hasActions}
/> />
@@ -624,7 +654,6 @@ function buildDetailSections(
<SubAgentDetailSection <SubAgentDetailSection
key="sub-agent" key="sub-agent"
log={detail.log} log={detail.log}
actions={detail.actions}
childSessionId={detail.childSessionId} childSessionId={detail.childSessionId}
subAgentType={detail.subAgentType} subAgentType={detail.subAgentType}
description={detail.description} description={detail.description}
@@ -844,7 +873,6 @@ const styles = StyleSheet.create((theme) => {
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
subAgentActionTool: { subAgentActionTool: {
minWidth: 56,
fontFamily: Fonts.mono, fontFamily: Fonts.mono,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
-24
View File
@@ -367,18 +367,6 @@ describe("stream reducer canonical tool calls", () => {
subAgentType: "Explore", subAgentType: "Explore",
description: "Inspect repository structure", description: "Inspect repository structure",
log: "[Read] README.md\n[Bash] ls", log: "[Read] README.md\n[Bash] ls",
actions: [
{
index: 1,
toolName: "Read",
summary: "README.md",
},
{
index: 2,
toolName: "Bash",
summary: "ls",
},
],
}, },
}), }),
timestamp: new Date("2025-01-01T10:12:00Z"), timestamp: new Date("2025-01-01T10:12:00Z"),
@@ -406,18 +394,6 @@ describe("stream reducer canonical tool calls", () => {
subAgentType: "Explore", subAgentType: "Explore",
description: "Inspect repository structure", description: "Inspect repository structure",
log: "[Read] README.md\n[Bash] ls", log: "[Read] README.md\n[Bash] ls",
actions: [
{
index: 1,
toolName: "Read",
summary: "README.md",
},
{
index: 2,
toolName: "Bash",
summary: "ls",
},
],
}); });
const display = buildToolCallDisplayModel({ const display = buildToolCallDisplayModel({
@@ -49,9 +49,7 @@ function hasMeaningfulWorktreeSetupDetail(
function hasMeaningfulSubAgentDetail( function hasMeaningfulSubAgentDetail(
detail: Extract<ToolCallDetail, { type: "sub_agent" }>, detail: Extract<ToolCallDetail, { type: "sub_agent" }>,
): boolean { ): boolean {
return Boolean( return Boolean(detail.subAgentType || detail.description || detail.log);
detail.subAgentType || detail.description || detail.log || detail.actions.length > 0,
);
} }
function hasMeaningfulPlainTextDetail( function hasMeaningfulPlainTextDetail(
@@ -48,13 +48,6 @@ describe("tool-call-display", () => {
subAgentType: "Explore", subAgentType: "Explore",
description: "Inspect repository structure", description: "Inspect repository structure",
log: "[Read] README.md", log: "[Read] README.md",
actions: [
{
index: 1,
toolName: "Read",
summary: "README.md",
},
],
}, },
}); });
@@ -23,7 +23,6 @@ describe("tool-call-icon", () => {
subAgentType: "Explore", subAgentType: "Explore",
description: "Inspect repository", description: "Inspect repository",
log: "[Read] README.md", log: "[Read] README.md",
actions: [{ index: 1, toolName: "Read", summary: "README.md" }],
}); });
expect(icon).toBe(iconMocks.Bot); expect(icon).toBe(iconMocks.Bot);
@@ -153,13 +153,6 @@ second line'`,
subAgentType: "Explore", subAgentType: "Explore",
description: "Investigate repository", description: "Investigate repository",
log: "[Read] README.md", log: "[Read] README.md",
actions: [
{
index: 1,
toolName: "Read",
summary: "README.md",
},
],
}, },
}), }),
toolCallItem({ toolCallItem({
@@ -171,18 +164,6 @@ second line'`,
subAgentType: "Explore", subAgentType: "Explore",
description: "Investigate repository", description: "Investigate repository",
log: "[Read] README.md\n[Bash] ls", log: "[Read] README.md\n[Bash] ls",
actions: [
{
index: 1,
toolName: "Read",
summary: "README.md",
},
{
index: 2,
toolName: "Bash",
summary: "ls",
},
],
}, },
}), }),
]; ];
@@ -237,35 +218,3 @@ second line'`,
expect(curateAgentActivity([])).toBe("No activity to display."); expect(curateAgentActivity([])).toBe("No activity to display.");
}); });
}); });
describe("curateAgentActivityActions", () => {
it("converts curated activity lines into sub-agent action entries", async () => {
const { curateAgentActivityActions } = await import("./activity-curator.js");
const timeline: AgentTimelineItem[] = [
{ type: "assistant_message", text: "Inspecting the repo" },
toolCallItem({
callId: "shell-1",
name: "shell",
detail: {
type: "shell",
command: "npm test",
output: "ok",
exitCode: 0,
},
}),
];
expect(curateAgentActivityActions(timeline)).toEqual([
{
index: 1,
toolName: "Assistant",
summary: "Inspecting the repo",
},
{
index: 2,
toolName: "Shell",
summary: "npm test",
},
]);
});
});
@@ -7,14 +7,13 @@ const DEFAULT_MAX_ITEMS = 0;
const MAX_TOOL_INPUT_CHARS = 400; const MAX_TOOL_INPUT_CHARS = 400;
const MAX_TOOL_SUMMARY_CHARS = 200; const MAX_TOOL_SUMMARY_CHARS = 200;
interface ActivityAction { interface ActivityCuratorOptions {
toolName: string; maxItems?: number;
summary?: string; labelAssistantMessages?: boolean;
} }
interface ActivityEntry { interface ActivityEntry {
text: string; text: string;
action: ActivityAction;
} }
function appendText(buffer: string, text: string): string { function appendText(buffer: string, text: string): string {
@@ -28,24 +27,22 @@ function appendText(buffer: string, text: string): string {
return `${buffer}\n${normalized}`; return `${buffer}\n${normalized}`;
} }
function activityEntry(text: string, toolName: string, summary?: string): ActivityEntry { function activityEntry(text: string): ActivityEntry {
return { return { text };
text,
action: {
toolName,
...(summary ? { summary } : {}),
},
};
} }
function flushBuffers(entries: ActivityEntry[], buffers: { message: string; thought: string }) { function flushBuffers(
entries: ActivityEntry[],
buffers: { message: string; thought: string },
options?: ActivityCuratorOptions,
) {
if (buffers.message.trim()) { if (buffers.message.trim()) {
const text = buffers.message.trim(); const text = buffers.message.trim();
entries.push(activityEntry(text, "Assistant", text)); entries.push(activityEntry(options?.labelAssistantMessages ? `[Assistant] ${text}` : text));
} }
if (buffers.thought.trim()) { if (buffers.thought.trim()) {
const text = buffers.thought.trim(); const text = buffers.thought.trim();
entries.push(activityEntry(`[Thought] ${text}`, "Thought", text)); entries.push(activityEntry(`[Thought] ${text}`));
} }
buffers.message = ""; buffers.message = "";
buffers.thought = ""; buffers.thought = "";
@@ -100,7 +97,7 @@ function projectForCuration(items: readonly AgentTimelineItem[]): AgentTimelineI
function curateAgentActivityEntries( function curateAgentActivityEntries(
timeline: AgentTimelineItem[], timeline: AgentTimelineItem[],
options?: { maxItems?: number }, options?: ActivityCuratorOptions,
): ActivityEntry[] { ): ActivityEntry[] {
if (timeline.length === 0) { if (timeline.length === 0) {
return []; return [];
@@ -118,8 +115,8 @@ function curateAgentActivityEntries(
for (const item of recentItems) { for (const item of recentItems) {
switch (item.type) { switch (item.type) {
case "user_message": case "user_message":
flushBuffers(entries, buffers); flushBuffers(entries, buffers, options);
entries.push(activityEntry(`[User] ${item.text.trim()}`, "User", item.text.trim())); entries.push(activityEntry(`[User] ${item.text.trim()}`));
break; break;
case "assistant_message": case "assistant_message":
buffers.message = appendText(buffers.message, item.text); buffers.message = appendText(buffers.message, item.text);
@@ -128,7 +125,7 @@ function curateAgentActivityEntries(
buffers.thought = appendText(buffers.thought, item.text); buffers.thought = appendText(buffers.thought, item.text);
break; break;
case "tool_call": { case "tool_call": {
flushBuffers(entries, buffers); flushBuffers(entries, buffers, options);
const inputJson = formatToolInputJson(inputFromUnknownDetail(item.detail)); const inputJson = formatToolInputJson(inputFromUnknownDetail(item.detail));
const display = buildToolCallDisplayModel({ const display = buildToolCallDisplayModel({
name: item.name, name: item.name,
@@ -140,37 +137,37 @@ function curateAgentActivityEntries(
const displayName = display.displayName; const displayName = display.displayName;
const summary = formatToolSummary(display.summary); const summary = formatToolSummary(display.summary);
if (isLikelyExternalToolName(item.name) && inputJson) { if (isLikelyExternalToolName(item.name) && inputJson) {
entries.push(activityEntry(`[${displayName}] ${inputJson}`, displayName, inputJson)); entries.push(activityEntry(`[${displayName}] ${inputJson}`));
break; break;
} }
if (summary) { if (summary) {
entries.push(activityEntry(`[${displayName}] ${summary}`, displayName, summary)); entries.push(activityEntry(`[${displayName}] ${summary}`));
} else { } else {
entries.push(activityEntry(`[${displayName}]`, displayName)); entries.push(activityEntry(`[${displayName}]`));
} }
break; break;
} }
case "todo": case "todo":
flushBuffers(entries, buffers); flushBuffers(entries, buffers, options);
entries.push(activityEntry("[Tasks]", "Tasks")); entries.push(activityEntry("[Tasks]"));
for (const entry of item.items) { for (const entry of item.items) {
const checkbox = entry.completed ? "[x]" : "[ ]"; const checkbox = entry.completed ? "[x]" : "[ ]";
const text = `- ${checkbox} ${entry.text}`; const text = `- ${checkbox} ${entry.text}`;
entries.push(activityEntry(text, "Assistant", text)); entries.push(activityEntry(text));
} }
break; break;
case "error": case "error":
flushBuffers(entries, buffers); flushBuffers(entries, buffers, options);
entries.push(activityEntry(`[Error] ${item.message}`, "Error", item.message)); entries.push(activityEntry(`[Error] ${item.message}`));
break; break;
case "compaction": case "compaction":
flushBuffers(entries, buffers); flushBuffers(entries, buffers, options);
entries.push(activityEntry("[Compacted]", "Compacted")); entries.push(activityEntry("[Compacted]"));
break; break;
} }
} }
flushBuffers(entries, buffers); flushBuffers(entries, buffers, options);
return entries; return entries;
} }
@@ -180,19 +177,10 @@ function curateAgentActivityEntries(
*/ */
export function curateAgentActivity( export function curateAgentActivity(
timeline: AgentTimelineItem[], timeline: AgentTimelineItem[],
options?: { maxItems?: number }, options?: ActivityCuratorOptions,
): string { ): string {
const entries = curateAgentActivityEntries(timeline, options); const entries = curateAgentActivityEntries(timeline, options);
return entries.length > 0 return entries.length > 0
? entries.map((entry) => entry.text).join("\n") ? entries.map((entry) => entry.text).join("\n")
: "No activity to display."; : "No activity to display.";
} }
export function curateAgentActivityActions(
timeline: AgentTimelineItem[],
options?: { maxItems?: number },
): Array<{ index: number; toolName: string; summary?: string }> {
return curateAgentActivityEntries(timeline, options).map((entry, index) =>
Object.assign({ index: index + 1 }, entry.action),
);
}
@@ -237,11 +237,6 @@ export type ToolCallDetail =
description?: string; description?: string;
childSessionId?: string; childSessionId?: string;
log: string; log: string;
actions: Array<{
index: number;
toolName: string;
summary?: string;
}>;
} }
| { | {
type: "plain_text"; type: "plain_text";
@@ -280,18 +280,6 @@ describe("ClaudeAgentSession sub-agent sidechain updates", () => {
expect(latest.detail.subAgentType).toBe("Explore"); expect(latest.detail.subAgentType).toBe("Explore");
expect(latest.detail.description).toBe("Inspect repository structure"); expect(latest.detail.description).toBe("Inspect repository structure");
expect(latest.detail.actions).toEqual([
{
index: 1,
toolName: "Read",
summary: "README.md",
},
{
index: 2,
toolName: "Edit",
summary: "src/index.ts",
},
]);
expect(latest.detail.log).toContain("[Read] README.md"); expect(latest.detail.log).toContain("[Read] README.md");
expect(latest.detail.log).toContain("[Edit] src/index.ts"); expect(latest.detail.log).toContain("[Edit] src/index.ts");
expect(latest.detail.log).not.toContain("VERY_LARGE_OLD_STRING"); expect(latest.detail.log).not.toContain("VERY_LARGE_OLD_STRING");
@@ -334,18 +322,6 @@ describe("ClaudeAgentSession sub-agent sidechain updates", () => {
throw new Error("expected sub_agent detail"); throw new Error("expected sub_agent detail");
} }
expect(latest.detail.actions).toHaveLength(200);
expect(latest.detail.actions[0]).toEqual({
index: 6,
toolName: "Read",
summary: "file-6.md",
});
expect(latest.detail.actions[199]).toEqual({
index: 205,
toolName: "Read",
summary: "file-205.md",
});
expect(latest.detail.log).not.toContain("[Read] file-1.md"); expect(latest.detail.log).not.toContain("[Read] file-1.md");
expect(latest.detail.log).not.toContain("[Read] file-5.md"); expect(latest.detail.log).not.toContain("[Read] file-5.md");
expect(latest.detail.log).toContain("[Read] file-6.md"); expect(latest.detail.log).toContain("[Read] file-6.md");
@@ -99,12 +99,6 @@ export class ClaudeSidechainTracker {
action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`, action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`,
) )
.join("\n"), .join("\n"),
actions: state.actions.map((action) =>
Object.assign(
{ index: action.index, toolName: action.toolName },
action.summary ? { summary: action.summary } : {},
),
),
}; };
return [ return [
@@ -30,6 +30,7 @@ interface CollaborationModeRecord {
interface CodexSessionTestAccess { interface CodexSessionTestAccess {
handleToolApprovalRequest(params: unknown): Promise<unknown>; handleToolApprovalRequest(params: unknown): Promise<unknown>;
handleNotification(method: string, params: unknown): void; handleNotification(method: string, params: unknown): void;
loadPersistedHistory(): Promise<void>;
refreshResolvedCollaborationMode(): void; refreshResolvedCollaborationMode(): void;
serviceTier: "fast" | null; serviceTier: "fast" | null;
planModeEnabled: boolean; planModeEnabled: boolean;
@@ -705,7 +706,6 @@ describe("Codex app-server provider", () => {
subAgentType: "Sub-agent", subAgentType: "Sub-agent",
description: "Inspect the stream path.", description: "Inspect the stream path.",
log: "", log: "",
actions: [],
}, },
}, },
}, },
@@ -762,12 +762,60 @@ describe("Codex app-server provider", () => {
type: "sub_agent", type: "sub_agent",
subAgentType: "Sub-agent", subAgentType: "Sub-agent",
description: "Report findings.", description: "Report findings.",
log: "Found the path.", log: "[Assistant] Found the path.",
actions: [{ index: 1, toolName: "Assistant", summary: "Found the path." }],
}, },
}); });
}); });
test("loads Codex persisted history from the app-server thread", async () => {
const session = createSession();
const requests: Array<{ method: string; params: unknown }> = [];
session.client = {
request: vi.fn(async (method: string, params: unknown) => {
requests.push({ method, params });
if (method !== "thread/read") {
return {};
}
return {
thread: {
turns: [
{
items: [
{
type: "agentMessage",
id: "message-history",
text: "History loaded.",
},
],
},
],
},
};
}),
};
await asInternals(session).loadPersistedHistory();
const history: AgentStreamEvent[] = [];
for await (const event of session.streamHistory()) {
history.push(event);
}
expect(requests.map((request) => [request.method, request.params])).toEqual([
["thread/read", { threadId: "test-thread", includeTurns: true }],
]);
expect(history).toEqual([
{
type: "timeline",
provider: "codex",
item: {
type: "assistant_message",
text: "History loaded.",
},
},
]);
});
test("maps question responses from headers back to question ids and completes the tool call", async () => { test("maps question responses from headers back to question ids and completes the tool call", async () => {
const session = createSession(); const session = createSession();
const events: AgentStreamEvent[] = []; const events: AgentStreamEvent[] = [];
@@ -37,9 +37,8 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import readline from "node:readline"; import readline from "node:readline";
import { z } from "zod"; import { z } from "zod";
import { loadCodexPersistedTimeline } from "./codex-rollout-timeline.js";
import { renderPromptAttachmentAsText } from "../prompt-attachments.js"; import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
import { curateAgentActivity, curateAgentActivityActions } from "../activity-curator.js"; import { curateAgentActivity } from "../activity-curator.js";
import { import {
mapCodexRolloutToolCall, mapCodexRolloutToolCall,
mapCodexToolCallFromThreadItem, mapCodexToolCallFromThreadItem,
@@ -1557,6 +1556,61 @@ function threadItemToTimeline(
} }
} }
const CodexThreadReadResponseSchema = z
.object({
thread: z
.object({
turns: z
.array(
z
.object({
items: z.array(z.unknown()).default([]),
})
.passthrough(),
)
.default([]),
})
.passthrough()
.default({ turns: [] }),
})
.passthrough();
type CodexThreadReadResponse = z.infer<typeof CodexThreadReadResponseSchema>;
type CodexThreadReadRequest = (threadId: string) => Promise<unknown>;
async function requestCodexThreadHistory(
requestThread: CodexThreadReadRequest,
threadId: string,
): Promise<CodexThreadReadResponse> {
const response = await requestThread(threadId);
return CodexThreadReadResponseSchema.parse(response);
}
async function loadCodexThreadHistoryTimeline(params: {
threadId: string;
cwd: string | null;
requestThread: CodexThreadReadRequest;
}): Promise<AgentTimelineItem[]> {
const response = await requestCodexThreadHistory(params.requestThread, params.threadId);
const timeline: AgentTimelineItem[] = [];
for (const turn of response.thread.turns) {
for (const item of turn.items) {
const timelineItem = threadItemToTimeline(item, { cwd: params.cwd });
if (timelineItem) {
timeline.push(timelineItem);
}
}
}
return timeline;
}
function readCodexThread(client: CodexAppServerClient, threadId: string): Promise<unknown> {
return client.request("thread/read", {
threadId,
includeTurns: true,
});
}
function toSandboxPolicy(type: string, networkAccess?: boolean): Record<string, unknown> { function toSandboxPolicy(type: string, networkAccess?: boolean): Record<string, unknown> {
switch (type) { switch (type) {
case "read-only": case "read-only":
@@ -2662,8 +2716,8 @@ class CodexAppServerAgentSession implements AgentSession {
await this.loadSkills(); await this.loadSkills();
if (this.currentThreadId) { if (this.currentThreadId) {
await this.loadPersistedHistory();
await this.ensureThreadLoaded(); await this.ensureThreadLoaded();
await this.loadPersistedHistory();
} }
this.connected = true; this.connected = true;
@@ -2867,33 +2921,17 @@ class CodexAppServerAgentSession implements AgentSession {
private async loadPersistedHistory(): Promise<void> { private async loadPersistedHistory(): Promise<void> {
if (!this.client || !this.currentThreadId) return; if (!this.client || !this.currentThreadId) return;
const client = this.client;
const threadId = this.currentThreadId;
try { try {
let rolloutTimeline: AgentTimelineItem[] = []; const timeline = await loadCodexThreadHistoryTimeline({
try { threadId,
rolloutTimeline = await loadCodexPersistedTimeline( cwd: this.config.cwd ?? null,
this.currentThreadId, requestThread: (threadIdToRead) => {
undefined, return readCodexThread(client, threadIdToRead);
this.logger, },
); });
} catch {
rolloutTimeline = [];
}
const response = (await this.client.request("thread/read", {
threadId: this.currentThreadId,
includeTurns: true,
})) as { thread?: { turns?: Array<{ items?: unknown[] }> } };
const thread = response?.thread;
const threadTimeline: AgentTimelineItem[] = [];
if (thread && Array.isArray(thread.turns)) {
for (const turn of thread.turns) {
const items = Array.isArray(turn.items) ? turn.items : [];
this.collectThreadTurnTimelineItems(items, threadTimeline);
}
}
const timeline = rolloutTimeline.length > 0 ? rolloutTimeline : threadTimeline;
if (timeline.length > 0) { if (timeline.length > 0) {
this.persistedHistory = timeline; this.persistedHistory = timeline;
this.historyPending = true; this.historyPending = true;
@@ -3625,11 +3663,6 @@ class CodexAppServerAgentSession implements AgentSession {
childItems: new Map<string, AgentTimelineItem>(), childItems: new Map<string, AgentTimelineItem>(),
} satisfies CodexSubAgentCallState); } satisfies CodexSubAgentCallState);
let actions = timelineItem.detail.actions;
if (actions.length === 0 && state.toolCall.detail.type === "sub_agent") {
actions = state.toolCall.detail.actions;
}
state.toolCall = { state.toolCall = {
...timelineItem, ...timelineItem,
detail: { detail: {
@@ -3637,7 +3670,6 @@ class CodexAppServerAgentSession implements AgentSession {
log: log:
timelineItem.detail.log || timelineItem.detail.log ||
(state.toolCall.detail.type === "sub_agent" ? state.toolCall.detail.log : ""), (state.toolCall.detail.type === "sub_agent" ? state.toolCall.detail.log : ""),
actions,
}, },
}; };
this.subAgentCallsByCallId.set(timelineItem.callId, state); this.subAgentCallsByCallId.set(timelineItem.callId, state);
@@ -3676,15 +3708,16 @@ class CodexAppServerAgentSession implements AgentSession {
return; return;
} }
const childTimeline = this.getSubAgentChildTimeline(state); const childTimeline = this.getSubAgentChildTimeline(state);
const log = childTimeline.length > 0 ? curateAgentActivity(childTimeline) : ""; const log =
const actions = childTimeline.length > 0 ? curateAgentActivityActions(childTimeline) : []; childTimeline.length > 0
? curateAgentActivity(childTimeline, { labelAssistantMessages: true })
: "";
const resolvedStatus = status ?? state.toolCall.status; const resolvedStatus = status ?? state.toolCall.status;
const baseToolCall = { const baseToolCall = {
...state.toolCall, ...state.toolCall,
detail: { detail: {
...state.toolCall.detail, ...state.toolCall.detail,
log, log,
actions,
}, },
}; };
const nextToolCall: ToolCallTimelineItem = const nextToolCall: ToolCallTimelineItem =
@@ -4284,21 +4317,6 @@ class CodexAppServerAgentSession implements AgentSession {
return true; return true;
} }
private collectThreadTurnTimelineItems(items: unknown[], target: AgentTimelineItem[]): void {
for (const item of items) {
const timelineItem = threadItemToTimeline(item, {
cwd: this.config.cwd ?? null,
});
if (!timelineItem) {
continue;
}
if (timelineItem.type === "tool_call") {
this.warnOnIncompleteEditToolCall(timelineItem, "thread_read", item);
}
target.push(timelineItem);
}
}
private warnOnIncompleteEditToolCall( private warnOnIncompleteEditToolCall(
item: ToolCallTimelineItem, item: ToolCallTimelineItem,
source: string, source: string,
@@ -4549,23 +4567,15 @@ export class CodexAppServerAgentClient implements AgentClient {
const cwd = typeof thread.cwd === "string" ? thread.cwd : process.cwd(); const cwd = typeof thread.cwd === "string" ? thread.cwd : process.cwd();
const title = typeof thread.preview === "string" ? thread.preview : null; const title = typeof thread.preview === "string" ? thread.preview : null;
let timeline: AgentTimelineItem[] = []; let timeline: AgentTimelineItem[] = [];
try { try {
const [rolloutTimeline, read] = await Promise.all([ timeline = await loadCodexThreadHistoryTimeline({
loadCodexPersistedTimeline(threadId, undefined, this.logger), threadId,
client.request("thread/read", { cwd,
threadId, requestThread: (threadIdToRead) => {
includeTurns: true, return readCodexThread(client, threadIdToRead);
}) as Promise<{ thread?: { turns?: Array<{ items?: unknown[] }> } }>, },
]); });
const turns = read.thread?.turns ?? [];
const itemsFromThreadRead: AgentTimelineItem[] = [];
for (const turn of turns) {
for (const item of turn.items ?? []) {
const timelineItem = threadItemToTimeline(item, { cwd });
if (timelineItem) itemsFromThreadRead.push(timelineItem);
}
}
timeline = rolloutTimeline.length > 0 ? rolloutTimeline : itemsFromThreadRead;
} catch { } catch {
timeline = []; timeline = [];
} }
@@ -174,7 +174,6 @@ describe("codex tool-call mapper", () => {
subAgentType: "Sub-agent", subAgentType: "Sub-agent",
description: "Inspect the Codex stream path.", description: "Inspect the Codex stream path.",
log: "", log: "",
actions: [],
}, },
}); });
}); });
@@ -851,7 +851,6 @@ function mapCollabAgentToolCallItem(
subAgentType: "Sub-agent", subAgentType: "Sub-agent",
...(item.prompt ? { description: item.prompt } : {}), ...(item.prompt ? { description: item.prompt } : {}),
log: "", log: "",
actions: [],
}; };
if (status === "failed") { if (status === "failed") {
@@ -1667,11 +1667,6 @@ function buildOpenCodeSubAgentTimelineItem(
...toolCall.detail, ...toolCall.detail,
...(childSessionId ? { childSessionId } : {}), ...(childSessionId ? { childSessionId } : {}),
log: buildOpenCodeSubAgentLog(toolCall.detail, activity), log: buildOpenCodeSubAgentLog(toolCall.detail, activity),
actions: activity.actions.map((action) => ({
index: action.index,
toolName: action.toolName,
...(action.summary ? { summary: action.summary } : {}),
})),
}, },
}; };
} }
@@ -111,7 +111,6 @@ function deriveOpencodeTaskDetail(
...(description ? { description } : {}), ...(description ? { description } : {}),
...(childSessionId ? { childSessionId } : {}), ...(childSessionId ? { childSessionId } : {}),
log, log,
actions: [],
}; };
} }
@@ -271,7 +271,6 @@ describe("opencode tool-call mapper", () => {
subAgentType: "explore", subAgentType: "explore",
description: "Explore agent-tools codebase", description: "Explore agent-tools codebase",
log: "", log: "",
actions: [],
}); });
}); });
@@ -296,7 +295,6 @@ describe("opencode tool-call mapper", () => {
subAgentType: "explore", subAgentType: "explore",
description: "Explore agent-tools codebase", description: "Explore agent-tools codebase",
log: "Found the CLI entrypoint and provider registry.", log: "Found the CLI entrypoint and provider registry.",
actions: [],
}); });
}); });
@@ -320,7 +318,6 @@ describe("opencode tool-call mapper", () => {
description: "Explore current directory", description: "Explore current directory",
childSessionId: "ses_2268db431ffe299vL1bbot8R7Z", childSessionId: "ses_2268db431ffe299vL1bbot8R7Z",
log: "task_id: ses_2268db431ffe299vL1bbot8R7Z\n\n<task_result>done</task_result>", log: "task_id: ses_2268db431ffe299vL1bbot8R7Z\n\n<task_result>done</task_result>",
actions: [],
}); });
}); });
@@ -346,7 +343,6 @@ describe("opencode tool-call mapper", () => {
subAgentType: "explore", subAgentType: "explore",
description: "Explore agent-tools codebase", description: "Explore agent-tools codebase",
log: "Tool execution aborted", log: "Tool execution aborted",
actions: [],
}); });
}); });
@@ -179,13 +179,6 @@ describe("shared messages stream parsing", () => {
subAgentType: "Explore", subAgentType: "Explore",
description: "Inspect repository structure", description: "Inspect repository structure",
log: "[Read] README.md", log: "[Read] README.md",
actions: [
{
index: 1,
toolName: "Read",
summary: "README.md",
},
],
}, },
error: null, error: null,
}, },
@@ -122,18 +122,6 @@ describe("shared messages tool_call schema", () => {
subAgentType: "Explore", subAgentType: "Explore",
description: "Inspect repository structure", description: "Inspect repository structure",
log: "[Read] README.md\n[Bash] ls", log: "[Read] README.md\n[Bash] ls",
actions: [
{
index: 1,
toolName: "Read",
summary: "README.md",
},
{
index: 2,
toolName: "Bash",
summary: "ls",
},
],
}, },
}); });
@@ -142,12 +130,11 @@ describe("shared messages tool_call schema", () => {
expect(parsed.detail.type).toBe("sub_agent"); expect(parsed.detail.type).toBe("sub_agent");
if (parsed.detail.type === "sub_agent") { if (parsed.detail.type === "sub_agent") {
expect(parsed.detail.subAgentType).toBe("Explore"); expect(parsed.detail.subAgentType).toBe("Explore");
expect(parsed.detail.actions).toHaveLength(2);
} }
} }
}); });
it("defaults missing sub_agent actions for older payloads", () => { it("parses sub_agent detail without structured actions", () => {
const parsed = AgentTimelineItemPayloadSchema.parse({ const parsed = AgentTimelineItemPayloadSchema.parse({
type: "tool_call", type: "tool_call",
callId: "call_sub_agent_legacy", callId: "call_sub_agent_legacy",
@@ -166,7 +153,7 @@ describe("shared messages tool_call schema", () => {
if (parsed.type === "tool_call") { if (parsed.type === "tool_call") {
expect(parsed.detail.type).toBe("sub_agent"); expect(parsed.detail.type).toBe("sub_agent");
if (parsed.detail.type === "sub_agent") { if (parsed.detail.type === "sub_agent") {
expect(parsed.detail.actions).toEqual([]); expect(parsed.detail.log).toBe("[Read] README.md");
} }
} }
}); });
-10
View File
@@ -414,16 +414,6 @@ const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail, z.ZodTypeDef, unkno
description: z.string().optional(), description: z.string().optional(),
childSessionId: z.string().optional(), childSessionId: z.string().optional(),
log: z.string(), log: z.string(),
actions: z
.array(
z.object({
index: z.number().int().positive(),
toolName: z.string(),
summary: z.string().optional(),
}),
)
.optional()
.default([]),
}), }),
z.object({ z.object({
type: z.literal("plain_text"), type: z.literal("plain_text"),
@@ -48,13 +48,6 @@ describe("shared tool-call display mapping", () => {
subAgentType: "Explore", subAgentType: "Explore",
description: "Inspect repository structure", description: "Inspect repository structure",
log: "[Read] README.md", log: "[Read] README.md",
actions: [
{
index: 1,
toolName: "Read",
summary: "README.md",
},
],
}, },
}); });