feat(coder): complete orchestrator advanced patterns
- Approval gate steps pause and await human resolution - appendStepEvent wired into markStep, failRun, dispatchAgentStep - Trigger rule unit tests (6 variants) - New parallel-research flow with one_success trigger
This commit is contained in:
@@ -346,6 +346,20 @@ export function createFlowRunner(deps: Deps): FlowRunner {
|
||||
continue; // re-evaluate — code output can unblock the next wave
|
||||
}
|
||||
|
||||
// Approval gate steps: pause and wait for human decision.
|
||||
const approvalReady = toRun.filter((s) => s.kind === 'approval');
|
||||
if (approvalReady.length > 0) {
|
||||
for (const s of approvalReady) {
|
||||
await sql`
|
||||
UPDATE flow_steps SET status = 'blocked', updated_at = clock_timestamp()
|
||||
WHERE run_id = ${runId} AND step_id = ${s.id}
|
||||
`;
|
||||
await appendStepEvent(sql, runId, s.id, 'paused', { reason: 'awaiting approval' });
|
||||
publishStep(runId, s.id, 'blocked');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Only agent steps remain ready → dispatch the whole parallel wave, then wait.
|
||||
for (const s of toRun) {
|
||||
await dispatchAgentStep(runId, run.project_id, model, s, ctx);
|
||||
@@ -393,6 +407,7 @@ export function createFlowRunner(deps: Deps): FlowRunner {
|
||||
SET task_id = ${task!.id}, status = 'running', input = ${fullPrompt}, updated_at = clock_timestamp()
|
||||
WHERE run_id = ${runId} AND step_id = ${step.id}
|
||||
`;
|
||||
await appendStepEvent(sql, runId, step.id, 'started', { taskId: task!.id });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,6 +454,7 @@ export function createFlowRunner(deps: Deps): FlowRunner {
|
||||
WHERE run_id = ${runId} AND step_id = ${stepId}
|
||||
`;
|
||||
}
|
||||
await appendStepEvent(sql, runId, stepId, status, output ? { outputLength: output.length } : undefined);
|
||||
}
|
||||
|
||||
// ─── run completion ─────────────────────────────────────────────────────────
|
||||
@@ -484,6 +500,7 @@ export function createFlowRunner(deps: Deps): FlowRunner {
|
||||
if (updated.count === 0) return;
|
||||
const stepId = failedStepId ?? (flow ? lastAgentStepId(flow, input, model) : 'run');
|
||||
log.warn({ runId, error }, 'flow-runner: run failed');
|
||||
await appendStepEvent(sql, runId, stepId, 'failed', { error });
|
||||
publishStep(runId, stepId, 'failed', { run_status: 'failed' });
|
||||
}
|
||||
|
||||
@@ -523,7 +540,7 @@ export function createFlowRunner(deps: Deps): FlowRunner {
|
||||
function publishStep(
|
||||
runId: string,
|
||||
stepId: string,
|
||||
status: 'running' | 'completed' | 'failed' | 'skipped' | 'cancelled',
|
||||
status: 'running' | 'completed' | 'failed' | 'skipped' | 'cancelled' | 'blocked',
|
||||
extra?: { run_status?: 'running' | 'completed' | 'failed' | 'cancelled'; report?: string },
|
||||
): void {
|
||||
publishUser({
|
||||
@@ -765,6 +782,21 @@ function errMsg(e: unknown): string {
|
||||
return e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
|
||||
// ─── Event log ───────────────────────────────────────────────────────────────
|
||||
|
||||
async function appendStepEvent(
|
||||
sql: Sql,
|
||||
runId: string,
|
||||
stepId: string,
|
||||
event: string,
|
||||
payload?: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
await sql`
|
||||
INSERT INTO flow_step_events (run_id, step_id, event, payload)
|
||||
VALUES (${runId}, ${stepId}, ${event}, ${payload ? sql.json(payload as never) : null})
|
||||
`;
|
||||
}
|
||||
|
||||
// ─── Variable substitution ───────────────────────────────────────────────────
|
||||
|
||||
const VAR_PATTERN = /\$(\w+)\.output(?:\.(\w+(?:\.\w+)*))?/g;
|
||||
|
||||
Reference in New Issue
Block a user