feat(opencode): add subagent timeline support (#658)
Map OpenCode `task` tool events into the existing sub_agent timeline shape, enabling live activity tracking for OpenCode subagent sessions. - Add `childSessionId` (optional) to sub_agent schema - Make `actions` optional with default [] for backward compatibility - Parse task tool input/output into sub_agent detail (tool-call-detail-parser) - Track child session linkage and buffer/flush child tool parts - Render sub_agent actions and session IDs in the app component - Add glob tool detail mapping - Include backward-compat schema test Co-authored-by: Ryan Swift <ryan@mlh.io>
This commit is contained in:
co-authored by
Ryan Swift
parent
a64be66353
commit
fdc510d3a0
@@ -244,15 +244,95 @@ function resolveSubAgentFallbackHeader(
|
|||||||
|
|
||||||
interface SubAgentDetailProps {
|
interface SubAgentDetailProps {
|
||||||
log: string;
|
log: string;
|
||||||
|
actions: Extract<ToolCallDetail, { type: "sub_agent" }>["actions"];
|
||||||
|
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 SubAgentDetailSection({ log, subAgentType, description, ds }: SubAgentDetailProps) {
|
function buildSubAgentActionLog(actions: SubAgentDetailProps["actions"]): string {
|
||||||
const activityLog = log.replace(/^\n+/, "");
|
return actions
|
||||||
const hasLog = activityLog.length > 0;
|
.map((action) =>
|
||||||
|
action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripSubAgentActionLog(log: string, actions: SubAgentDetailProps["actions"]): string {
|
||||||
|
const actionLog = buildSubAgentActionLog(actions);
|
||||||
|
const trimmedLog = log.replace(/^\n+/, "");
|
||||||
|
if (!actionLog || !trimmedLog.startsWith(actionLog)) {
|
||||||
|
return trimmedLog;
|
||||||
|
}
|
||||||
|
return trimmedLog.slice(actionLog.length).replace(/^\n+/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubAgentActionRow({ action }: { action: SubAgentDetailProps["actions"][number] }) {
|
||||||
|
return (
|
||||||
|
<View style={styles.subAgentActionRow}>
|
||||||
|
<Text selectable style={styles.subAgentActionTool}>
|
||||||
|
{formatSubAgentToolName(action.toolName)}
|
||||||
|
</Text>
|
||||||
|
{action.summary ? (
|
||||||
|
<Text selectable style={styles.subAgentActionSummary}>
|
||||||
|
{action.summary}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSubAgentToolName(toolName: string): string {
|
||||||
|
const trimmed = toolName.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return toolName;
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
.replace(/[._-]+/g, " ")
|
||||||
|
.split(" ")
|
||||||
|
.filter((segment) => segment.length > 0)
|
||||||
|
.map((segment) => `${segment[0]?.toUpperCase() ?? ""}${segment.slice(1)}`)
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubAgentLogText({
|
||||||
|
activityLog,
|
||||||
|
fallbackHeader,
|
||||||
|
hasActions,
|
||||||
|
}: {
|
||||||
|
activityLog: string;
|
||||||
|
fallbackHeader: string;
|
||||||
|
hasActions: boolean;
|
||||||
|
}) {
|
||||||
|
if (activityLog.length > 0) {
|
||||||
|
return (
|
||||||
|
<Text selectable style={styles.scrollText}>
|
||||||
|
{activityLog}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!hasActions) {
|
||||||
|
return (
|
||||||
|
<Text selectable style={styles.scrollText}>
|
||||||
|
{fallbackHeader}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubAgentDetailSection({
|
||||||
|
log,
|
||||||
|
actions,
|
||||||
|
childSessionId,
|
||||||
|
subAgentType,
|
||||||
|
description,
|
||||||
|
ds,
|
||||||
|
}: SubAgentDetailProps) {
|
||||||
|
const activityLog = stripSubAgentActionLog(log, actions);
|
||||||
const fallbackHeader = resolveSubAgentFallbackHeader(subAgentType, description);
|
const fallbackHeader = resolveSubAgentFallbackHeader(subAgentType, description);
|
||||||
|
const hasActions = actions.length > 0;
|
||||||
return (
|
return (
|
||||||
<View style={ds.sectionFillStyle}>
|
<View style={ds.sectionFillStyle}>
|
||||||
<View style={ds.codeBlockFillStyle}>
|
<View style={ds.codeBlockFillStyle}>
|
||||||
@@ -270,9 +350,23 @@ function SubAgentDetailSection({ log, subAgentType, description, ds }: SubAgentD
|
|||||||
contentContainerStyle={styles.codeHorizontalContent}
|
contentContainerStyle={styles.codeHorizontalContent}
|
||||||
>
|
>
|
||||||
<View style={styles.codeLine}>
|
<View style={styles.codeLine}>
|
||||||
<Text selectable style={styles.scrollText}>
|
{childSessionId ? (
|
||||||
{hasLog ? activityLog : fallbackHeader}
|
<Text selectable style={styles.subAgentSessionText}>
|
||||||
</Text>
|
session {childSessionId}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{hasActions ? (
|
||||||
|
<View style={styles.subAgentActions}>
|
||||||
|
{actions.map((action) => (
|
||||||
|
<SubAgentActionRow key={action.index} action={action} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
<SubAgentLogText
|
||||||
|
activityLog={activityLog}
|
||||||
|
fallbackHeader={fallbackHeader}
|
||||||
|
hasActions={hasActions}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -530,6 +624,8 @@ function buildDetailSections(
|
|||||||
<SubAgentDetailSection
|
<SubAgentDetailSection
|
||||||
key="sub-agent"
|
key="sub-agent"
|
||||||
log={detail.log}
|
log={detail.log}
|
||||||
|
actions={detail.actions}
|
||||||
|
childSessionId={detail.childSessionId}
|
||||||
subAgentType={detail.subAgentType}
|
subAgentType={detail.subAgentType}
|
||||||
description={detail.description}
|
description={detail.description}
|
||||||
ds={ds}
|
ds={ds}
|
||||||
@@ -731,6 +827,35 @@ const styles = StyleSheet.create((theme) => {
|
|||||||
shellPrompt: {
|
shellPrompt: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
},
|
},
|
||||||
|
subAgentSessionText: {
|
||||||
|
fontFamily: Fonts.mono,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
lineHeight: 18,
|
||||||
|
marginBottom: theme.spacing[2],
|
||||||
|
},
|
||||||
|
subAgentActions: {
|
||||||
|
gap: theme.spacing[1],
|
||||||
|
marginBottom: theme.spacing[2],
|
||||||
|
},
|
||||||
|
subAgentActionRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
},
|
||||||
|
subAgentActionTool: {
|
||||||
|
minWidth: 56,
|
||||||
|
fontFamily: Fonts.mono,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
lineHeight: 18,
|
||||||
|
},
|
||||||
|
subAgentActionSummary: {
|
||||||
|
fontFamily: Fonts.mono,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
lineHeight: 18,
|
||||||
|
},
|
||||||
jsonScroll: {
|
jsonScroll: {
|
||||||
borderWidth: theme.borderWidth[1],
|
borderWidth: theme.borderWidth[1],
|
||||||
borderColor: theme.colors.border,
|
borderColor: theme.colors.border,
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export type ToolCallDetail =
|
|||||||
type: "sub_agent";
|
type: "sub_agent";
|
||||||
subAgentType?: string;
|
subAgentType?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
childSessionId?: string;
|
||||||
log: string;
|
log: string;
|
||||||
actions: Array<{
|
actions: Array<{
|
||||||
index: number;
|
index: number;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
import { findExecutable, isCommandAvailable } from "../../../utils/executable.js";
|
import { findExecutable, isCommandAvailable } from "../../../utils/executable.js";
|
||||||
import { withTimeout } from "../../../utils/promise-timeout.js";
|
import { withTimeout } from "../../../utils/promise-timeout.js";
|
||||||
import { spawnProcess } from "../../../utils/spawn.js";
|
import { spawnProcess } from "../../../utils/spawn.js";
|
||||||
|
import { buildToolCallDisplayModel } from "../../../shared/tool-call-display.js";
|
||||||
import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
|
import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
|
||||||
import {
|
import {
|
||||||
formatDiagnosticStatus,
|
formatDiagnosticStatus,
|
||||||
@@ -1312,16 +1313,43 @@ export class OpenCodeAgentClient implements AgentClient {
|
|||||||
|
|
||||||
export interface OpenCodeEventTranslationState {
|
export interface OpenCodeEventTranslationState {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
cwd?: string;
|
||||||
messageRoles: Map<string, OpenCodeMessageRole>;
|
messageRoles: Map<string, OpenCodeMessageRole>;
|
||||||
accumulatedUsage: AgentUsage;
|
accumulatedUsage: AgentUsage;
|
||||||
streamedPartKeys: Set<string>;
|
streamedPartKeys: Set<string>;
|
||||||
emittedStructuredMessageIds: Set<string>;
|
emittedStructuredMessageIds: Set<string>;
|
||||||
/** Tracks the type of each part by ID, learned from message.part.updated events. */
|
/** Tracks the type of each part by ID, learned from message.part.updated events. */
|
||||||
partTypes: Map<string, string>;
|
partTypes: Map<string, string>;
|
||||||
|
subAgentsByCallId?: Map<string, OpenCodeSubAgentActivityState>;
|
||||||
|
subAgentCallIdByChildSessionId?: Map<string, string>;
|
||||||
|
pendingChildToolPartsBySessionId?: Map<string, OpenCodeToolPartEventPart[]>;
|
||||||
modelContextWindowsByModelKey?: ReadonlyMap<string, number>;
|
modelContextWindowsByModelKey?: ReadonlyMap<string, number>;
|
||||||
onAssistantModelContextWindowResolved?: (contextWindowMaxTokens: number) => void;
|
onAssistantModelContextWindowResolved?: (contextWindowMaxTokens: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenCodeToolPartEventPart = Extract<
|
||||||
|
Extract<OpenCodeEvent, { type: "message.part.updated" }>["properties"]["part"],
|
||||||
|
{ type: "tool" }
|
||||||
|
>;
|
||||||
|
|
||||||
|
interface OpenCodeSubAgentActionEntry {
|
||||||
|
index: number;
|
||||||
|
key: string;
|
||||||
|
toolName: string;
|
||||||
|
summary?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenCodeSubAgentActivityState {
|
||||||
|
toolCall: ToolCallTimelineItem;
|
||||||
|
actions: OpenCodeSubAgentActionEntry[];
|
||||||
|
actionIndexByKey: Map<string, number>;
|
||||||
|
nextActionIndex: number;
|
||||||
|
childSessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_OPENCODE_SUB_AGENT_ACTIONS = 200;
|
||||||
|
const MAX_OPENCODE_PENDING_CHILD_TOOL_PARTS = 200;
|
||||||
|
|
||||||
function stringifyStructuredAssistantMessage(value: unknown): string | null {
|
function stringifyStructuredAssistantMessage(value: unknown): string | null {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return null;
|
return null;
|
||||||
@@ -1564,6 +1592,269 @@ function resetOpenCodeTurnTrackingState(state: OpenCodeEventTranslationState): v
|
|||||||
state.partTypes.clear();
|
state.partTypes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOpenCodeSubAgentMaps(state: OpenCodeEventTranslationState): {
|
||||||
|
byCallId: Map<string, OpenCodeSubAgentActivityState>;
|
||||||
|
callIdByChildSessionId: Map<string, string>;
|
||||||
|
pendingChildToolPartsBySessionId: Map<string, OpenCodeToolPartEventPart[]>;
|
||||||
|
} {
|
||||||
|
state.subAgentsByCallId ??= new Map();
|
||||||
|
state.subAgentCallIdByChildSessionId ??= new Map();
|
||||||
|
state.pendingChildToolPartsBySessionId ??= new Map();
|
||||||
|
return {
|
||||||
|
byCallId: state.subAgentsByCallId,
|
||||||
|
callIdByChildSessionId: state.subAgentCallIdByChildSessionId,
|
||||||
|
pendingChildToolPartsBySessionId: state.pendingChildToolPartsBySessionId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpenCodeSubAgentState(
|
||||||
|
callId: string,
|
||||||
|
state: OpenCodeEventTranslationState,
|
||||||
|
toolCall: ToolCallTimelineItem,
|
||||||
|
): OpenCodeSubAgentActivityState {
|
||||||
|
const maps = getOpenCodeSubAgentMaps(state);
|
||||||
|
const existing = maps.byCallId.get(callId);
|
||||||
|
if (existing) {
|
||||||
|
existing.toolCall = toolCall;
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const created: OpenCodeSubAgentActivityState = {
|
||||||
|
toolCall,
|
||||||
|
actions: [],
|
||||||
|
actionIndexByKey: new Map(),
|
||||||
|
nextActionIndex: 1,
|
||||||
|
};
|
||||||
|
maps.byCallId.set(callId, created);
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linkOpenCodeSubAgentChildSession(
|
||||||
|
activity: OpenCodeSubAgentActivityState,
|
||||||
|
childSessionId: string,
|
||||||
|
state: OpenCodeEventTranslationState,
|
||||||
|
): void {
|
||||||
|
activity.childSessionId = childSessionId;
|
||||||
|
const maps = getOpenCodeSubAgentMaps(state);
|
||||||
|
maps.callIdByChildSessionId.set(childSessionId, activity.toolCall.callId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOpenCodeSubAgentLog(
|
||||||
|
detail: Extract<ToolCallDetail, { type: "sub_agent" }>,
|
||||||
|
activity: OpenCodeSubAgentActivityState,
|
||||||
|
): string {
|
||||||
|
const actionLog = activity.actions
|
||||||
|
.map((action) =>
|
||||||
|
action.summary ? `[${action.toolName}] ${action.summary}` : `[${action.toolName}]`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
const parts = [actionLog, detail.log].filter((part) => part.trim().length > 0);
|
||||||
|
return parts.join("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOpenCodeSubAgentTimelineItem(
|
||||||
|
activity: OpenCodeSubAgentActivityState,
|
||||||
|
): ToolCallTimelineItem {
|
||||||
|
const toolCall = activity.toolCall;
|
||||||
|
if (toolCall.detail.type !== "sub_agent") {
|
||||||
|
return toolCall;
|
||||||
|
}
|
||||||
|
const childSessionId = activity.childSessionId ?? toolCall.detail.childSessionId;
|
||||||
|
return {
|
||||||
|
...toolCall,
|
||||||
|
detail: {
|
||||||
|
...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 } : {}),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerOpenCodeSubAgentToolCall(
|
||||||
|
item: ToolCallTimelineItem,
|
||||||
|
state: OpenCodeEventTranslationState,
|
||||||
|
): ToolCallTimelineItem {
|
||||||
|
if (item.detail.type !== "sub_agent") {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
const activity = getOpenCodeSubAgentState(item.callId, state, item);
|
||||||
|
if (item.detail.childSessionId) {
|
||||||
|
linkOpenCodeSubAgentChildSession(activity, item.detail.childSessionId, state);
|
||||||
|
}
|
||||||
|
return buildOpenCodeSubAgentTimelineItem(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bufferOpenCodeSubAgentChildToolPart(
|
||||||
|
part: OpenCodeToolPartEventPart,
|
||||||
|
state: OpenCodeEventTranslationState,
|
||||||
|
): void {
|
||||||
|
const maps = getOpenCodeSubAgentMaps(state);
|
||||||
|
if (maps.byCallId.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const totalPending = [...maps.pendingChildToolPartsBySessionId.values()].reduce(
|
||||||
|
(total, parts) => total + parts.length,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
if (totalPending >= MAX_OPENCODE_PENDING_CHILD_TOOL_PARTS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pending = maps.pendingChildToolPartsBySessionId.get(part.sessionID) ?? [];
|
||||||
|
pending.push(part);
|
||||||
|
maps.pendingChildToolPartsBySessionId.set(part.sessionID, pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushOpenCodeSubAgentChildToolParts(
|
||||||
|
childSessionId: string,
|
||||||
|
state: OpenCodeEventTranslationState,
|
||||||
|
events: AgentStreamEvent[],
|
||||||
|
): void {
|
||||||
|
const maps = getOpenCodeSubAgentMaps(state);
|
||||||
|
const pending = maps.pendingChildToolPartsBySessionId.get(childSessionId);
|
||||||
|
if (!pending || pending.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
maps.pendingChildToolPartsBySessionId.delete(childSessionId);
|
||||||
|
for (const part of pending) {
|
||||||
|
appendOpenCodeSubAgentChildToolPart(part, state, events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findOnlyOpenCodeSubAgentWaitingForChild(
|
||||||
|
state: OpenCodeEventTranslationState,
|
||||||
|
): OpenCodeSubAgentActivityState | null {
|
||||||
|
const maps = getOpenCodeSubAgentMaps(state);
|
||||||
|
const candidates = [...maps.byCallId.values()].filter(
|
||||||
|
(activity) =>
|
||||||
|
activity.toolCall.status === "running" &&
|
||||||
|
activity.toolCall.detail.type === "sub_agent" &&
|
||||||
|
!activity.childSessionId,
|
||||||
|
);
|
||||||
|
return candidates.length === 1 ? (candidates[0] ?? null) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeOpenCodeSubAgentAction(
|
||||||
|
item: ToolCallTimelineItem,
|
||||||
|
cwd: string | undefined,
|
||||||
|
): string | undefined {
|
||||||
|
const display = buildToolCallDisplayModel({
|
||||||
|
name: item.name,
|
||||||
|
status: item.status,
|
||||||
|
error: item.error,
|
||||||
|
metadata: item.metadata,
|
||||||
|
detail: item.detail,
|
||||||
|
cwd,
|
||||||
|
});
|
||||||
|
return display.summary ?? display.errorText;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendOpenCodeSubAgentAction(
|
||||||
|
activity: OpenCodeSubAgentActivityState,
|
||||||
|
item: ToolCallTimelineItem,
|
||||||
|
cwd: string | undefined,
|
||||||
|
): boolean {
|
||||||
|
const key = item.callId || `${item.name}:${activity.actions.length}`;
|
||||||
|
const existingIndex = activity.actionIndexByKey.get(key);
|
||||||
|
const summary = summarizeOpenCodeSubAgentAction(item, cwd);
|
||||||
|
|
||||||
|
if (existingIndex !== undefined) {
|
||||||
|
const action = activity.actions[existingIndex];
|
||||||
|
if (!action) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const changed = action.toolName !== item.name || action.summary !== summary;
|
||||||
|
action.toolName = item.name;
|
||||||
|
if (summary) {
|
||||||
|
action.summary = summary;
|
||||||
|
} else {
|
||||||
|
delete action.summary;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activity.actions.length >= MAX_OPENCODE_SUB_AGENT_ACTIONS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.actionIndexByKey.set(key, activity.actions.length);
|
||||||
|
activity.actions.push({
|
||||||
|
index: activity.nextActionIndex,
|
||||||
|
key,
|
||||||
|
toolName: item.name,
|
||||||
|
...(summary ? { summary } : {}),
|
||||||
|
});
|
||||||
|
activity.nextActionIndex += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendOpenCodeToolCallTimelineItem(
|
||||||
|
item: ToolCallTimelineItem,
|
||||||
|
state: OpenCodeEventTranslationState,
|
||||||
|
events: AgentStreamEvent[],
|
||||||
|
): void {
|
||||||
|
const timelineItem = registerOpenCodeSubAgentToolCall(item, state);
|
||||||
|
events.push({
|
||||||
|
type: "timeline",
|
||||||
|
provider: "opencode",
|
||||||
|
item: timelineItem,
|
||||||
|
});
|
||||||
|
if (timelineItem.detail.type === "sub_agent" && timelineItem.detail.childSessionId) {
|
||||||
|
flushOpenCodeSubAgentChildToolParts(timelineItem.detail.childSessionId, state, events);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendOpenCodeSubAgentChildSessionLinked(
|
||||||
|
childSessionId: string,
|
||||||
|
state: OpenCodeEventTranslationState,
|
||||||
|
events: AgentStreamEvent[],
|
||||||
|
): void {
|
||||||
|
const activity = findOnlyOpenCodeSubAgentWaitingForChild(state);
|
||||||
|
if (!activity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
linkOpenCodeSubAgentChildSession(activity, childSessionId, state);
|
||||||
|
events.push({
|
||||||
|
type: "timeline",
|
||||||
|
provider: "opencode",
|
||||||
|
item: buildOpenCodeSubAgentTimelineItem(activity),
|
||||||
|
});
|
||||||
|
flushOpenCodeSubAgentChildToolParts(childSessionId, state, events);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendOpenCodeSubAgentChildToolPart(
|
||||||
|
part: OpenCodeToolPartEventPart,
|
||||||
|
state: OpenCodeEventTranslationState,
|
||||||
|
events: AgentStreamEvent[],
|
||||||
|
): void {
|
||||||
|
const maps = getOpenCodeSubAgentMaps(state);
|
||||||
|
const parentCallId = maps.callIdByChildSessionId.get(part.sessionID);
|
||||||
|
if (!parentCallId) {
|
||||||
|
bufferOpenCodeSubAgentChildToolPart(part, state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const activity = maps.byCallId.get(parentCallId);
|
||||||
|
if (!activity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
|
||||||
|
if (!parsedToolPart.success || !parsedToolPart.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!appendOpenCodeSubAgentAction(activity, parsedToolPart.data, state.cwd)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
events.push({
|
||||||
|
type: "timeline",
|
||||||
|
provider: "opencode",
|
||||||
|
item: buildOpenCodeSubAgentTimelineItem(activity),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function appendOpenCodeSessionCreatedOrUpdated(
|
function appendOpenCodeSessionCreatedOrUpdated(
|
||||||
event: Extract<OpenCodeEvent, { type: "session.created" | "session.updated" }>,
|
event: Extract<OpenCodeEvent, { type: "session.created" | "session.updated" }>,
|
||||||
state: OpenCodeEventTranslationState,
|
state: OpenCodeEventTranslationState,
|
||||||
@@ -1575,6 +1866,13 @@ function appendOpenCodeSessionCreatedOrUpdated(
|
|||||||
sessionId: state.sessionId,
|
sessionId: state.sessionId,
|
||||||
provider: "opencode",
|
provider: "opencode",
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = readOpenCodeRecord(event.properties.info);
|
||||||
|
const parentSessionId = readNonEmptyString(info?.parentID) ?? readNonEmptyString(info?.parentId);
|
||||||
|
if (parentSessionId === state.sessionId) {
|
||||||
|
appendOpenCodeSubAgentChildSessionLinked(event.properties.info.id, state, events);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1620,6 +1918,9 @@ function appendOpenCodeMessagePartUpdated(
|
|||||||
): void {
|
): void {
|
||||||
const part = event.properties.part;
|
const part = event.properties.part;
|
||||||
if (part.sessionID !== state.sessionId) {
|
if (part.sessionID !== state.sessionId) {
|
||||||
|
if (part.type === "tool") {
|
||||||
|
appendOpenCodeSubAgentChildToolPart(part, state, events);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const messageRole = state.messageRoles.get(part.messageID);
|
const messageRole = state.messageRoles.get(part.messageID);
|
||||||
@@ -1636,7 +1937,7 @@ function appendOpenCodeMessagePartUpdated(
|
|||||||
if (part.type === "tool") {
|
if (part.type === "tool") {
|
||||||
const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
|
const parsedToolPart = OpencodeToolPartToTimelineItemSchema.safeParse(part);
|
||||||
if (parsedToolPart.success && parsedToolPart.data) {
|
if (parsedToolPart.success && parsedToolPart.data) {
|
||||||
events.push({ type: "timeline", provider: "opencode", item: parsedToolPart.data });
|
appendOpenCodeToolCallTimelineItem(parsedToolPart.data, state, events);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1914,6 +2215,9 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
private nextTurnOrdinal = 0;
|
private nextTurnOrdinal = 0;
|
||||||
private activeForegroundTurnId: string | null = null;
|
private activeForegroundTurnId: string | null = null;
|
||||||
private readonly runningToolCalls = new Map<string, ToolCallTimelineItem>();
|
private readonly runningToolCalls = new Map<string, ToolCallTimelineItem>();
|
||||||
|
private subAgentsByCallId = new Map<string, OpenCodeSubAgentActivityState>();
|
||||||
|
private subAgentCallIdByChildSessionId = new Map<string, string>();
|
||||||
|
private pendingChildToolPartsBySessionId = new Map<string, OpenCodeToolPartEventPart[]>();
|
||||||
private selectedModelContextWindowMaxTokens: number | undefined;
|
private selectedModelContextWindowMaxTokens: number | undefined;
|
||||||
private releaseServer: (() => void) | null;
|
private releaseServer: (() => void) | null;
|
||||||
constructor(
|
constructor(
|
||||||
@@ -2066,6 +2370,9 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.runningToolCalls.clear();
|
this.runningToolCalls.clear();
|
||||||
|
this.subAgentsByCallId.clear();
|
||||||
|
this.subAgentCallIdByChildSessionId.clear();
|
||||||
|
this.pendingChildToolPartsBySessionId.clear();
|
||||||
const turnAbortController = new AbortController();
|
const turnAbortController = new AbortController();
|
||||||
this.abortController = turnAbortController;
|
this.abortController = turnAbortController;
|
||||||
await this.ensureMcpServersConfigured();
|
await this.ensureMcpServersConfigured();
|
||||||
@@ -2352,6 +2659,7 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
|
|
||||||
private synthesizeInterruptedToolCalls(turnId: string): void {
|
private synthesizeInterruptedToolCalls(turnId: string): void {
|
||||||
for (const item of this.runningToolCalls.values()) {
|
for (const item of this.runningToolCalls.values()) {
|
||||||
|
const error = { message: "Tool execution aborted" };
|
||||||
this.notifySubscribers(
|
this.notifySubscribers(
|
||||||
{
|
{
|
||||||
type: "timeline",
|
type: "timeline",
|
||||||
@@ -2359,7 +2667,16 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
item: {
|
item: {
|
||||||
...item,
|
...item,
|
||||||
status: "failed",
|
status: "failed",
|
||||||
error: { message: "Tool execution aborted" },
|
error,
|
||||||
|
detail:
|
||||||
|
item.detail.type === "sub_agent"
|
||||||
|
? {
|
||||||
|
...item.detail,
|
||||||
|
log: [item.detail.log, error.message]
|
||||||
|
.filter((entry) => entry.trim().length > 0)
|
||||||
|
.join("\n"),
|
||||||
|
}
|
||||||
|
: item.detail,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
turnId,
|
turnId,
|
||||||
@@ -2710,11 +3027,15 @@ class OpenCodeAgentSession implements AgentSession {
|
|||||||
private async translateEvent(event: OpenCodeEvent): Promise<AgentStreamEvent[]> {
|
private async translateEvent(event: OpenCodeEvent): Promise<AgentStreamEvent[]> {
|
||||||
const translated = translateOpenCodeEvent(event, {
|
const translated = translateOpenCodeEvent(event, {
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
|
cwd: this.config.cwd,
|
||||||
messageRoles: this.messageRoles,
|
messageRoles: this.messageRoles,
|
||||||
accumulatedUsage: this.accumulatedUsage,
|
accumulatedUsage: this.accumulatedUsage,
|
||||||
streamedPartKeys: this.streamedPartKeys,
|
streamedPartKeys: this.streamedPartKeys,
|
||||||
emittedStructuredMessageIds: this.emittedStructuredMessageIds,
|
emittedStructuredMessageIds: this.emittedStructuredMessageIds,
|
||||||
partTypes: this.partTypes,
|
partTypes: this.partTypes,
|
||||||
|
subAgentsByCallId: this.subAgentsByCallId,
|
||||||
|
subAgentCallIdByChildSessionId: this.subAgentCallIdByChildSessionId,
|
||||||
|
pendingChildToolPartsBySessionId: this.pendingChildToolPartsBySessionId,
|
||||||
modelContextWindowsByModelKey: this.modelContextWindowsByModelKey,
|
modelContextWindowsByModelKey: this.modelContextWindowsByModelKey,
|
||||||
onAssistantModelContextWindowResolved: (contextWindowMaxTokens) => {
|
onAssistantModelContextWindowResolved: (contextWindowMaxTokens) => {
|
||||||
this.accumulatedUsage.contextWindowMaxTokens = contextWindowMaxTokens;
|
this.accumulatedUsage.contextWindowMaxTokens = contextWindowMaxTokens;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { ToolCallDetail } from "../../agent-sdk-types.js";
|
import type { ToolCallDetail } from "../../agent-sdk-types.js";
|
||||||
|
import { nonEmptyString } from "../tool-call-mapper-utils.js";
|
||||||
import {
|
import {
|
||||||
ToolEditInputSchema,
|
ToolEditInputSchema,
|
||||||
ToolEditOutputSchema,
|
ToolEditOutputSchema,
|
||||||
|
ToolGlobOutputSchema,
|
||||||
ToolReadInputSchema,
|
ToolReadInputSchema,
|
||||||
ToolReadOutputSchema,
|
ToolReadOutputSchema,
|
||||||
ToolSearchInputSchema,
|
ToolSearchInputSchema,
|
||||||
@@ -19,6 +21,100 @@ import {
|
|||||||
toolDetailBranchByToolName,
|
toolDetailBranchByToolName,
|
||||||
} from "../tool-call-detail-primitives.js";
|
} from "../tool-call-detail-primitives.js";
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSubAgentText(value: unknown): string | undefined {
|
||||||
|
return nonEmptyString(value)?.trim().replace(/\s+/g, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOutputText(value: unknown): string | undefined {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return nonEmptyString(value.trim());
|
||||||
|
}
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const directText =
|
||||||
|
readOutputText(value.output) ??
|
||||||
|
readOutputText(value.text) ??
|
||||||
|
readOutputText(value.content) ??
|
||||||
|
readOutputText(value.result);
|
||||||
|
if (directText) {
|
||||||
|
return directText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLogEntry(value: unknown): string | undefined {
|
||||||
|
const outputText = readOutputText(value);
|
||||||
|
if (outputText) {
|
||||||
|
return outputText;
|
||||||
|
}
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractOpenCodeTaskSessionId(value: unknown): string | undefined {
|
||||||
|
const text = readOutputText(value);
|
||||||
|
if (text) {
|
||||||
|
const match = text.match(/\btask_id:\s*(ses_[A-Za-z0-9]+)/i);
|
||||||
|
if (match?.[1]) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
normalizeSubAgentText(value.task_id) ??
|
||||||
|
normalizeSubAgentText(value.taskId) ??
|
||||||
|
normalizeSubAgentText(value.sessionID) ??
|
||||||
|
normalizeSubAgentText(value.sessionId) ??
|
||||||
|
extractOpenCodeTaskSessionId(value.output) ??
|
||||||
|
extractOpenCodeTaskSessionId(value.text) ??
|
||||||
|
extractOpenCodeTaskSessionId(value.content) ??
|
||||||
|
extractOpenCodeTaskSessionId(value.result)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveOpencodeTaskDetail(
|
||||||
|
input: unknown,
|
||||||
|
output: unknown,
|
||||||
|
error: unknown,
|
||||||
|
): ToolCallDetail | null {
|
||||||
|
if (!isRecord(input)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subAgentType = normalizeSubAgentText(input.subagent_type ?? input.subAgentType);
|
||||||
|
const description = normalizeSubAgentText(input.description);
|
||||||
|
if (!subAgentType && !description) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const log = [formatLogEntry(output), formatLogEntry(error)].filter((entry) => entry).join("\n");
|
||||||
|
const childSessionId = extractOpenCodeTaskSessionId(output);
|
||||||
|
return {
|
||||||
|
type: "sub_agent",
|
||||||
|
...(subAgentType ? { subAgentType } : {}),
|
||||||
|
...(description ? { description } : {}),
|
||||||
|
...(childSessionId ? { childSessionId } : {}),
|
||||||
|
log,
|
||||||
|
actions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const OpencodeKnownToolDetailSchema = z.union([
|
const OpencodeKnownToolDetailSchema = z.union([
|
||||||
toolDetailBranchByToolName(
|
toolDetailBranchByToolName(
|
||||||
"shell",
|
"shell",
|
||||||
@@ -80,6 +176,14 @@ const OpencodeKnownToolDetailSchema = z.union([
|
|||||||
toolDetailBranchByToolName("search", ToolSearchInputSchema, z.unknown(), (input) =>
|
toolDetailBranchByToolName("search", ToolSearchInputSchema, z.unknown(), (input) =>
|
||||||
toSearchToolDetail({ input, toolName: "search" }),
|
toSearchToolDetail({ input, toolName: "search" }),
|
||||||
),
|
),
|
||||||
|
toolDetailBranchByToolName("glob", ToolSearchInputSchema, z.unknown(), (input, output) => {
|
||||||
|
const parsedOutput = ToolGlobOutputSchema.safeParse(output);
|
||||||
|
return toSearchToolDetail({
|
||||||
|
input,
|
||||||
|
output: parsedOutput.success ? parsedOutput.data : null,
|
||||||
|
toolName: "glob",
|
||||||
|
});
|
||||||
|
}),
|
||||||
toolDetailBranchByToolName("web_search", ToolSearchInputSchema, z.unknown(), (input) =>
|
toolDetailBranchByToolName("web_search", ToolSearchInputSchema, z.unknown(), (input) =>
|
||||||
toSearchToolDetail({ input, toolName: "web_search" }),
|
toSearchToolDetail({ input, toolName: "web_search" }),
|
||||||
),
|
),
|
||||||
@@ -89,7 +193,15 @@ export function deriveOpencodeToolDetail(
|
|||||||
toolName: string,
|
toolName: string,
|
||||||
input: unknown,
|
input: unknown,
|
||||||
output: unknown,
|
output: unknown,
|
||||||
|
error: unknown = null,
|
||||||
): ToolCallDetail {
|
): ToolCallDetail {
|
||||||
|
if (toolName.trim().toLowerCase() === "task") {
|
||||||
|
const taskDetail = deriveOpencodeTaskDetail(input, output, error);
|
||||||
|
if (taskDetail) {
|
||||||
|
return taskDetail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const parsed = OpencodeKnownToolDetailSchema.safeParse({
|
const parsed = OpencodeKnownToolDetailSchema.safeParse({
|
||||||
toolName,
|
toolName,
|
||||||
input,
|
input,
|
||||||
|
|||||||
@@ -88,6 +88,21 @@ describe("opencode tool-call mapper", () => {
|
|||||||
query: "opencode mapper",
|
query: "opencode mapper",
|
||||||
toolName: "web_search",
|
toolName: "web_search",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const globItem = expectMapped(
|
||||||
|
mapOpencodeToolCall({
|
||||||
|
toolName: "glob",
|
||||||
|
callId: "opencode-running-glob",
|
||||||
|
status: "running",
|
||||||
|
input: { pattern: "**/*.md" },
|
||||||
|
output: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(globItem.detail).toEqual({
|
||||||
|
type: "search",
|
||||||
|
query: "**/*.md",
|
||||||
|
toolName: "glob",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps completed read calls", () => {
|
it("maps completed read calls", () => {
|
||||||
@@ -235,6 +250,106 @@ describe("opencode tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps running task calls with subagent input to sub_agent detail", () => {
|
||||||
|
const item = expectMapped(
|
||||||
|
mapOpencodeToolCall({
|
||||||
|
toolName: "task",
|
||||||
|
callId: "opencode-task-running",
|
||||||
|
status: "running",
|
||||||
|
input: {
|
||||||
|
subagent_type: "explore",
|
||||||
|
description: "Explore agent-tools codebase",
|
||||||
|
},
|
||||||
|
output: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(item.status).toBe("running");
|
||||||
|
expect(item.error).toBeNull();
|
||||||
|
expect(item.detail).toEqual({
|
||||||
|
type: "sub_agent",
|
||||||
|
subAgentType: "explore",
|
||||||
|
description: "Explore agent-tools codebase",
|
||||||
|
log: "",
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps completed task calls with final output log to sub_agent detail", () => {
|
||||||
|
const item = expectMapped(
|
||||||
|
mapOpencodeToolCall({
|
||||||
|
toolName: "task",
|
||||||
|
callId: "opencode-task-completed",
|
||||||
|
status: "completed",
|
||||||
|
input: {
|
||||||
|
subagent_type: "explore",
|
||||||
|
description: "Explore agent-tools codebase",
|
||||||
|
},
|
||||||
|
output: { result: "Found the CLI entrypoint and provider registry." },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(item.status).toBe("completed");
|
||||||
|
expect(item.error).toBeNull();
|
||||||
|
expect(item.detail).toEqual({
|
||||||
|
type: "sub_agent",
|
||||||
|
subAgentType: "explore",
|
||||||
|
description: "Explore agent-tools codebase",
|
||||||
|
log: "Found the CLI entrypoint and provider registry.",
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts child session ids from completed task output", () => {
|
||||||
|
const item = expectMapped(
|
||||||
|
mapOpencodeToolCall({
|
||||||
|
toolName: "task",
|
||||||
|
callId: "opencode-task-completed-with-id",
|
||||||
|
status: "completed",
|
||||||
|
input: {
|
||||||
|
subagent_type: "explore",
|
||||||
|
description: "Explore current directory",
|
||||||
|
},
|
||||||
|
output: "task_id: ses_2268db431ffe299vL1bbot8R7Z\n\n<task_result>done</task_result>",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(item.detail).toEqual({
|
||||||
|
type: "sub_agent",
|
||||||
|
subAgentType: "explore",
|
||||||
|
description: "Explore current directory",
|
||||||
|
childSessionId: "ses_2268db431ffe299vL1bbot8R7Z",
|
||||||
|
log: "task_id: ses_2268db431ffe299vL1bbot8R7Z\n\n<task_result>done</task_result>",
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps aborted task calls with preserved error log to sub_agent detail", () => {
|
||||||
|
const item = expectMapped(
|
||||||
|
mapOpencodeToolCall({
|
||||||
|
toolName: "task",
|
||||||
|
callId: "opencode-task-aborted",
|
||||||
|
status: "aborted",
|
||||||
|
input: {
|
||||||
|
subagent_type: "explore",
|
||||||
|
description: "Explore agent-tools codebase",
|
||||||
|
},
|
||||||
|
output: null,
|
||||||
|
error: "Tool execution aborted",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(item.status).toBe("failed");
|
||||||
|
expect(item.error).toBe("Tool execution aborted");
|
||||||
|
expect(item.detail).toEqual({
|
||||||
|
type: "sub_agent",
|
||||||
|
subAgentType: "explore",
|
||||||
|
description: "Explore agent-tools codebase",
|
||||||
|
log: "Tool execution aborted",
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("does not apply cross-provider speak normalization in opencode mapper", () => {
|
it("does not apply cross-provider speak normalization in opencode mapper", () => {
|
||||||
const item = expectMapped(
|
const item = expectMapped(
|
||||||
mapOpencodeToolCall({
|
mapOpencodeToolCall({
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function mapOpencodeToolCall(params: OpencodeToolCallParams): ToolCallTim
|
|||||||
const error = raw.error ?? null;
|
const error = raw.error ?? null;
|
||||||
const rawStatus = typeof raw.status === "string" ? raw.status : undefined;
|
const rawStatus = typeof raw.status === "string" ? raw.status : undefined;
|
||||||
const status = normalizeToolCallStatus(rawStatus, error, output);
|
const status = normalizeToolCallStatus(rawStatus, error, output);
|
||||||
const detail = deriveOpencodeToolDetail(name, input, output);
|
const detail = deriveOpencodeToolDetail(name, input, output, error);
|
||||||
|
|
||||||
if (status === "failed") {
|
if (status === "failed") {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -147,6 +147,30 @@ describe("shared messages tool_call schema", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("defaults missing sub_agent actions for older payloads", () => {
|
||||||
|
const parsed = AgentTimelineItemPayloadSchema.parse({
|
||||||
|
type: "tool_call",
|
||||||
|
callId: "call_sub_agent_legacy",
|
||||||
|
name: "Task",
|
||||||
|
status: "running",
|
||||||
|
error: null,
|
||||||
|
detail: {
|
||||||
|
type: "sub_agent",
|
||||||
|
subAgentType: "Explore",
|
||||||
|
description: "Inspect repository structure",
|
||||||
|
log: "[Read] README.md",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parsed.type).toBe("tool_call");
|
||||||
|
if (parsed.type === "tool_call") {
|
||||||
|
expect(parsed.detail.type).toBe("sub_agent");
|
||||||
|
if (parsed.detail.type === "sub_agent") {
|
||||||
|
expect(parsed.detail.actions).toEqual([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("parses plain_text detail with icon and rejects unknown icon names", () => {
|
it("parses plain_text detail with icon and rejects unknown icon names", () => {
|
||||||
const parsed = AgentTimelineItemPayloadSchema.parse({
|
const parsed = AgentTimelineItemPayloadSchema.parse({
|
||||||
type: "tool_call",
|
type: "tool_call",
|
||||||
|
|||||||
@@ -412,14 +412,18 @@ const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail, z.ZodTypeDef, unkno
|
|||||||
type: z.literal("sub_agent"),
|
type: z.literal("sub_agent"),
|
||||||
subAgentType: z.string().optional(),
|
subAgentType: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
childSessionId: z.string().optional(),
|
||||||
log: z.string(),
|
log: z.string(),
|
||||||
actions: z.array(
|
actions: z
|
||||||
z.object({
|
.array(
|
||||||
index: z.number().int().positive(),
|
z.object({
|
||||||
toolName: z.string(),
|
index: z.number().int().positive(),
|
||||||
summary: z.string().optional(),
|
toolName: z.string(),
|
||||||
}),
|
summary: z.string().optional(),
|
||||||
),
|
}),
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.default([]),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
type: z.literal("plain_text"),
|
type: z.literal("plain_text"),
|
||||||
|
|||||||
Reference in New Issue
Block a user