Skip to content
Open
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
5 changes: 5 additions & 0 deletions src/browser/features/Tools/Shared/NestedToolRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ToolStatus } from "./toolUtils";
import { getToolComponent } from "./getToolComponent";
import { HookOutputDisplay, extractHookDuration, extractHookOutput } from "./HookOutputDisplay";
import { ToolNameProvider } from "../../Messages/ToolNameContext";
import type { WorkflowRunToolAttachment } from "@/common/orpc/schemas/message";

interface NestedToolRendererProps {
toolName: string;
Expand All @@ -12,6 +13,8 @@ interface NestedToolRendererProps {
workspaceId?: string;
toolCallId?: string;
toolCallTimestamp?: number;
/** Persisted run identity for nested workflow tool calls; only the workflow card consumes it. */
workflowRunHint?: WorkflowRunToolAttachment;
}

/**
Expand All @@ -26,6 +29,7 @@ export const NestedToolRenderer: React.FC<NestedToolRendererProps> = ({
workspaceId,
toolCallId,
toolCallTimestamp,
workflowRunHint,
}) => {
const ToolComponent = getToolComponent(toolName, input);
const hookOutput = extractHookOutput(output);
Expand All @@ -43,6 +47,7 @@ export const NestedToolRenderer: React.FC<NestedToolRendererProps> = ({
workspaceId={workspaceId}
toolCallId={toolCallId}
toolCallTimestamp={toolCallTimestamp}
workflowRunHint={workflowRunHint}
/>
</ToolNameProvider>
{hookOutput && <HookOutputDisplay output={hookOutput} durationMs={hookDuration} />}
Expand Down
1 change: 1 addition & 0 deletions src/browser/features/Tools/Shared/NestedToolsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const NestedToolsContainer: React.FC<NestedToolsContainerProps> = ({
workspaceId={workspaceId}
toolCallId={call.toolCallId}
toolCallTimestamp={call.timestamp ?? toolCallTimestamp}
workflowRunHint={call.workflowRun}
/>
);
})}
Expand Down
4 changes: 4 additions & 0 deletions src/browser/features/Tools/Shared/codeExecutionTypes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { WorkflowRunToolAttachment } from "@/common/orpc/schemas/message";

export type {
CodeExecutionConsoleRecord as ConsoleRecord,
CodeExecutionResult,
Expand All @@ -13,4 +15,6 @@ export interface NestedToolCall {
state: "input-available" | "output-available" | "output-redacted";
failed?: boolean;
timestamp?: number;
/** Durable run identity persisted for nested workflow tool calls. */
workflowRun?: WorkflowRunToolAttachment;
}
12 changes: 12 additions & 0 deletions src/browser/features/Tools/Shared/getToolComponent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ describe("getToolComponent", () => {
expect(component).toBe(WorkflowRunToolCall);
});

test("routes kernel-bounded workflow_run args to the workflow card", () => {
// Kernel-nested calls with oversized launch args arrive as a marker; the
// card renders from the attached durable run instead of raw JSON.
const marker = { __kernelBounded: true, bytes: 18_457, preview: '{"script_path":"skill…' };
expect(getToolComponent("workflow_run", marker)).toBe(WorkflowRunToolCall);
expect(
getToolComponent("workflow_run", { ...marker, script_path: "skill://demo/workflow.js" })
).toBe(WorkflowRunToolCall);
// Other tools keep the generic fallback for bounded args.
expect(getToolComponent("bash", marker)).toBe(GenericToolCall);
});

test("returns WorkflowResumeToolCall for workflow_resume", () => {
const component = getToolComponent("workflow_resume", { run_id: "wfr_123" });
expect(component).toBe(WorkflowResumeToolCall);
Expand Down
8 changes: 7 additions & 1 deletion src/browser/features/Tools/Shared/getToolComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,14 @@ const TOOL_REGISTRY: Record<string, ToolRegistryEntry> = {
schema: TaskWorkspaceLifecycleToolArgsSchema,
},
workflow_run: {
// Kernel-nested calls can arrive with a __kernelBounded args marker
// (oversized launch args); the card still renders from the attached
// durable run, so don't bounce those to the generic JSON renderer.
component: WorkflowRunToolCall,
schema: TOOL_DEFINITIONS.workflow_run.schema,
schema: z.union([
TOOL_DEFINITIONS.workflow_run.schema,
z.object({ __kernelBounded: z.literal(true), script_path: z.string().nullish() }),
]),
},
workflow_resume: {
component: WorkflowResumeToolCall,
Expand Down
78 changes: 78 additions & 0 deletions src/browser/features/Tools/WorkflowRunToolCall.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2136,6 +2136,84 @@ describe("WorkflowRunToolCall", () => {
);
});

test("renders a workflow card from kernel-bounded args via the attached run hint", async () => {
// A kernel-nested workflow_run whose launch args were replaced by a
// __kernelBounded marker must still render the live card: identity comes
// from the attached run hint, and the marker offers nothing for the
// heuristic name+args discovery to match (listRuns must stay untouched).
const attachedRun = createWorkflowRunForExpansionTest({ id: "wfr_kernel", status: "running" });
const listRuns = mock(async () => []);
const getRun = mock(async () => attachedRun);

const view = render(
<APIHarness client={{ workflows: { listRuns, getRun } }}>
<ThemeProvider forcedTheme="dark">
<TooltipProvider>
<WorkflowRunToolCall
args={{ __kernelBounded: true, bytes: 18_457, preview: "{…}" }}
status="executing"
workspaceId={TEST_WORKSPACE_ID}
toolCallId="nested-workflow-1"
workflowRunHint={{ runId: "wfr_kernel", run: attachedRun }}
/>
</TooltipProvider>
</ThemeProvider>
</APIHarness>
);

// The workflow name renders in both the header and the script card.
expect(view.getAllByText("deep-research").length).toBeGreaterThan(0);
expect(view.getByText("wfr_kernel")).toBeTruthy();
expect(listRuns).not.toHaveBeenCalled();
await waitFor(() =>
expect(getRun).toHaveBeenCalledWith({ workspaceId: TEST_WORKSPACE_ID, runId: "wfr_kernel" })
);
});

test("recovers run identity from a kernel-bounded result marker", async () => {
// With both args and result bounded, the retained runId on the result
// marker is the only identity; expanding the card must refetch the
// durable run by that id.
const completedRun = createWorkflowRunForExpansionTest({
id: "wfr_bounded_result",
status: "completed",
});
const getRun = mock(async () => completedRun);

const view = render(
<APIHarness client={{ workflows: { getRun } }}>
<ThemeProvider forcedTheme="dark">
<TooltipProvider>
<WorkflowRunToolCall
args={{ __kernelBounded: true, bytes: 18_457, preview: "{…}" }}
result={
{
__kernelBounded: true,
bytes: 40_000,
preview: "{…}",
runId: "wfr_bounded_result",
status: "completed",
} as never
}
status="completed"
workspaceId={TEST_WORKSPACE_ID}
toolCallId="nested-workflow-2"
/>
</TooltipProvider>
</ThemeProvider>
</APIHarness>
);

fireEvent.click(getWorkflowHeader(view));
await waitFor(() =>
expect(getRun).toHaveBeenCalledWith({
workspaceId: TEST_WORKSPACE_ID,
runId: "wfr_bounded_result",
})
);
await waitFor(() => expect(view.getAllByText("deep-research").length).toBeGreaterThan(0));
});

test("uses resume result status when the attachment run snapshot is stale", async () => {
const staleRun = {
...createWorkflowRunForExpansionTest({ id: "wfr_resume_stale", status: "running" }),
Expand Down
72 changes: 60 additions & 12 deletions src/browser/features/Tools/WorkflowRunToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,32 @@ import {
type WorkflowToolLiveRunState,
} from "@/browser/stores/WorkspaceStore";
import { workflowScriptMatchesPath } from "@/browser/utils/workflowRunScriptPaths";
import {
isKernelBoundedMarker,
type KernelBoundedMarker,
} from "@/common/utils/tools/kernelBoundedMarker";
import { MarkdownRenderer } from "../Messages/MarkdownRenderer";

type WorkflowRunToolDisplayArgs =
type WorkflowRunToolLaunchArgs =
| WorkflowRunToolArgs
| (Omit<WorkflowRunToolArgs, "script_path"> & { script_path?: string; name: string });

/**
* Kernel-nested calls can arrive with their launch args replaced by a
* __kernelBounded marker; capture retains script_path when it fits (see
* retainPersistenceCriticalArgsFields). Run identity then comes from the
* workflow-run-attached hint or the result's retained runId.
*/
type KernelBoundedWorkflowRunArgs = KernelBoundedMarker & { script_path?: string | null };

type WorkflowRunToolDisplayArgs = WorkflowRunToolLaunchArgs | KernelBoundedWorkflowRunArgs;

function getWorkflowRunLaunchArgs(
args: WorkflowRunToolDisplayArgs
): WorkflowRunToolLaunchArgs | null {
return isKernelBoundedMarker(args) ? null : args;
}

interface WorkflowRunToolCallProps {
args: WorkflowRunToolDisplayArgs;
result?: WorkflowRunToolResult;
Expand Down Expand Up @@ -204,7 +224,21 @@ async function updateWorkflowRunFromAction(input: {
function isWorkflowRunSuccessResult(
value: WorkflowRunToolResult | undefined
): value is WorkflowRunToolSuccessResult {
return value != null && !isToolErrorResult(value);
return value != null && !isToolErrorResult(value) && !isKernelBoundedMarker(value);
}

/** RunId/status retained on a kernel-bounded result marker (see retainWorkflowResultIdentityFields). */
function getKernelBoundedResultIdentity(
result: unknown
): { runId: string; status?: string } | null {
if (!isKernelBoundedMarker(result)) {
return null;
}
const { runId, status } = result as { runId?: unknown; status?: unknown };
if (typeof runId !== "string" || runId.length === 0) {
return null;
}
return { runId, ...(typeof status === "string" ? { status } : {}) };
}

// Schema-shaped workflow agent reports carry structuredOutput only; their placeholder
Expand Down Expand Up @@ -1133,12 +1167,16 @@ function getWorkflowRunDisplayName(args: WorkflowRunToolDisplayArgs): string {
if (scriptPath.length > 0) {
return scriptPath;
}
if (isKernelBoundedMarker(args)) {
// Placeholder until the attached durable run supplies the real name.
return "workflow";
}
return args.script_source != null ? "inline workflow" : "";
}

function workflowRunMatchesLaunchArgs(
run: WorkflowRunRecord,
args: WorkflowRunToolDisplayArgs
args: WorkflowRunToolLaunchArgs
): boolean {
const invocationArgs = args.args ?? {};
if (!workflowArgsEqual(run.args ?? {}, invocationArgs)) {
Expand All @@ -1154,7 +1192,7 @@ function workflowRunMatchesLaunchArgs(

function findForegroundWorkflowRun(input: {
runs: readonly WorkflowRunRecord[];
args: WorkflowRunToolDisplayArgs;
args: WorkflowRunToolLaunchArgs;
startedAt?: number;
}): WorkflowRunRecord | null {
const candidates = input.runs.filter(
Expand Down Expand Up @@ -1241,6 +1279,10 @@ export const WorkflowRunToolCall: React.FC<WorkflowRunToolCallProps> = ({
const registerCommandSource = commandRegistry?.registerSource;
const errorResult = isToolErrorResult(result) ? result : null;
const successResult = isWorkflowRunSuccessResult(result) ? result : null;
// Kernel-nested calls: launch args may be a bounded marker (no launch args
// to match against) and the result marker may retain only runId/status.
const launchArgs = getWorkflowRunLaunchArgs(args);
const boundedResultIdentity = getKernelBoundedResultIdentity(result);
const liveWorkflowRunHint = useWorkflowToolLiveRun(workspaceId, toolCallId);
const workflowRunHint = explicitWorkflowRunHint ?? liveWorkflowRunHint;
const [refreshedRun, setRefreshedRun] = useState<WorkflowRunRecord | null>(null);
Expand All @@ -1260,7 +1302,8 @@ export const WorkflowRunToolCall: React.FC<WorkflowRunToolCallProps> = ({
const selectedRun = selectWorkflowRunSnapshot({
// knownRunId (workflow_resume) and workflowRunHint provide exact identities before any
// result arrives, which also disables the heuristic name+args foreground discovery below.
runId: successResult?.runId ?? knownRunId ?? workflowRunHint?.runId,
runId:
successResult?.runId ?? boundedResultIdentity?.runId ?? knownRunId ?? workflowRunHint?.runId,
baseRun,
refreshedRun,
});
Expand All @@ -1274,13 +1317,13 @@ export const WorkflowRunToolCall: React.FC<WorkflowRunToolCallProps> = ({
const displayStatus =
successResult?.run == null && successResult?.status != null && !hasRefreshedRunSnapshot
? successResult.status
: (run?.status ?? successResult?.status ?? status);
: (run?.status ?? successResult?.status ?? boundedResultIdentity?.status ?? status);
const parentRunActive = isWorkflowDisplayStatusActive(displayStatus);
const displayEventSequence = getLatestWorkflowEventSequence(run);
const resultValue = successResult?.result ?? getLatestResultEvent(run);
const reportMarkdown = getReportMarkdown(resultValue);
const structuredOutput = getStructuredOutput(resultValue);
const invocationArgs = run?.args ?? args.args ?? {};
const invocationArgs = run?.args ?? launchArgs?.args ?? {};
const events = run?.events ?? [];
const displayRows = getWorkflowDisplayRows(events);
const headerStatus = toToolStatus(displayStatus);
Expand Down Expand Up @@ -1335,7 +1378,8 @@ export const WorkflowRunToolCall: React.FC<WorkflowRunToolCallProps> = ({
// A uniquely discovered foreground run is actionable before the blocking tool call returns.
const discoveredForegroundRunConfirmed =
status === "executing" &&
args.run_in_background !== true &&
launchArgs != null &&
launchArgs.run_in_background !== true &&
workspaceId != null &&
refreshedRun != null &&
runId === refreshedRun.id &&
Expand All @@ -1353,6 +1397,7 @@ export const WorkflowRunToolCall: React.FC<WorkflowRunToolCallProps> = ({
(workflowRunHint.run?.workspaceId == null || workflowRunHint.run.workspaceId === workspaceId);
const runIdentityConfirmed =
successResult?.runId != null ||
boundedResultIdentity?.runId != null ||
baseRun?.id != null ||
discoveredForegroundRunConfirmed ||
discoveredKnownRunConfirmed ||
Expand Down Expand Up @@ -1496,7 +1541,10 @@ export const WorkflowRunToolCall: React.FC<WorkflowRunToolCallProps> = ({
workspaceId == null ||
runId != null ||
status !== "executing" ||
args.run_in_background === true
// Bounded marker args carry nothing to match against; identity arrives
// via the workflow-run-attached hint instead.
launchArgs == null ||
launchArgs.run_in_background === true
) {
return;
}
Expand All @@ -1507,7 +1555,7 @@ export const WorkflowRunToolCall: React.FC<WorkflowRunToolCallProps> = ({
const runs = await apiState.api.workflows.listRuns({ workspaceId });
const foregroundRun = findForegroundWorkflowRun({
runs,
args,
args: launchArgs,
startedAt: discoveryFreshnessBound,
});
if (!ignore && foregroundRun != null) {
Expand All @@ -1526,9 +1574,9 @@ export const WorkflowRunToolCall: React.FC<WorkflowRunToolCallProps> = ({
ignore = true;
window.clearInterval(interval);
};
}, [apiState?.api, args, runId, discoveryFreshnessBound, status, workspaceId]);
}, [apiState?.api, launchArgs, runId, discoveryFreshnessBound, status, workspaceId]);

const exactDiscoveryRunId = knownRunId ?? workflowRunHint?.runId;
const exactDiscoveryRunId = knownRunId ?? workflowRunHint?.runId ?? boundedResultIdentity?.runId;

useEffect(() => {
// workflow_resume args and workflowRunHint carry exact run identity, so fetch by ID while
Expand Down
25 changes: 17 additions & 8 deletions src/browser/stores/WorkspaceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1939,17 +1939,26 @@ export class WorkspaceStore {
const activeBashToolCallIds = new Set<string>();
const activeAdvisorToolCallIds = new Set<string>();
const activeWorkflowToolCallIds = new Set<string>();
const collectToolCallId = (toolName: string, toolCallId: string) => {
if (toolName === "bash") {
activeBashToolCallIds.add(toolCallId);
}
if (toolName === "advisor") {
activeAdvisorToolCallIds.add(toolCallId);
}
if (isWorkflowRunEmittingToolName(toolName)) {
activeWorkflowToolCallIds.add(toolCallId);
}
};
for (const msg of aggregator.getDisplayedMessages()) {
if (msg.type !== "tool") continue;

if (msg.toolName === "bash") {
activeBashToolCallIds.add(msg.toolCallId);
}
if (msg.toolName === "advisor") {
activeAdvisorToolCallIds.add(msg.toolCallId);
}
if (isWorkflowRunEmittingToolName(msg.toolName)) {
activeWorkflowToolCallIds.add(msg.toolCallId);
collectToolCallId(msg.toolName, msg.toolCallId);
// Kernel-nested calls (code_execution) render inside the parent card but
// key their live state by their own nested ids; without collecting them
// the sweep would prune a nested workflow/bash card's live state mid-run.
for (const nested of msg.nestedCalls ?? []) {
collectToolCallId(nested.toolName, nested.toolCallId);
}
}

Expand Down
Loading
Loading