Skip to content

Commit 42c5411

Browse files
committed
fix(web): wire manage_workflows as a HIL tool with structured action handler
1 parent 46c4826 commit 42c5411

1 file changed

Lines changed: 157 additions & 3 deletions

File tree

apps/web/src/components/sessions/tool-part-renderer.tsx

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const HUMAN_IN_LOOP_TOOLS = new Set([
4343
"manage_streams",
4444
"manage_keys",
4545
"manage_subgraphs",
46+
"manage_workflows",
4647
"deploy_workflow",
4748
"edit_workflow",
4849
"rollback_workflow",
@@ -228,13 +229,16 @@ export function ToolPartRenderer({
228229
const input = part.input as {
229230
action: string;
230231
targets: Array<{ id?: string; name: string; reason?: string }>;
232+
triggerInput?: string;
231233
};
232234
const resourceType =
233235
toolName === "manage_keys"
234236
? "keys"
235237
: toolName === "manage_subgraphs"
236238
? "subgraphs"
237-
: "streams";
239+
: toolName === "manage_workflows"
240+
? "workflows"
241+
: "streams";
238242
return (
239243
<>
240244
<ToolCallIndicator
@@ -250,6 +254,18 @@ export function ToolPartRenderer({
250254
reason: t.reason,
251255
}))}
252256
onConfirm={async () => {
257+
if (toolName === "manage_workflows") {
258+
const result = await executeManageWorkflows(
259+
input.action,
260+
input.targets,
261+
input.triggerInput,
262+
);
263+
addToolOutput({
264+
toolCallId: part.toolCallId,
265+
output: result,
266+
});
267+
return;
268+
}
253269
await executeAction(toolName, input.action, input.targets);
254270
addToolOutput({
255271
toolCallId: part.toolCallId,
@@ -380,10 +396,17 @@ function renderOutputCard(toolName: string, output: Record<string, unknown>) {
380396

381397
case "manage_streams":
382398
case "manage_keys":
383-
case "manage_subgraphs": {
399+
case "manage_subgraphs":
400+
case "manage_workflows": {
384401
const msg = (output as { message?: string }).message;
385402
if ((output as { confirmed?: boolean }).confirmed === false) {
386-
return <SuccessBanner message={msg ?? "Action cancelled"} />;
403+
return (
404+
<SuccessBanner tone="info" message={msg ?? "Action cancelled"} />
405+
);
406+
}
407+
const errored = (output as { error?: string }).error;
408+
if (errored) {
409+
return <SuccessBanner tone="error" message={errored} />;
387410
}
388411
return <SuccessBanner message={msg ?? "Action completed"} />;
389412
}
@@ -602,6 +625,137 @@ async function executeAction(
602625
await Promise.allSettled(calls);
603626
}
604627

628+
/**
629+
* Handler for `manage_workflows` HIL confirms. Split from the generic
630+
* `executeAction` because workflow operations need structured response
631+
* handling:
632+
* - `trigger` returns a runId we want to pass back to the agent so it can
633+
* tail the run in a follow-up step.
634+
* - Any non-2xx upstream response needs to surface as a chat error rather
635+
* than being silently swallowed by `Promise.allSettled`.
636+
*/
637+
async function executeManageWorkflows(
638+
action: string,
639+
targets: Array<{ id?: string; name: string; reason?: string }>,
640+
triggerInput?: string,
641+
): Promise<{
642+
confirmed: boolean;
643+
ok: boolean;
644+
action: string;
645+
name?: string;
646+
runId?: string;
647+
message?: string;
648+
error?: string;
649+
}> {
650+
const first = targets[0];
651+
if (!first) {
652+
return {
653+
confirmed: true,
654+
ok: false,
655+
action,
656+
error: "No workflow targets provided",
657+
};
658+
}
659+
660+
try {
661+
if (action === "trigger") {
662+
let inputBody: Record<string, unknown> = {};
663+
if (triggerInput) {
664+
try {
665+
const parsed = JSON.parse(triggerInput);
666+
if (parsed && typeof parsed === "object") {
667+
inputBody = { input: parsed };
668+
}
669+
} catch {
670+
return {
671+
confirmed: true,
672+
ok: false,
673+
action,
674+
name: first.name,
675+
error: "triggerInput was not valid JSON",
676+
};
677+
}
678+
}
679+
const res = await fetch(`/api/workflows/${first.name}/trigger`, {
680+
method: "POST",
681+
headers: { "Content-Type": "application/json" },
682+
credentials: "same-origin",
683+
body: JSON.stringify(inputBody),
684+
});
685+
const body = (await res.json().catch(() => ({}))) as {
686+
runId?: string;
687+
error?: string;
688+
};
689+
if (!res.ok) {
690+
return {
691+
confirmed: true,
692+
ok: false,
693+
action,
694+
name: first.name,
695+
error: body.error ?? `HTTP ${res.status}`,
696+
};
697+
}
698+
return {
699+
confirmed: true,
700+
ok: true,
701+
action,
702+
name: first.name,
703+
runId: body.runId,
704+
message: body.runId
705+
? `Triggered ${first.name} — run ${body.runId}`
706+
: `Triggered ${first.name}`,
707+
};
708+
}
709+
710+
const path =
711+
action === "pause"
712+
? `/api/workflows/${first.name}/pause`
713+
: action === "resume"
714+
? `/api/workflows/${first.name}/resume`
715+
: action === "delete"
716+
? `/api/workflows/${first.name}`
717+
: null;
718+
if (!path) {
719+
return {
720+
confirmed: true,
721+
ok: false,
722+
action,
723+
error: `Unknown workflow action: ${action}`,
724+
};
725+
}
726+
const res = await fetch(path, {
727+
method: action === "delete" ? "DELETE" : "POST",
728+
credentials: "same-origin",
729+
headers: { "Content-Type": "application/json" },
730+
});
731+
if (!res.ok) {
732+
const body = (await res.json().catch(() => ({}))) as { error?: string };
733+
return {
734+
confirmed: true,
735+
ok: false,
736+
action,
737+
name: first.name,
738+
error: body.error ?? `HTTP ${res.status}`,
739+
};
740+
}
741+
return {
742+
confirmed: true,
743+
ok: true,
744+
action,
745+
name: first.name,
746+
message: `${first.name} ${action}d`,
747+
};
748+
} catch (err) {
749+
return {
750+
confirmed: true,
751+
ok: false,
752+
action,
753+
name: first.name,
754+
error: err instanceof Error ? err.message : String(err),
755+
};
756+
}
757+
}
758+
605759
type BundleWorkflowResult = {
606760
ok: boolean;
607761
name?: string;

0 commit comments

Comments
 (0)