Sub-agent wait and review loop
Sources: src/workflows/shared/actions/sub-agent-dispatch.ts and src/workflows/shared/actions/sub-agent-review-loop.ts. Sub-agent dispatch is fire-and-forget by default; these helpers add an opt-in wait: the child run carries the parent's Workflow SDK webhook from creation, and Convex delivers the terminal payload (status + runs.output) with bounded retries when the child closes.
Helpers
- dispatchSubAgentAndWait — dispatches a registered Sub-agent workflow with a completion webhook and durably suspends until the child reports. A child that finishes
failedorcancelledis returned in the payload, not thrown. Idempotency-reused runs are re-bound to the new webhook, with immediate delivery when already terminal. - openAndWaitForFollowUpTurn (in
src/workflows/shared/actions/coding-agent.ts) — queues a custom-prompt follow-up turn in an existing local coding session and waits for its webhook. Declines (busy, done, or cloud session) are returned structurally as{ queued: false, reason }. - runSubAgentReviewLoop — composes both: dispatch a review Sub-agent, evaluate its findings, run a fix follow-up in the parent session, and re-dispatch until the review is clean or the iteration cap is reached.
Waiting for one Sub-agent
typescript
import { dispatchSubAgentAndWait } from "@/workflows/shared/actions/sub-agent-dispatch";
const result = await dispatchSubAgentAndWait({
record,
dispatch: {
// Illustrative slug: dispatch resolves workflows through
// src/lib/sub-agent-workflow-registry.ts and throws for unregistered
// slugs. Register your child workflow there first; today only
// "ticket-voice-summary" and "pull-request-ready" exist.
workflowSlug: "visual-review",
taskKey: "visual-review",
userMessage: "Review the implemented screens for layout problems.",
subject: { kind: "run", id: record.runSlug },
context: { version: 1, origin: { type: "workflow", phase: "review" } },
idempotencyKey: `${record.runSlug}:visual-review:iter-1`,
},
});
// result.status: "completed" | "failed" | "cancelled"
// result.output: the child run's terminal output, verbatimReview loop
typescript
import { runSubAgentReviewLoop } from "@/workflows/shared/actions/sub-agent-review-loop";
const review = await runSubAgentReviewLoop({
record,
sessionId, // the parent coding session that receives fix turns
dispatch: {
workflowSlug: "visual-review",
taskKey: "visual-review",
userMessage: "Review the implemented screens.",
subject: { kind: "run", id: record.runSlug },
context: { version: 1, origin: { type: "workflow", phase: "review" } },
},
evaluate: (payload) => {
if (payload.status !== "completed") {
return { verdict: "abort", reason: "Review run did not complete." };
}
const findings = readFindings(payload.output); // caller-defined parser
return findings.length === 0
? { verdict: "clean" }
: { verdict: "issues", followUpPrompt: renderFixPrompt(findings) };
},
maxIterations: 5, // default 5, clamped to 1–10
});
// review.outcome: "clean" | "max_iterations" | "aborted"Guardrails and failure behavior
maxIterationsdefaults to 5 and is clamped to 1–10. Convex additionally refuses attempt numbers beyond 25 per (parent, task, subject) family as defense in depth.- Every waited dispatch is replay-safe: the loop supplies a per-iteration idempotency key, and
dispatchSubAgentAndWaitderives one from the parent run and wait step key when the caller omits it — so workflow step replays reuse the same child run instead of spawning duplicates. evaluatemust be pure and deterministic — it runs in workflow context on every replay.- Webhook delivery is at-least-once and durable: a fast retry ladder (5s, 30s, 120s) followed by slow redelivery every 10 minutes for roughly 5.5 hours, so a webhook-endpoint outage cannot permanently strand a waiting parent. A webhook that is gone (parent already resumed or ended) is absorbed without affecting the closed child run. Cancelling either side is safe: a cancelled child delivers its cancelled status to the waiting parent, and cancelling the parent lets an in-flight child finish.
- Fix follow-up turns require a local parent coding session; cloud sessions are declined with
cloud_sessionand the loop ends asaborted.