diff --git a/packages/web/src/__tests__/SubagentCard.test.tsx b/packages/web/src/__tests__/SubagentCard.test.tsx
new file mode 100644
index 0000000..2abb033
--- /dev/null
+++ b/packages/web/src/__tests__/SubagentCard.test.tsx
@@ -0,0 +1,101 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it } from "vitest";
+import { SubagentCard } from "../components/StepCards";
+import type { TrajectoryStep } from "../types";
+
+function nativeStep(
+ status = "CORTEX_STEP_STATUS_DONE",
+): TrajectoryStep {
+ return {
+ type: "CORTEX_STEP_TYPE_INVOKE_SUBAGENT",
+ status,
+ invokeSubagent: {
+ subagents: [
+ {
+ role: "Integration Reviewer",
+ typeName: "general-purpose",
+ initialPrompt: "Review the integration",
+ },
+ {
+ role: "Security Reviewer",
+ typeName: "research",
+ initialPrompt: "Review security boundaries",
+ },
+ ],
+ },
+ };
+}
+
+describe("SubagentCard", () => {
+ it("renders every native subagent and expands every prompt", async () => {
+ render();
+
+ expect(screen.getByText("Integration Reviewer")).toBeInTheDocument();
+ expect(screen.getByText("Security Reviewer")).toBeInTheDocument();
+ expect(screen.queryByText("Review the integration")).not.toBeInTheDocument();
+
+ await userEvent.click(
+ screen.getByRole("button", { name: /2 Subagents Invoked/i }),
+ );
+
+ expect(screen.getByText("Review the integration")).toBeInTheDocument();
+ expect(screen.getByText("Review security boundaries")).toBeInTheDocument();
+ });
+
+ it.each([
+ ["CORTEX_STEP_STATUS_PENDING", "Pending", "cmd-wait"],
+ ["CORTEX_STEP_STATUS_ERROR", "Failed", "cmd-fail"],
+ ["CORTEX_STEP_STATUS_CANCELED", "Canceled", "cmd-fail"],
+ ["CORTEX_STEP_STATUS_INTERRUPTED", "Interrupted", "cmd-fail"],
+ ["CORTEX_STEP_STATUS_DONE", "Done", "cmd-ok"],
+ ])("renders %s with the correct state", (status, label, className) => {
+ const { container } = render();
+
+ expect(screen.getByText(label)).toBeInTheDocument();
+ expect(container.querySelector(".subagent-card")).toHaveClass(className);
+ });
+
+ it("renders tool-specific send_message content", async () => {
+ const step: TrajectoryStep = {
+ type: "CORTEX_STEP_TYPE_TOOL_CALL",
+ metadata: {
+ toolCall: {
+ name: "send_message",
+ argumentsJson: JSON.stringify({
+ Recipient: "conversation-123",
+ Message: "Please inspect the auth flow",
+ }),
+ },
+ },
+ };
+ render();
+
+ expect(screen.getByText("Message to conversation-123")).toBeInTheDocument();
+ expect(screen.getByText("conversation-123")).toBeInTheDocument();
+ await userEvent.click(
+ screen.getByRole("button", { name: /Message to conversation-123/i }),
+ );
+ expect(screen.getByText("Please inspect the auth flow")).toBeInTheDocument();
+ });
+
+ it("renders untrusted labels as text rather than HTML", () => {
+ const step: TrajectoryStep = {
+ type: "CORTEX_STEP_TYPE_INVOKE_SUBAGENT",
+ invokeSubagent: {
+ subagents: [
+ {
+ role: '
',
+ initialPrompt: "safe text",
+ },
+ ],
+ },
+ };
+ const { container } = render();
+
+ expect(
+ screen.getByText('
'),
+ ).toBeInTheDocument();
+ expect(container.querySelector("img")).toBeNull();
+ });
+});
diff --git a/packages/web/src/__tests__/stepsToMessages.test.ts b/packages/web/src/__tests__/stepsToMessages.test.ts
index 404018d..8115d4c 100644
--- a/packages/web/src/__tests__/stepsToMessages.test.ts
+++ b/packages/web/src/__tests__/stepsToMessages.test.ts
@@ -455,5 +455,188 @@ describe("stepsToMessages", () => {
expect(msgs).toHaveLength(1);
expect(msgs[0].type).toBe("CORTEX_STEP_TYPE_SUBAGENT");
expect(msgs[0].step).toBe(step);
+ expect(msgs[0].subagent).toMatchObject({
+ kind: "invoke",
+ title: "Subagent Invoked",
+ action: "Invoking research subagent",
+ items: [
+ {
+ role: "Config Auditor",
+ typeName: "research",
+ details: [
+ { label: "Instructions", text: "Audit all config files" },
+ ],
+ },
+ ],
+ });
+ });
+
+ it("correlates a payloadless native marker with captured planner arguments", () => {
+ const marker: TrajectoryStep = {
+ type: "CORTEX_STEP_TYPE_INVOKE_SUBAGENT",
+ status: "CORTEX_STEP_STATUS_DONE",
+ };
+ const steps: TrajectoryStep[] = [
+ {
+ type: "CORTEX_STEP_TYPE_PLANNER_RESPONSE",
+ plannerResponse: {
+ toolCalls: [
+ {
+ name: "invoke_subagent",
+ argumentsJson: JSON.stringify({
+ Subagents: [
+ {
+ Role: "Integration Reviewer",
+ TypeName: "general-purpose",
+ Prompt: "Review API integration",
+ },
+ {
+ Role: "Security Reviewer",
+ TypeName: "research",
+ Prompt: "Review trust boundaries",
+ },
+ ],
+ }),
+ },
+ ],
+ },
+ },
+ marker,
+ ];
+
+ const msgs = stepsToMessages(steps);
+
+ expect(msgs).toHaveLength(1);
+ expect(msgs[0].step).toBe(marker);
+ expect(msgs[0].subagent?.title).toBe("2 Subagents Invoked");
+ expect(msgs[0].subagent?.items).toHaveLength(2);
+ expect(msgs[0].subagent?.items.map((item) => item.role)).toEqual([
+ "Integration Reviewer",
+ "Security Reviewer",
+ ]);
+ });
+
+ it("does not correlate a stale tool call across planner turns", () => {
+ const steps: TrajectoryStep[] = [
+ {
+ type: "CORTEX_STEP_TYPE_PLANNER_RESPONSE",
+ plannerResponse: {
+ toolCalls: [
+ {
+ name: "invoke_subagent",
+ argumentsJson: JSON.stringify({
+ Subagents: [{ Role: "Stale Reviewer" }],
+ }),
+ },
+ ],
+ },
+ },
+ {
+ type: "CORTEX_STEP_TYPE_PLANNER_RESPONSE",
+ plannerResponse: {},
+ },
+ {
+ type: "CORTEX_STEP_TYPE_INVOKE_SUBAGENT",
+ },
+ ];
+
+ const [msg] = stepsToMessages(steps);
+
+ expect(msg.subagent?.items[0].role).toBe("Subagent");
+ });
+
+ it("uses the current native invokeSubagent payload and metadata", () => {
+ const step: TrajectoryStep = {
+ type: "CORTEX_STEP_TYPE_INVOKE_SUBAGENT",
+ status: "CORTEX_STEP_STATUS_RUNNING",
+ metadata: {
+ toolSummary: "Review team",
+ toolAction: "Running two reviews",
+ },
+ invokeSubagent: {
+ subagents: [
+ {
+ role: "Bug Hunter",
+ typeName: "general-purpose",
+ initialPrompt: "Find bugs",
+ modelTier: "MODEL_TIER_PRO",
+ },
+ {
+ role: "Security Auditor",
+ typeName: "research",
+ initialPrompt: "Find vulnerabilities",
+ },
+ ],
+ },
+ };
+
+ const [msg] = stepsToMessages([step]);
+
+ expect(msg.subagent).toMatchObject({
+ kind: "invoke",
+ title: "Review team",
+ action: "Running two reviews",
+ });
+ expect(msg.subagent?.items).toHaveLength(2);
+ expect(msg.subagent?.items[0]).toMatchObject({
+ role: "Bug Hunter",
+ typeName: "general-purpose",
+ model: "MODEL_TIER_PRO",
+ });
+ });
+
+ it.each([
+ {
+ name: "define_subagent",
+ args: {
+ name: "security-reviewer",
+ description: "Reviews trust boundaries",
+ system_prompt: "Inspect untrusted input",
+ },
+ kind: "define",
+ title: "Define security-reviewer",
+ role: "security-reviewer",
+ },
+ {
+ name: "send_message",
+ args: { Recipient: "conversation-123", Message: "Check the parser" },
+ kind: "message",
+ title: "Message to conversation-123",
+ role: "conversation-123",
+ },
+ {
+ name: "manage_subagents",
+ args: { Action: "kill", ConversationIds: ["one", "two"] },
+ kind: "manage",
+ title: "Stop Subagents",
+ role: "kill",
+ },
+ ])("normalizes $name instead of labeling it as an invocation", (fixture) => {
+ const step: TrajectoryStep = {
+ type: "CORTEX_STEP_TYPE_TOOL_CALL",
+ metadata: {
+ toolCall: {
+ name: fixture.name,
+ argumentsJson: JSON.stringify(fixture.args),
+ },
+ },
+ };
+
+ const [msg] = stepsToMessages([step]);
+
+ expect(msg.subagent).toMatchObject({
+ kind: fixture.kind,
+ title: fixture.title,
+ items: [{ role: fixture.role }],
+ });
+ });
+
+ it("does not treat inherited object properties as subagent tool names", () => {
+ const step: TrajectoryStep = {
+ type: "CORTEX_STEP_TYPE_TOOL_CALL",
+ metadata: { toolCall: { name: "toString" } },
+ };
+
+ expect(stepsToMessages([step])).toEqual([]);
});
});
diff --git a/packages/web/src/components/ChatPanel.tsx b/packages/web/src/components/ChatPanel.tsx
index 4a3f792..8751eae 100644
--- a/packages/web/src/components/ChatPanel.tsx
+++ b/packages/web/src/components/ChatPanel.tsx
@@ -317,7 +317,7 @@ function SystemMessage({
if (msg.type === "CORTEX_STEP_TYPE_SUBAGENT") {
return (
-
+
);
}
diff --git a/packages/web/src/components/StepCards.tsx b/packages/web/src/components/StepCards.tsx
index 0387cd9..33fdc61 100644
--- a/packages/web/src/components/StepCards.tsx
+++ b/packages/web/src/components/StepCards.tsx
@@ -14,9 +14,11 @@ import type {
AskQuestionEntry,
AskQuestionOption,
AskQuestionRequest,
- TrajectoryStep,
FilePermissionRequest,
+ SubagentDisplayData,
+ TrajectoryStep,
} from "../types";
+import { subagentDataFromStep } from "../utils/subagents";
/** Extract file basename from a URI or path */
function basename(uriOrPath: string): string {
@@ -596,63 +598,125 @@ export function CodeActionCard({ step }: CodeActionCardProps) {
// ── Subagent Card ──
-export function SubagentCard({ step }: { step: TrajectoryStep }) {
- const [expanded, setExpanded] = useState(false);
- const toolCall = step.metadata?.toolCall;
- const toolName = toolCall?.name ?? "invoke_subagent";
+const ACTIVE_SUBAGENT_STATUSES = new Set([
+ "CORTEX_STEP_STATUS_GENERATING",
+ "CORTEX_STEP_STATUS_QUEUED",
+ "CORTEX_STEP_STATUS_PENDING",
+ "CORTEX_STEP_STATUS_RUNNING",
+ "CORTEX_STEP_STATUS_WAITING",
+]);
+
+const FAILED_SUBAGENT_STATUSES = new Set([
+ "CORTEX_STEP_STATUS_INVALID",
+ "CORTEX_STEP_STATUS_CANCELED",
+ "CORTEX_STEP_STATUS_ERROR",
+ "CORTEX_STEP_STATUS_INTERRUPTED",
+]);
+
+function subagentStatus(status?: string): {
+ label?: string;
+ className: string;
+} {
+ if (!status) return { className: "" };
+ if (status === "CORTEX_STEP_STATUS_DONE") {
+ return { label: "Done", className: "cmd-ok" };
+ }
+ if (FAILED_SUBAGENT_STATUSES.has(status)) {
+ const labels: Record = {
+ CORTEX_STEP_STATUS_INVALID: "Invalid",
+ CORTEX_STEP_STATUS_CANCELED: "Canceled",
+ CORTEX_STEP_STATUS_ERROR: "Failed",
+ CORTEX_STEP_STATUS_INTERRUPTED: "Interrupted",
+ };
+ return { label: labels[status], className: "cmd-fail" };
+ }
+ if (ACTIVE_SUBAGENT_STATUSES.has(status)) {
+ const labels: Record = {
+ CORTEX_STEP_STATUS_GENERATING: "Generating",
+ CORTEX_STEP_STATUS_QUEUED: "Queued",
+ CORTEX_STEP_STATUS_PENDING: "Pending",
+ CORTEX_STEP_STATUS_RUNNING: "Running",
+ CORTEX_STEP_STATUS_WAITING: "Waiting",
+ };
+ return { label: labels[status], className: "cmd-wait" };
+ }
+ return { className: "" };
+}
- let subagents: Array<{ Role?: string; TypeName?: string; Prompt?: string; Model?: string }> = [];
- let toolAction = (step as any).toolAction ?? "";
- let toolSummary = (step as any).toolSummary ?? "";
+interface SubagentCardProps {
+ step: TrajectoryStep;
+ data?: SubagentDisplayData;
+}
- if (toolCall?.argumentsJson) {
- try {
- const parsed = JSON.parse(toolCall.argumentsJson);
- if (Array.isArray(parsed.Subagents)) {
- subagents = parsed.Subagents;
- }
- if (parsed.toolAction) toolAction = parsed.toolAction;
- if (parsed.toolSummary) toolSummary = parsed.toolSummary;
- } catch {
- // ignore
- }
- }
+export function SubagentCard({ step, data }: SubagentCardProps) {
+ const [expanded, setExpanded] = useState(false);
+ const display = data ?? subagentDataFromStep(step);
+ if (!display) return null;
- const primaryRole =
- subagents[0]?.Role ||
- toolSummary ||
- (toolName === "define_subagent" ? "Subagent Defined" : "Subagent Invoked");
- const primaryTypeName = subagents[0]?.TypeName || "subagent";
- const promptText = subagents[0]?.Prompt || "";
- const isRunning =
- step.status === "CORTEX_STEP_STATUS_RUNNING" ||
- step.status === "CORTEX_STEP_STATUS_WAITING";
+ const hasDetails = display.items.some((item) => item.details.length > 0);
+ const status = subagentStatus(step.status);
return (
-
+
- {expanded && promptText && (
-
-
Instructions:
-
{promptText}
-
- )}
+
+ {display.items.map((item, itemIndex) => (
+
+
+ {item.role}
+ {item.typeName}
+ {item.model && (
+ {item.model}
+ )}
+
+ {expanded && item.details.length > 0 && (
+
+ {item.details.map((itemDetail, detailIndex) => (
+
+
+ {itemDetail.label}:
+
+
+ {itemDetail.text}
+
+
+ ))}
+
+ )}
+
+ ))}
+
);
}
diff --git a/packages/web/src/styles/step-cards.css b/packages/web/src/styles/step-cards.css
index 5b6a5a9..b60e04a 100644
--- a/packages/web/src/styles/step-cards.css
+++ b/packages/web/src/styles/step-cards.css
@@ -694,7 +694,7 @@
.subagent-card .subagent-role {
font-weight: 600;
color: var(--text-primary);
- margin-right: 6px;
+ min-width: 0;
}
.subagent-type-badge {
@@ -707,18 +707,60 @@
background: var(--bg-hover);
color: var(--accent);
border: 1px solid var(--border-subtle);
- margin-right: 8px;
+ white-space: nowrap;
}
-.step-card-subagent-prompt {
+.subagent-status {
+ margin-left: auto;
+ color: var(--text-muted);
+ font-size: 11px;
+ white-space: nowrap;
+}
+
+.subagent-list {
border-top: 1px solid var(--border-subtle);
- padding: 10px 12px;
- font-size: 12px;
background: var(--bg-surface);
border-bottom-left-radius: var(--radius-md);
border-bottom-right-radius: var(--radius-md);
}
+.subagent-entry {
+ padding: 8px 14px;
+}
+
+.subagent-entry + .subagent-entry {
+ border-top: 1px solid var(--border-subtle);
+}
+
+.subagent-entry-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+.subagent-entry-role {
+ color: var(--text-secondary);
+ font-size: 12px;
+ font-weight: 500;
+ overflow-wrap: anywhere;
+}
+
+.subagent-model {
+ color: var(--text-muted);
+ font-family: var(--font-mono);
+ font-size: 10px;
+ overflow-wrap: anywhere;
+}
+
+.subagent-details {
+ margin-top: 8px;
+}
+
+.subagent-detail + .subagent-detail {
+ margin-top: 10px;
+}
+
.subagent-prompt-label {
font-weight: 600;
color: var(--text-muted);
diff --git a/packages/web/src/transforms/stepsToMessages.ts b/packages/web/src/transforms/stepsToMessages.ts
index 7242953..7ba5d70 100644
--- a/packages/web/src/transforms/stepsToMessages.ts
+++ b/packages/web/src/transforms/stepsToMessages.ts
@@ -1,5 +1,9 @@
-import type { ChatMessage, TrajectoryStep } from "../types";
+import type { ChatMessage, ToolCallData, TrajectoryStep } from "../types";
import { getAskQuestionRequest, getFilePermissionRequest } from "../utils/stepCards";
+import {
+ isSubagentToolName,
+ subagentDataFromStep,
+} from "../utils/subagents";
function textFromItems(items?: { text?: string }[]): string {
if (!items) return "";
@@ -12,11 +16,25 @@ function textFromItems(items?: { text?: string }[]): string {
/** Extract displayable messages from raw trajectory steps */
export function stepsToMessages(steps: TrajectoryStep[]): ChatMessage[] {
const messages: ChatMessage[] = [];
+ const pendingInvokeToolCalls: ToolCallData[] = [];
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
const type = step.type;
+ // Older AG trajectories put invoke_subagent arguments on the planner
+ // response, followed by a payloadless native invocation marker.
+ if (type === "CORTEX_STEP_TYPE_PLANNER_RESPONSE") {
+ // Do not let an incomplete/truncated older invocation become the
+ // fallback for a later planner turn.
+ pendingInvokeToolCalls.length = 0;
+ for (const toolCall of step.plannerResponse?.toolCalls ?? []) {
+ if (toolCall.name === "invoke_subagent") {
+ pendingInvokeToolCalls.push(toolCall);
+ }
+ }
+ }
+
// File permission request: emit as a dedicated message type
const fpr = getFilePermissionRequest(step);
if (fpr) {
@@ -46,22 +64,27 @@ export function stepsToMessages(steps: TrajectoryStep[]): ChatMessage[] {
continue;
}
- const toolName = step.metadata?.toolCall?.name ?? "";
- const isSubagent =
- toolName === "invoke_subagent" ||
- toolName === "define_subagent" ||
- toolName === "manage_subagents" ||
- toolName === "send_message" ||
+ const toolName = step.metadata?.toolCall?.name;
+ const isNativeSubagent =
type === "CORTEX_STEP_TYPE_INVOKE_SUBAGENT" ||
type === "CORTEX_STEP_TYPE_SUBAGENT";
+ const isSubagent = isSubagentToolName(toolName) || isNativeSubagent;
if (isSubagent) {
+ const fallbackToolCall = isNativeSubagent
+ ? pendingInvokeToolCalls.shift()
+ : undefined;
+ if (!isNativeSubagent && toolName === "invoke_subagent") {
+ pendingInvokeToolCalls.shift();
+ }
+ const subagent = subagentDataFromStep(step, fallbackToolCall);
messages.push({
role: "system",
content: "",
stepIndex: i,
type: "CORTEX_STEP_TYPE_SUBAGENT",
step,
+ subagent,
});
continue;
}
diff --git a/packages/web/src/types/index.ts b/packages/web/src/types/index.ts
index 7b0beff..815f5bb 100644
--- a/packages/web/src/types/index.ts
+++ b/packages/web/src/types/index.ts
@@ -124,6 +124,7 @@ export interface TrajectoryStep {
metadata?: StepMetadata;
userInput?: { items: StepItem[]; media?: unknown[] };
plannerResponse?: PlannerResponseData;
+ invokeSubagent?: InvokeSubagentData;
runCommand?: RunCommandData;
codeAction?: CodeActionData;
commandStatus?: CommandStatusData;
@@ -146,23 +147,54 @@ export interface PlannerResponseData {
modifiedResponse?: string;
thinking?: string;
thinkingDuration?: string;
+ toolCalls?: ToolCallData[];
+}
+
+export interface ToolCallData {
+ id?: string;
+ name?: string;
+ argumentsJson?: string;
}
export interface StepMetadata {
createdAt?: string;
completedAt?: string;
source?: string;
- toolCall?: {
- id?: string;
- name?: string;
- argumentsJson?: string;
- };
+ executionId?: string;
+ toolCall?: ToolCallData;
+ toolSummary?: string;
+ toolAction?: string;
sourceTrajectoryStepInfo?: {
trajectoryId?: string;
stepIndex?: number;
};
}
+export interface NativeSubagentSpec {
+ typeName?: string;
+ role?: string;
+ initialPrompt?: string;
+ model?: string;
+ modelTier?: string;
+}
+
+export interface SubagentResult {
+ conversationId?: string;
+ logAbsoluteUri?: string;
+ workspaceUris?: string[];
+}
+
+/** Native payload of CORTEX_STEP_TYPE_INVOKE_SUBAGENT. */
+export interface InvokeSubagentData {
+ subagents?: NativeSubagentSpec[];
+ taskMode?: boolean;
+ results?: SubagentResult[];
+ /** Legacy single-subagent fields retained by the AG protocol. */
+ subagentName?: string;
+ prompt?: string;
+ conversationId?: string;
+}
+
export interface RunCommandData {
command?: string;
commandLine?: string;
@@ -257,6 +289,29 @@ export interface StepItem {
text?: string;
}
+export type SubagentToolKind = "invoke" | "define" | "manage" | "message";
+
+export interface SubagentDisplayDetail {
+ label: string;
+ text: string;
+}
+
+export interface SubagentDisplayItem {
+ role: string;
+ typeName: string;
+ model?: string;
+ details: SubagentDisplayDetail[];
+}
+
+/** Sanitized, tool-independent data consumed by SubagentCard. */
+export interface SubagentDisplayData {
+ toolName: string;
+ kind: SubagentToolKind;
+ title: string;
+ action?: string;
+ items: SubagentDisplayItem[];
+}
+
/** Normalized message for display */
export interface ChatMessage {
role: "user" | "assistant" | "system";
@@ -265,6 +320,8 @@ export interface ChatMessage {
type: string;
/** Original step data for rich rendering */
step?: TrajectoryStep;
+ /** Normalized subagent data for rich rendering */
+ subagent?: SubagentDisplayData;
/** Media attachments (images/video) */
media?: unknown[];
/** Extended thinking / chain-of-thought content */
diff --git a/packages/web/src/utils/subagents.ts b/packages/web/src/utils/subagents.ts
new file mode 100644
index 0000000..f788028
--- /dev/null
+++ b/packages/web/src/utils/subagents.ts
@@ -0,0 +1,239 @@
+import type {
+ SubagentDisplayData,
+ SubagentDisplayDetail,
+ SubagentDisplayItem,
+ SubagentToolKind,
+ ToolCallData,
+ TrajectoryStep,
+} from "../types";
+
+const SUBAGENT_TOOL_KINDS: Record
= {
+ invoke_subagent: "invoke",
+ define_subagent: "define",
+ manage_subagents: "manage",
+ send_message: "message",
+};
+
+type JsonRecord = Record;
+
+function isRecord(value: unknown): value is JsonRecord {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+
+function parseArguments(argumentsJson?: string): JsonRecord | undefined {
+ if (!argumentsJson) return undefined;
+ try {
+ const parsed: unknown = JSON.parse(argumentsJson);
+ return isRecord(parsed) ? parsed : undefined;
+ } catch {
+ return undefined;
+ }
+}
+
+function stringField(
+ value: JsonRecord | undefined,
+ ...names: string[]
+): string | undefined {
+ if (!value) return undefined;
+ for (const name of names) {
+ const field = value[name];
+ if (typeof field === "string" && field.trim()) return field.trim();
+ }
+ return undefined;
+}
+
+function stringArrayField(
+ value: JsonRecord | undefined,
+ ...names: string[]
+): string[] {
+ if (!value) return [];
+ for (const name of names) {
+ const field = value[name];
+ if (!Array.isArray(field)) continue;
+ return field.filter(
+ (entry): entry is string => typeof entry === "string" && !!entry.trim(),
+ );
+ }
+ return [];
+}
+
+function detail(label: string, text?: string): SubagentDisplayDetail[] {
+ return text ? [{ label, text }] : [];
+}
+
+function parsedSubagents(args?: JsonRecord): JsonRecord[] {
+ const value = args?.Subagents ?? args?.subagents;
+ return Array.isArray(value) ? value.filter(isRecord) : [];
+}
+
+function invokeItems(
+ step: TrajectoryStep,
+ args?: JsonRecord,
+): SubagentDisplayItem[] {
+ const nativeItems = step.invokeSubagent?.subagents ?? [];
+ if (nativeItems.length > 0) {
+ return nativeItems.map((subagent) => ({
+ role: subagent.role?.trim() || "Subagent",
+ typeName: subagent.typeName?.trim() || "subagent",
+ model: subagent.model?.trim() || subagent.modelTier?.trim() || undefined,
+ details: detail("Instructions", subagent.initialPrompt?.trim()),
+ }));
+ }
+
+ const items = parsedSubagents(args).map((subagent) => {
+ const model = stringField(
+ subagent,
+ "Model",
+ "model",
+ "ModelTier",
+ "modelTier",
+ );
+ return {
+ role: stringField(subagent, "Role", "role") ?? "Subagent",
+ typeName:
+ stringField(subagent, "TypeName", "typeName", "Name", "name") ??
+ "subagent",
+ model,
+ details: detail(
+ "Instructions",
+ stringField(
+ subagent,
+ "Prompt",
+ "prompt",
+ "InitialPrompt",
+ "initialPrompt",
+ ),
+ ),
+ };
+ });
+ if (items.length > 0) return items;
+
+ const role =
+ step.invokeSubagent?.subagentName?.trim() ||
+ stringField(args, "SubagentName", "subagentName", "Role", "role");
+ const prompt =
+ step.invokeSubagent?.prompt?.trim() ||
+ stringField(args, "Prompt", "prompt");
+
+ return [
+ {
+ role: role || "Subagent",
+ typeName: "subagent",
+ details: detail("Instructions", prompt),
+ },
+ ];
+}
+
+function defineItems(args?: JsonRecord): SubagentDisplayItem[] {
+ const name = stringField(args, "name", "Name") ?? "Subagent definition";
+ const description = stringField(args, "description", "Description");
+ const systemPrompt = stringField(args, "system_prompt", "systemPrompt");
+ const details: SubagentDisplayDetail[] = [];
+ if (description) details.push({ label: "Description", text: description });
+ if (systemPrompt) details.push({ label: "System prompt", text: systemPrompt });
+ return [{ role: name, typeName: "definition", details }];
+}
+
+function messageItems(args?: JsonRecord): SubagentDisplayItem[] {
+ const recipient =
+ stringField(args, "Recipient", "recipient") ?? "Subagent";
+ const message = stringField(args, "Message", "message");
+ return [
+ {
+ role: recipient,
+ typeName: "message",
+ details: detail("Message", message),
+ },
+ ];
+}
+
+function manageItems(args?: JsonRecord): SubagentDisplayItem[] {
+ const action = stringField(args, "Action", "action") ?? "Manage";
+ const ids = stringArrayField(
+ args,
+ "ConversationIds",
+ "conversationIds",
+ "conversation_ids",
+ );
+ return [
+ {
+ role: action.replaceAll("_", " "),
+ typeName: "manage",
+ details: detail("Conversation IDs", ids.join("\n")),
+ },
+ ];
+}
+
+function defaultTitle(
+ kind: SubagentToolKind,
+ items: SubagentDisplayItem[],
+ args?: JsonRecord,
+): string {
+ switch (kind) {
+ case "invoke":
+ return items.length === 1
+ ? "Subagent Invoked"
+ : `${items.length} Subagents Invoked`;
+ case "define":
+ return `Define ${items[0]?.role ?? "Subagent"}`;
+ case "message":
+ return `Message to ${items[0]?.role ?? "Subagent"}`;
+ case "manage": {
+ const action = stringField(args, "Action", "action")?.toLowerCase();
+ if (action === "list") return "List Subagents";
+ if (action === "kill_all") return "Stop All Subagents";
+ if (action === "kill") return "Stop Subagents";
+ return "Manage Subagents";
+ }
+ }
+}
+
+export function isSubagentToolName(name?: string): name is string {
+ return (
+ !!name && Object.prototype.hasOwnProperty.call(SUBAGENT_TOOL_KINDS, name)
+ );
+}
+
+/**
+ * Converts both current native AG subagent steps and older tool-call-shaped
+ * steps into a small display model. The fallback call covers older captures
+ * where the native marker has no payload and arguments only exist on the
+ * preceding planner response.
+ */
+export function subagentDataFromStep(
+ step: TrajectoryStep,
+ fallbackToolCall?: ToolCallData,
+): SubagentDisplayData | undefined {
+ const nativeInvoke =
+ step.type === "CORTEX_STEP_TYPE_INVOKE_SUBAGENT" ||
+ step.type === "CORTEX_STEP_TYPE_SUBAGENT";
+ const toolCall = step.metadata?.toolCall ?? fallbackToolCall;
+ const toolName = nativeInvoke
+ ? toolCall?.name || "invoke_subagent"
+ : toolCall?.name;
+ if (!isSubagentToolName(toolName)) return undefined;
+
+ const kind = SUBAGENT_TOOL_KINDS[toolName];
+ const args = parseArguments(toolCall?.argumentsJson);
+ const items =
+ kind === "invoke"
+ ? invokeItems(step, args)
+ : kind === "define"
+ ? defineItems(args)
+ : kind === "message"
+ ? messageItems(args)
+ : manageItems(args);
+ const legacySummary = stringField(args, "toolSummary", "tool_summary");
+ const legacyAction = stringField(args, "toolAction", "tool_action");
+
+ return {
+ toolName,
+ kind,
+ title:
+ step.metadata?.toolSummary ||
+ legacySummary ||
+ defaultTitle(kind, items, args),
+ action: step.metadata?.toolAction || legacyAction,
+ items,
+ };
+}