Render sub-agent activity from log
This commit is contained in:
@@ -244,31 +244,62 @@ function resolveSubAgentFallbackHeader(
|
||||
|
||||
interface SubAgentDetailProps {
|
||||
log: string;
|
||||
actions: Extract<ToolCallDetail, { type: "sub_agent" }>["actions"];
|
||||
childSessionId: string | null | undefined;
|
||||
subAgentType: string | null | undefined;
|
||||
description: string | null | undefined;
|
||||
ds: DetailStyles;
|
||||
}
|
||||
|
||||
function buildSubAgentActionLog(actions: SubAgentDetailProps["actions"]): string {
|
||||
return actions
|
||||
.map((action) =>
|
||||
action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`,
|
||||
)
|
||||
.join("\n");
|
||||
interface SubAgentActivityRow {
|
||||
index: number;
|
||||
toolName: string;
|
||||
summary?: string;
|
||||
}
|
||||
|
||||
function stripSubAgentActionLog(log: string, actions: SubAgentDetailProps["actions"]): string {
|
||||
const actionLog = buildSubAgentActionLog(actions);
|
||||
const trimmedLog = log.replace(/^\n+/, "");
|
||||
if (!actionLog || !trimmedLog.startsWith(actionLog)) {
|
||||
return trimmedLog;
|
||||
interface ParsedSubAgentLog {
|
||||
actions: SubAgentActivityRow[];
|
||||
remainingLog: string;
|
||||
}
|
||||
|
||||
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 (
|
||||
<View style={styles.subAgentActionRow}>
|
||||
<Text selectable style={styles.subAgentActionTool}>
|
||||
@@ -324,13 +355,12 @@ function SubAgentLogText({
|
||||
|
||||
function SubAgentDetailSection({
|
||||
log,
|
||||
actions,
|
||||
childSessionId,
|
||||
subAgentType,
|
||||
description,
|
||||
ds,
|
||||
}: SubAgentDetailProps) {
|
||||
const activityLog = stripSubAgentActionLog(log, actions);
|
||||
const { actions, remainingLog } = useMemo(() => parseSubAgentLog(log), [log]);
|
||||
const fallbackHeader = resolveSubAgentFallbackHeader(subAgentType, description);
|
||||
const hasActions = actions.length > 0;
|
||||
return (
|
||||
@@ -363,7 +393,7 @@ function SubAgentDetailSection({
|
||||
</View>
|
||||
) : null}
|
||||
<SubAgentLogText
|
||||
activityLog={activityLog}
|
||||
activityLog={remainingLog}
|
||||
fallbackHeader={fallbackHeader}
|
||||
hasActions={hasActions}
|
||||
/>
|
||||
@@ -624,7 +654,6 @@ function buildDetailSections(
|
||||
<SubAgentDetailSection
|
||||
key="sub-agent"
|
||||
log={detail.log}
|
||||
actions={detail.actions}
|
||||
childSessionId={detail.childSessionId}
|
||||
subAgentType={detail.subAgentType}
|
||||
description={detail.description}
|
||||
@@ -844,7 +873,6 @@ const styles = StyleSheet.create((theme) => {
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
subAgentActionTool: {
|
||||
minWidth: 56,
|
||||
fontFamily: Fonts.mono,
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foregroundMuted,
|
||||
|
||||
@@ -367,18 +367,6 @@ describe("stream reducer canonical tool calls", () => {
|
||||
subAgentType: "Explore",
|
||||
description: "Inspect repository structure",
|
||||
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"),
|
||||
@@ -406,18 +394,6 @@ describe("stream reducer canonical tool calls", () => {
|
||||
subAgentType: "Explore",
|
||||
description: "Inspect repository structure",
|
||||
log: "[Read] README.md\n[Bash] ls",
|
||||
actions: [
|
||||
{
|
||||
index: 1,
|
||||
toolName: "Read",
|
||||
summary: "README.md",
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
toolName: "Bash",
|
||||
summary: "ls",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const display = buildToolCallDisplayModel({
|
||||
|
||||
@@ -49,9 +49,7 @@ function hasMeaningfulWorktreeSetupDetail(
|
||||
function hasMeaningfulSubAgentDetail(
|
||||
detail: Extract<ToolCallDetail, { type: "sub_agent" }>,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
detail.subAgentType || detail.description || detail.log || detail.actions.length > 0,
|
||||
);
|
||||
return Boolean(detail.subAgentType || detail.description || detail.log);
|
||||
}
|
||||
|
||||
function hasMeaningfulPlainTextDetail(
|
||||
|
||||
@@ -48,13 +48,6 @@ describe("tool-call-display", () => {
|
||||
subAgentType: "Explore",
|
||||
description: "Inspect repository structure",
|
||||
log: "[Read] README.md",
|
||||
actions: [
|
||||
{
|
||||
index: 1,
|
||||
toolName: "Read",
|
||||
summary: "README.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ describe("tool-call-icon", () => {
|
||||
subAgentType: "Explore",
|
||||
description: "Inspect repository",
|
||||
log: "[Read] README.md",
|
||||
actions: [{ index: 1, toolName: "Read", summary: "README.md" }],
|
||||
});
|
||||
|
||||
expect(icon).toBe(iconMocks.Bot);
|
||||
|
||||
@@ -153,13 +153,6 @@ second line'`,
|
||||
subAgentType: "Explore",
|
||||
description: "Investigate repository",
|
||||
log: "[Read] README.md",
|
||||
actions: [
|
||||
{
|
||||
index: 1,
|
||||
toolName: "Read",
|
||||
summary: "README.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
toolCallItem({
|
||||
@@ -171,18 +164,6 @@ second line'`,
|
||||
subAgentType: "Explore",
|
||||
description: "Investigate repository",
|
||||
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.");
|
||||
});
|
||||
});
|
||||
|
||||
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_SUMMARY_CHARS = 200;
|
||||
|
||||
interface ActivityAction {
|
||||
toolName: string;
|
||||
summary?: string;
|
||||
interface ActivityCuratorOptions {
|
||||
maxItems?: number;
|
||||
labelAssistantMessages?: boolean;
|
||||
}
|
||||
|
||||
interface ActivityEntry {
|
||||
text: string;
|
||||
action: ActivityAction;
|
||||
}
|
||||
|
||||
function appendText(buffer: string, text: string): string {
|
||||
@@ -28,24 +27,22 @@ function appendText(buffer: string, text: string): string {
|
||||
return `${buffer}\n${normalized}`;
|
||||
}
|
||||
|
||||
function activityEntry(text: string, toolName: string, summary?: string): ActivityEntry {
|
||||
return {
|
||||
text,
|
||||
action: {
|
||||
toolName,
|
||||
...(summary ? { summary } : {}),
|
||||
},
|
||||
};
|
||||
function activityEntry(text: string): ActivityEntry {
|
||||
return { text };
|
||||
}
|
||||
|
||||
function flushBuffers(entries: ActivityEntry[], buffers: { message: string; thought: string }) {
|
||||
function flushBuffers(
|
||||
entries: ActivityEntry[],
|
||||
buffers: { message: string; thought: string },
|
||||
options?: ActivityCuratorOptions,
|
||||
) {
|
||||
if (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()) {
|
||||
const text = buffers.thought.trim();
|
||||
entries.push(activityEntry(`[Thought] ${text}`, "Thought", text));
|
||||
entries.push(activityEntry(`[Thought] ${text}`));
|
||||
}
|
||||
buffers.message = "";
|
||||
buffers.thought = "";
|
||||
@@ -100,7 +97,7 @@ function projectForCuration(items: readonly AgentTimelineItem[]): AgentTimelineI
|
||||
|
||||
function curateAgentActivityEntries(
|
||||
timeline: AgentTimelineItem[],
|
||||
options?: { maxItems?: number },
|
||||
options?: ActivityCuratorOptions,
|
||||
): ActivityEntry[] {
|
||||
if (timeline.length === 0) {
|
||||
return [];
|
||||
@@ -118,8 +115,8 @@ function curateAgentActivityEntries(
|
||||
for (const item of recentItems) {
|
||||
switch (item.type) {
|
||||
case "user_message":
|
||||
flushBuffers(entries, buffers);
|
||||
entries.push(activityEntry(`[User] ${item.text.trim()}`, "User", item.text.trim()));
|
||||
flushBuffers(entries, buffers, options);
|
||||
entries.push(activityEntry(`[User] ${item.text.trim()}`));
|
||||
break;
|
||||
case "assistant_message":
|
||||
buffers.message = appendText(buffers.message, item.text);
|
||||
@@ -128,7 +125,7 @@ function curateAgentActivityEntries(
|
||||
buffers.thought = appendText(buffers.thought, item.text);
|
||||
break;
|
||||
case "tool_call": {
|
||||
flushBuffers(entries, buffers);
|
||||
flushBuffers(entries, buffers, options);
|
||||
const inputJson = formatToolInputJson(inputFromUnknownDetail(item.detail));
|
||||
const display = buildToolCallDisplayModel({
|
||||
name: item.name,
|
||||
@@ -140,37 +137,37 @@ function curateAgentActivityEntries(
|
||||
const displayName = display.displayName;
|
||||
const summary = formatToolSummary(display.summary);
|
||||
if (isLikelyExternalToolName(item.name) && inputJson) {
|
||||
entries.push(activityEntry(`[${displayName}] ${inputJson}`, displayName, inputJson));
|
||||
entries.push(activityEntry(`[${displayName}] ${inputJson}`));
|
||||
break;
|
||||
}
|
||||
if (summary) {
|
||||
entries.push(activityEntry(`[${displayName}] ${summary}`, displayName, summary));
|
||||
entries.push(activityEntry(`[${displayName}] ${summary}`));
|
||||
} else {
|
||||
entries.push(activityEntry(`[${displayName}]`, displayName));
|
||||
entries.push(activityEntry(`[${displayName}]`));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "todo":
|
||||
flushBuffers(entries, buffers);
|
||||
entries.push(activityEntry("[Tasks]", "Tasks"));
|
||||
flushBuffers(entries, buffers, options);
|
||||
entries.push(activityEntry("[Tasks]"));
|
||||
for (const entry of item.items) {
|
||||
const checkbox = entry.completed ? "[x]" : "[ ]";
|
||||
const text = `- ${checkbox} ${entry.text}`;
|
||||
entries.push(activityEntry(text, "Assistant", text));
|
||||
entries.push(activityEntry(text));
|
||||
}
|
||||
break;
|
||||
case "error":
|
||||
flushBuffers(entries, buffers);
|
||||
entries.push(activityEntry(`[Error] ${item.message}`, "Error", item.message));
|
||||
flushBuffers(entries, buffers, options);
|
||||
entries.push(activityEntry(`[Error] ${item.message}`));
|
||||
break;
|
||||
case "compaction":
|
||||
flushBuffers(entries, buffers);
|
||||
entries.push(activityEntry("[Compacted]", "Compacted"));
|
||||
flushBuffers(entries, buffers, options);
|
||||
entries.push(activityEntry("[Compacted]"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
flushBuffers(entries, buffers);
|
||||
flushBuffers(entries, buffers, options);
|
||||
|
||||
return entries;
|
||||
}
|
||||
@@ -180,19 +177,10 @@ function curateAgentActivityEntries(
|
||||
*/
|
||||
export function curateAgentActivity(
|
||||
timeline: AgentTimelineItem[],
|
||||
options?: { maxItems?: number },
|
||||
options?: ActivityCuratorOptions,
|
||||
): string {
|
||||
const entries = curateAgentActivityEntries(timeline, options);
|
||||
return entries.length > 0
|
||||
? entries.map((entry) => entry.text).join("\n")
|
||||
: "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;
|
||||
childSessionId?: string;
|
||||
log: string;
|
||||
actions: Array<{
|
||||
index: number;
|
||||
toolName: string;
|
||||
summary?: string;
|
||||
}>;
|
||||
}
|
||||
| {
|
||||
type: "plain_text";
|
||||
|
||||
@@ -280,18 +280,6 @@ describe("ClaudeAgentSession sub-agent sidechain updates", () => {
|
||||
|
||||
expect(latest.detail.subAgentType).toBe("Explore");
|
||||
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("[Edit] src/index.ts");
|
||||
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");
|
||||
}
|
||||
|
||||
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-5.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}]`,
|
||||
)
|
||||
.join("\n"),
|
||||
actions: state.actions.map((action) =>
|
||||
Object.assign(
|
||||
{ index: action.index, toolName: action.toolName },
|
||||
action.summary ? { summary: action.summary } : {},
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
return [
|
||||
|
||||
@@ -30,6 +30,7 @@ interface CollaborationModeRecord {
|
||||
interface CodexSessionTestAccess {
|
||||
handleToolApprovalRequest(params: unknown): Promise<unknown>;
|
||||
handleNotification(method: string, params: unknown): void;
|
||||
loadPersistedHistory(): Promise<void>;
|
||||
refreshResolvedCollaborationMode(): void;
|
||||
serviceTier: "fast" | null;
|
||||
planModeEnabled: boolean;
|
||||
@@ -705,7 +706,6 @@ describe("Codex app-server provider", () => {
|
||||
subAgentType: "Sub-agent",
|
||||
description: "Inspect the stream path.",
|
||||
log: "",
|
||||
actions: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -762,12 +762,60 @@ describe("Codex app-server provider", () => {
|
||||
type: "sub_agent",
|
||||
subAgentType: "Sub-agent",
|
||||
description: "Report findings.",
|
||||
log: "Found the path.",
|
||||
actions: [{ index: 1, toolName: "Assistant", summary: "Found the path." }],
|
||||
log: "[Assistant] 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 () => {
|
||||
const session = createSession();
|
||||
const events: AgentStreamEvent[] = [];
|
||||
|
||||
@@ -37,9 +37,8 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
import { z } from "zod";
|
||||
import { loadCodexPersistedTimeline } from "./codex-rollout-timeline.js";
|
||||
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
||||
import { curateAgentActivity, curateAgentActivityActions } from "../activity-curator.js";
|
||||
import { curateAgentActivity } from "../activity-curator.js";
|
||||
import {
|
||||
mapCodexRolloutToolCall,
|
||||
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> {
|
||||
switch (type) {
|
||||
case "read-only":
|
||||
@@ -2662,8 +2716,8 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
await this.loadSkills();
|
||||
|
||||
if (this.currentThreadId) {
|
||||
await this.loadPersistedHistory();
|
||||
await this.ensureThreadLoaded();
|
||||
await this.loadPersistedHistory();
|
||||
}
|
||||
|
||||
this.connected = true;
|
||||
@@ -2867,33 +2921,17 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
|
||||
private async loadPersistedHistory(): Promise<void> {
|
||||
if (!this.client || !this.currentThreadId) return;
|
||||
const client = this.client;
|
||||
const threadId = this.currentThreadId;
|
||||
|
||||
try {
|
||||
let rolloutTimeline: AgentTimelineItem[] = [];
|
||||
try {
|
||||
rolloutTimeline = await loadCodexPersistedTimeline(
|
||||
this.currentThreadId,
|
||||
undefined,
|
||||
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;
|
||||
|
||||
const timeline = await loadCodexThreadHistoryTimeline({
|
||||
threadId,
|
||||
cwd: this.config.cwd ?? null,
|
||||
requestThread: (threadIdToRead) => {
|
||||
return readCodexThread(client, threadIdToRead);
|
||||
},
|
||||
});
|
||||
if (timeline.length > 0) {
|
||||
this.persistedHistory = timeline;
|
||||
this.historyPending = true;
|
||||
@@ -3625,11 +3663,6 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
childItems: new Map<string, AgentTimelineItem>(),
|
||||
} satisfies CodexSubAgentCallState);
|
||||
|
||||
let actions = timelineItem.detail.actions;
|
||||
if (actions.length === 0 && state.toolCall.detail.type === "sub_agent") {
|
||||
actions = state.toolCall.detail.actions;
|
||||
}
|
||||
|
||||
state.toolCall = {
|
||||
...timelineItem,
|
||||
detail: {
|
||||
@@ -3637,7 +3670,6 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
log:
|
||||
timelineItem.detail.log ||
|
||||
(state.toolCall.detail.type === "sub_agent" ? state.toolCall.detail.log : ""),
|
||||
actions,
|
||||
},
|
||||
};
|
||||
this.subAgentCallsByCallId.set(timelineItem.callId, state);
|
||||
@@ -3676,15 +3708,16 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
return;
|
||||
}
|
||||
const childTimeline = this.getSubAgentChildTimeline(state);
|
||||
const log = childTimeline.length > 0 ? curateAgentActivity(childTimeline) : "";
|
||||
const actions = childTimeline.length > 0 ? curateAgentActivityActions(childTimeline) : [];
|
||||
const log =
|
||||
childTimeline.length > 0
|
||||
? curateAgentActivity(childTimeline, { labelAssistantMessages: true })
|
||||
: "";
|
||||
const resolvedStatus = status ?? state.toolCall.status;
|
||||
const baseToolCall = {
|
||||
...state.toolCall,
|
||||
detail: {
|
||||
...state.toolCall.detail,
|
||||
log,
|
||||
actions,
|
||||
},
|
||||
};
|
||||
const nextToolCall: ToolCallTimelineItem =
|
||||
@@ -4284,21 +4317,6 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
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(
|
||||
item: ToolCallTimelineItem,
|
||||
source: string,
|
||||
@@ -4549,23 +4567,15 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
const cwd = typeof thread.cwd === "string" ? thread.cwd : process.cwd();
|
||||
const title = typeof thread.preview === "string" ? thread.preview : null;
|
||||
let timeline: AgentTimelineItem[] = [];
|
||||
|
||||
try {
|
||||
const [rolloutTimeline, read] = await Promise.all([
|
||||
loadCodexPersistedTimeline(threadId, undefined, this.logger),
|
||||
client.request("thread/read", {
|
||||
threadId,
|
||||
includeTurns: true,
|
||||
}) 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;
|
||||
timeline = await loadCodexThreadHistoryTimeline({
|
||||
threadId,
|
||||
cwd,
|
||||
requestThread: (threadIdToRead) => {
|
||||
return readCodexThread(client, threadIdToRead);
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
timeline = [];
|
||||
}
|
||||
|
||||
@@ -174,7 +174,6 @@ describe("codex tool-call mapper", () => {
|
||||
subAgentType: "Sub-agent",
|
||||
description: "Inspect the Codex stream path.",
|
||||
log: "",
|
||||
actions: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -851,7 +851,6 @@ function mapCollabAgentToolCallItem(
|
||||
subAgentType: "Sub-agent",
|
||||
...(item.prompt ? { description: item.prompt } : {}),
|
||||
log: "",
|
||||
actions: [],
|
||||
};
|
||||
|
||||
if (status === "failed") {
|
||||
|
||||
@@ -1667,11 +1667,6 @@ function buildOpenCodeSubAgentTimelineItem(
|
||||
...toolCall.detail,
|
||||
...(childSessionId ? { childSessionId } : {}),
|
||||
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 } : {}),
|
||||
...(childSessionId ? { childSessionId } : {}),
|
||||
log,
|
||||
actions: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -271,7 +271,6 @@ describe("opencode tool-call mapper", () => {
|
||||
subAgentType: "explore",
|
||||
description: "Explore agent-tools codebase",
|
||||
log: "",
|
||||
actions: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -296,7 +295,6 @@ describe("opencode tool-call mapper", () => {
|
||||
subAgentType: "explore",
|
||||
description: "Explore agent-tools codebase",
|
||||
log: "Found the CLI entrypoint and provider registry.",
|
||||
actions: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -320,7 +318,6 @@ describe("opencode tool-call mapper", () => {
|
||||
description: "Explore current directory",
|
||||
childSessionId: "ses_2268db431ffe299vL1bbot8R7Z",
|
||||
log: "task_id: ses_2268db431ffe299vL1bbot8R7Z\n\n<task_result>done</task_result>",
|
||||
actions: [],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -346,7 +343,6 @@ describe("opencode tool-call mapper", () => {
|
||||
subAgentType: "explore",
|
||||
description: "Explore agent-tools codebase",
|
||||
log: "Tool execution aborted",
|
||||
actions: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -179,13 +179,6 @@ describe("shared messages stream parsing", () => {
|
||||
subAgentType: "Explore",
|
||||
description: "Inspect repository structure",
|
||||
log: "[Read] README.md",
|
||||
actions: [
|
||||
{
|
||||
index: 1,
|
||||
toolName: "Read",
|
||||
summary: "README.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
error: null,
|
||||
},
|
||||
|
||||
@@ -122,18 +122,6 @@ describe("shared messages tool_call schema", () => {
|
||||
subAgentType: "Explore",
|
||||
description: "Inspect repository structure",
|
||||
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");
|
||||
if (parsed.detail.type === "sub_agent") {
|
||||
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({
|
||||
type: "tool_call",
|
||||
callId: "call_sub_agent_legacy",
|
||||
@@ -166,7 +153,7 @@ describe("shared messages tool_call schema", () => {
|
||||
if (parsed.type === "tool_call") {
|
||||
expect(parsed.detail.type).toBe("sub_agent");
|
||||
if (parsed.detail.type === "sub_agent") {
|
||||
expect(parsed.detail.actions).toEqual([]);
|
||||
expect(parsed.detail.log).toBe("[Read] README.md");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -414,16 +414,6 @@ const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail, z.ZodTypeDef, unkno
|
||||
description: z.string().optional(),
|
||||
childSessionId: z.string().optional(),
|
||||
log: z.string(),
|
||||
actions: z
|
||||
.array(
|
||||
z.object({
|
||||
index: z.number().int().positive(),
|
||||
toolName: z.string(),
|
||||
summary: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.default([]),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("plain_text"),
|
||||
|
||||
@@ -48,13 +48,6 @@ describe("shared tool-call display mapping", () => {
|
||||
subAgentType: "Explore",
|
||||
description: "Inspect repository structure",
|
||||
log: "[Read] README.md",
|
||||
actions: [
|
||||
{
|
||||
index: 1,
|
||||
toolName: "Read",
|
||||
summary: "README.md",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user