Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/hooks/tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -659,8 +659,8 @@ If a value is too large for the environment, it may be omitted (not set). Mux al
| `MUX_TOOL_INPUT_FILTER` | `filter` | string | Optional regex to filter bash task output lines. By default, only matching lines are returned. When filter_exclude is true, matching lines are excluded instead. Non-matching lines are discarded and cannot be retrieved later. |
| `MUX_TOOL_INPUT_FILTER_EXCLUDE` | `filter_exclude` | boolean | When true, lines matching 'filter' are excluded instead of kept. Requires 'filter' to be set. |
| `MUX_TOOL_INPUT_MIN_COMPLETED` | `min_completed` | number | Number of awaited tasks that must complete before this call returns. Defaults to 1, so by default task_await returns as soon as the FIRST awaited task completes, letting you act on it while the rest keep running. The result still includes every task complete at that moment plus current status (running/queued) for the rest. Tasks that have not yet completed keep running and remain re-awaitable on a later task_await call. Raise this (e.g. set it to the total number of awaited tasks) when you genuinely need more before proceeding — for example best-of-N synthesis that must compare every candidate. Clamped to the number of awaited tasks; values above that behave like 'wait for all'. |
| `MUX_TOOL_INPUT_TASK_IDS_<INDEX>` | `task_ids[<INDEX>]` | string | List of task IDs or workflow run IDs to await — use only real IDs returned by prior task, bash, or workflow_run results; never fabricate an ID. task_list can rediscover sub-agent/background bash IDs, but workflow run rediscovery is done by omitting task_ids. When omitted, waits for active descendant tasks and workflow runs of the current workspace, excluding workflow-owned sub-agents and their background bash tasks because those results are consumed through workflow runs. |
| `MUX_TOOL_INPUT_TASK_IDS_COUNT` | `task_ids.length` | number | Number of elements in task_ids (List of task IDs or workflow run IDs to await — use only real IDs returned by prior task, bash, or workflow_run results; never fabricate an ID. task_list can rediscover sub-agent/background bash IDs, but workflow run rediscovery is done by omitting task_ids. When omitted, waits for active descendant tasks and workflow runs of the current workspace, excluding workflow-owned sub-agents and their background bash tasks because those results are consumed through workflow runs.) |
| `MUX_TOOL_INPUT_TASK_IDS_<INDEX>` | `task_ids[<INDEX>]` | string | List of task IDs or workflow run IDs to await — use only real IDs returned by prior task, bash, or workflow_run results; never fabricate an ID. task_list can rediscover sub-agent/background bash IDs, but top-level workflow run rediscovery is done by omitting task_ids. When omitted, waits for active descendant tasks and top-level workflow runs of the current workspace, excluding workflow-owned sub-agents/background bash tasks and nested child workflow runs because those results are consumed through parent workflow runs. |
| `MUX_TOOL_INPUT_TASK_IDS_COUNT` | `task_ids.length` | number | Number of elements in task_ids (List of task IDs or workflow run IDs to await — use only real IDs returned by prior task, bash, or workflow_run results; never fabricate an ID. task_list can rediscover sub-agent/background bash IDs, but top-level workflow run rediscovery is done by omitting task_ids. When omitted, waits for active descendant tasks and top-level workflow runs of the current workspace, excluding workflow-owned sub-agents/background bash tasks and nested child workflow runs because those results are consumed through parent workflow runs.) |
| `MUX_TOOL_INPUT_TIMEOUT_SECS` | `timeout_secs` | number | Maximum time to wait in seconds for each task. For bash tasks, this waits for NEW output (or process exit). If exceeded, the result returns status=queued\|starting\|running\|awaiting_report (task is still active). Defaults to 600 seconds (10 minutes) if not specified. Set to 0 for a non-blocking status check. |

</details>
Expand Down
68 changes: 68 additions & 0 deletions src/browser/features/Tools/WorkflowRunToolCall.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,74 @@ describe("WorkflowRunToolCall", () => {
expect(view.container.textContent).toContain("durationMs");
});

test("coalesces nested workflow start and completion events into one row with child run details", () => {
const view = render(
<ThemeProvider forcedTheme="dark">
<TooltipProvider>
<WorkflowRunToolCall
args={{ name: "parent-simple", args: {}, run_in_background: false }}
status="completed"
result={{
status: "completed",
runId: "wfr_parent",
result: { reportMarkdown: "done" },
run: {
id: "wfr_parent",
workspaceId: "workspace-1",
definition: {
name: "parent-simple",
description: "Parent",
scope: "scratch",
executable: true,
},
definitionSource: "export default function workflow() { return null; }",
definitionHash: "sha256:parent",
args: {},
status: "completed",
createdAt: "2026-05-29T00:00:00.000Z",
updatedAt: "2026-05-29T00:00:01.000Z",
events: [
{
sequence: 1,
type: "workflow",
at: "2026-05-29T00:00:00.000Z",
stepId: "child-simple",
runId: "wfr_child_abc",
name: "child-simple",
status: "started",
},
{
sequence: 2,
type: "workflow",
at: "2026-05-29T00:00:01.000Z",
stepId: "child-simple",
runId: "wfr_child_abc",
name: "child-simple",
status: "completed",
details: { reportMarkdown: "child done", runId: "wfr_child_abc" },
},
{
sequence: 3,
type: "result",
at: "2026-05-29T00:00:01.000Z",
result: { reportMarkdown: "done" },
},
],
steps: [],
},
}}
/>
</TooltipProvider>
</ThemeProvider>
);

fireEvent.click(getWorkflowHeader(view));

expect(view.getByText("Workflow events (1)")).toBeTruthy();
expect(view.getByText("child-simple / child-simple / wfr_child_abc / completed")).toBeTruthy();
expect(view.queryByText("#2")).toBeNull();
});

test("coalesces patch start and applied events into one row with combined details", () => {
const view = render(
<ThemeProvider forcedTheme="dark">
Expand Down
46 changes: 44 additions & 2 deletions src/browser/features/Tools/WorkflowRunToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,16 +209,19 @@ function getStructuredOutput(value: unknown): unknown {

type WorkflowTaskEvent = Extract<WorkflowRunEvent, { type: "task" }>;
type WorkflowActionEvent = Extract<WorkflowRunEvent, { type: "action" }>;
type WorkflowChildEvent = Extract<WorkflowRunEvent, { type: "workflow" }>;
type WorkflowPatchEvent = Extract<WorkflowRunEvent, { type: "patch" }>;

type WorkflowDisplayRow =
| { kind: "event"; event: WorkflowRunEvent }
| { kind: "task"; firstEvent: WorkflowTaskEvent; latestEvent: WorkflowTaskEvent }
| { kind: "action"; firstEvent: WorkflowActionEvent; latestEvent: WorkflowActionEvent }
| { kind: "workflow"; firstEvent: WorkflowChildEvent; latestEvent: WorkflowChildEvent }
| { kind: "patch"; firstEvent: WorkflowPatchEvent; latestEvent: WorkflowPatchEvent };

type WorkflowTaskRow = Extract<WorkflowDisplayRow, { kind: "task" }>;
type WorkflowActionRow = Extract<WorkflowDisplayRow, { kind: "action" }>;
type WorkflowChildRow = Extract<WorkflowDisplayRow, { kind: "workflow" }>;
type WorkflowPatchRow = Extract<WorkflowDisplayRow, { kind: "patch" }>;
interface PendingWorkflowActionRows {
rows: WorkflowActionRow[];
Expand All @@ -229,6 +232,10 @@ function getTaskEventKey(event: WorkflowTaskEvent): string {
return `task:${event.stepId}:${event.taskId}`;
}

function getWorkflowChildEventKey(event: WorkflowChildEvent): string {
return `workflow:${event.stepId}:${event.runId}`;
}

function getPatchEventKey(event: WorkflowPatchEvent): string {
return `patch:${event.stepId}:${event.sourceTaskId}`;
}
Expand Down Expand Up @@ -273,6 +280,7 @@ function getWorkflowDisplayRows(events: readonly WorkflowRunEvent[]): WorkflowDi
const rows: WorkflowDisplayRow[] = [];
const taskRows = new Map<string, WorkflowTaskRow>();
const actionRows = new Map<string, PendingWorkflowActionRows>();
const workflowRows = new Map<string, WorkflowChildRow>();
const patchRows = new Map<string, WorkflowPatchRow>();

for (const event of events) {
Expand All @@ -297,6 +305,24 @@ function getWorkflowDisplayRows(events: readonly WorkflowRunEvent[]): WorkflowDi
continue;
}

if (event.type === "workflow") {
const key = getWorkflowChildEventKey(event);
const existingRow = workflowRows.get(key);
if (existingRow != null) {
existingRow.latestEvent = event;
continue;
}

const row: WorkflowChildRow = {
kind: "workflow",
firstEvent: event,
latestEvent: event,
};
workflowRows.set(key, row);
rows.push(row);
continue;
}

if (event.type === "patch") {
// A patch step emits started → applied/conflict/failed for the same
// stepId+sourceTaskId; collapse them into one row (latest status wins),
Expand Down Expand Up @@ -377,6 +403,9 @@ function getDisplayRowKey(row: WorkflowDisplayRow): string {
if (row.kind === "action") {
return `${getActionEventKey(row.firstEvent)}:${row.firstEvent.sequence}`;
}
if (row.kind === "workflow") {
return getWorkflowChildEventKey(row.firstEvent);
}
if (row.kind === "patch") {
return getPatchEventKey(row.firstEvent);
}
Expand All @@ -396,6 +425,8 @@ function getWorkflowEventLabel(event: WorkflowRunEvent): string {
// Prefer the human-readable sub-agent title (matches the spawned
// workspace title); fall back to stepId for legacy events without one.
return `${event.title ?? event.stepId} / ${event.taskId} / ${event.status}`;
case "workflow":
return `${event.stepId} / ${event.name} / ${event.runId} / ${event.status}`;
case "patch":
return `${event.stepId} / ${event.sourceTaskId} / ${event.status}`;
case "action":
Expand Down Expand Up @@ -423,6 +454,8 @@ function getWorkflowEventDetail(event: WorkflowRunEvent): unknown {
return event.data;
case "result":
return event.result;
case "workflow":
return event.details;
case "patch":
return event.details;
case "action":
Expand Down Expand Up @@ -459,6 +492,13 @@ function getEventTone(event: WorkflowRunEvent): "normal" | "success" | "warning"
}
return event.status === "failed" ? "warning" : "normal";
}
if (event.type === "workflow") {
return event.status === "completed"
? "success"
: event.status === "failed" || event.status === "interrupted"
? "warning"
: "normal";
}
if (event.type === "result") {
return "success";
}
Expand Down Expand Up @@ -551,7 +591,9 @@ function WorkflowEventTooltip(props: {
);
}

function getWorkflowMergedRowDetail(row: WorkflowActionRow | WorkflowPatchRow): unknown {
function getWorkflowMergedRowDetail(
row: WorkflowActionRow | WorkflowChildRow | WorkflowPatchRow
): unknown {
const firstDetail = getWorkflowEventDetail(row.firstEvent);
const latestDetail = getWorkflowEventDetail(row.latestEvent);
if (row.firstEvent === row.latestEvent || row.latestEvent.status === "started") {
Expand Down Expand Up @@ -1544,7 +1586,7 @@ export const WorkflowRunToolCall: React.FC<WorkflowRunToolCallProps> = ({
/>
);
}
if (row.kind === "action" || row.kind === "patch") {
if (row.kind === "action" || row.kind === "workflow" || row.kind === "patch") {
return (
<WorkflowEventRow
key={getDisplayRowKey(row)}
Expand Down
1 change: 1 addition & 0 deletions src/common/orpc/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export {
WorkflowResultSchema,
WorkflowRunEventSchema,
WorkflowRunIdSchema,
WorkflowRunParentSchema,
WorkflowRunRecordSchema,
WorkflowRunStatusSchema,
WorkflowRunStatusTransitionSchema,
Expand Down
20 changes: 20 additions & 0 deletions src/common/orpc/schemas/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ export const WorkflowRunEventSchema = z.discriminatedUnion("type", [
// Optional so legacy persisted events without it still parse.
title: z.string().min(1).optional(),
}),
z.object({
sequence: z.number().int().positive(),
type: z.literal("workflow"),
at: IsoDateTimeSchema,
stepId: z.string().min(1),
runId: WorkflowRunIdSchema,
name: WorkflowNameSchema,
status: z.enum(["started", "running", "backgrounded", "completed", "failed", "interrupted"]),
details: JsonValueSchema.optional(),
}),
z.object({
sequence: z.number().int().positive(),
type: z.literal("patch"),
Expand Down Expand Up @@ -224,6 +234,15 @@ export const WorkflowRunStatusTransitionSchema = z
path: ["to"],
});

export const WorkflowRunParentSchema = z
.object({
runId: WorkflowRunIdSchema,
stepId: z.string().min(1),
inputHash: z.string().min(1),
depth: z.number().int().nonnegative(),
})
.strict();

export const WorkflowRunRecordSchema = z.object({
id: WorkflowRunIdSchema,
workspaceId: z.string().min(1),
Expand All @@ -232,6 +251,7 @@ export const WorkflowRunRecordSchema = z.object({
definitionHash: z.string().min(1),
args: JsonValueSchema,
defaultActionCwd: z.string().min(1).optional(),
parentWorkflow: WorkflowRunParentSchema.optional(),
status: WorkflowRunStatusSchema,
createdAt: IsoDateTimeSchema,
updatedAt: IsoDateTimeSchema,
Expand Down
6 changes: 6 additions & 0 deletions src/common/types/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
WorkflowResultSchema,
WorkflowRunEventSchema,
WorkflowRunIdSchema,
WorkflowRunParentSchema,
WorkflowRunRecordSchema,
WorkflowRunStatusSchema,
WorkflowStepRecordSchema,
Expand All @@ -33,8 +34,13 @@ export type WorkflowResult = z.infer<typeof WorkflowResultSchema>;
export type StructuredTaskOutput = z.infer<typeof StructuredTaskOutputSchema>;
export type WorkflowRunEvent = z.infer<typeof WorkflowRunEventSchema>;
export type WorkflowStepRecord = z.infer<typeof WorkflowStepRecordSchema>;
export type WorkflowRunParent = z.infer<typeof WorkflowRunParentSchema>;
export type WorkflowRunRecord = z.infer<typeof WorkflowRunRecordSchema>;

export function isNestedWorkflowRun(run: WorkflowRunRecord): boolean {
return run.parentWorkflow != null;
}

export function assertWorkflowRunStatusTransition(
from: WorkflowRunStatus,
to: WorkflowRunStatus
Expand Down
Loading
Loading