Plan — Workflows (#100)¶
Implements
docs/workflows.md(the manual = the spec + acceptance criterion). Read the manual first. This plan only sequences the build; if it disagrees with the manual, the manual wins. Follow/tddper phase; FE code also follows/tdd(vitest). Gate at the end with the full suite + 100% coverage (no pipe-mask),ruff,ty, FE typecheck + build, and a live canned check of a real run.
Phasing¶
One v1 scope. Both use cases end in an irreversible commit (publish report; write
into collections) that must be confirmed first, so human_gate is in v1 (manual
§10, §21). The build is sequenced (foundations → engine → endpoints → human_gate →
FE → gate), but nothing is "done" until the produce → review → commit path works
end-to-end. Only steer-and-resume + the other non-goals are deferred.
Key existing seams to hook (don't rebuild)¶
api/turns.pyChatTurnEngine—enqueue(key, content, ctx, on_complete) -> Future. An agent node =enqueuea turn,awaitthe future, capture the producedTurnMessages inon_complete, then write the artifact. FIFO-per-key gives serial-per-item; parallel for-each uses distinct keys (§11).apps/profiles —apps/profiles.pyloader +AppCatalog3-layer resolve (#89). Workflow lives in a profile;run.py+MANIFESTin_profile.json. Discover by scanning profiles (mirrorregistry._app_models()scan).apps/registry.pyscan pattern — drop-in discovery for workflow profiles.kb/ingest.pyIngestor.store+index,kb/doc_id.encode_doc_id— reuse verbatim foringest_to_collection(idempotent upsert).api/events.py+web/src/events.ts— add the step/phase events; keep in sync.InvestigationRegistry(sandbox lifecycle) — per-item sandbox; extend for per-element ephemeral sandboxes +close_sessionteardown.rm.using(user=…)— acting-user for background steps (job-pod pattern).factories.create_app— wire the workflow registry + Run routes + validation.
Phases¶
P1 — WorkflowRun resource¶
- specstar
WorkflowRunStruct (manual §13):statusenum,current_phase, per-phase progress,failures,item_id,captured_user,started/ended,result.INDEXED_FIELDS=["item_id", "status"]. Register inmake_spec. - TDD: create/read; status transitions; list-by-item (one item → N runs); aggregate
scoped by item (not global group-by, per
reference_specstar_indexed_queries).
P2 — step library + filesystem-journal engine (the core)¶
wfrun handle:read/read_json/glob/files(over the item's FileStore),config(profile config), the captured user, the run-scoped credential handle.agent_step— builds theAgentToolContext(profile tools⊆, item key),enqueues a turn, awaits, runs thecheck; on fail feeds the reason back and retries up toretries; on success writesstep_<name>/<key>+ input-hash =hash(args). Mandatorycheck=(a call without it is a type/validation error).sandbox_node— dispatches a script/command into the sandbox, captures result, runscheck, writes artifact/receipt + input-hash.- on-demand skip — before executing any step: if
step_<name>/<key>exists AND its recorded input-hash matches → return cached artifact (no run, no LLM, no chat post).cache=False⇒ always run. check.*builders (file_nonempty,choice_in,collection_has,exec),fail/StepFailed.- TDD anchors: skip-on-rerun (artifact present + hash match); re-run on hash change
(edited upstream → downstream re-runs); deleted artifact → re-run;
cache=Falsealways runs; mandatory-gate enforcement; retry-with-feedback then abort; agent node NOT re-calling the LLM on skip (assert withScriptedAgentRunner).
P3 — profile workflow discovery + MANIFEST¶
- Extend the profile loader: a profile with
run.py+workflowin_profile.jsonis a workflow profile. Parsephases+input_jsonpath. Surface inGET /a/{slug}/profiles(flag has-workflow + return MANIFEST). - Coherence check at startup (extend
validate_all_apps):run.pyimportable, phases well-formed, agent_step tools ⊆ profile ceiling. - TDD: discovery (drop-in profile registered), MANIFEST surfaced, coherence errors
fail loud at startup;
_-prefixed skipped (mirror app scan).
P4 — Run endpoint + orchestration driver¶
POST /a/{slug}/items/{item_id}/run— validate item under slug (sharedDepends, #95), load the profile'srun, create aWorkflowRun(captureget_user), start the orchestration as a background task, return{ run_id, item_id }. Enforce one active run per item.- The driver: reads
input_json→ passes parsedinputstorun(wf, inputs); updatesWorkflowRun.status/current_phaseas phases enter; persistsresult/erroron terminal. GET .../runs/{run_id}(poll),GET .../runs/{run_id}/stream(reusesubscribe_sse).- TDD (ScriptedAgentRunner): happy-path run →
done+ result; failing step →error - phase + reason; re-run skips completed; second run on same item; reject double active run.
P5 — ingest_to_collection capability + run-scoped credential¶
- HTTP capability endpoint: read workspace file →
rm.using(captured)→Ingestor.store+index→ awaitready; idempotent viaencode_doc_id; require collection exists (404 else). Writesstep_ingest/<file>.donereceipt. - Run-scoped credential: minted at run start, injected into the sandbox env, maps to captured user, scoped + expiring. Sandbox node auths capability calls with it.
- TDD: ingest lands a
readydoc; re-ingest upserts (no dup); unknown collection → fail;collection_hasgate; credential scope + expiry (reject after terminal). (The full live check lands in P13.)
P6 — parallel for-each (wf.map)¶
wf.map(fn, items, *, concurrency=cap)— per-element own turn-key + ephemeral sandbox; bounded by the global cap; skip+collect per element; aggregate. Parent serial sections still use the item's main key/sandbox.- TDD: N elements run concurrently (distinct keys, not serialized); one failing element collected, others complete; per-element artifacts don't collide; cap bounds concurrency.
P7 — observability events¶
- Add
PhaseEntered/StepStarted/StepPassed/StepFailed/StepSkipped/StepRetryingtoapi/events.py; mirror inweb/src/events.ts. Engine emits them - updates
WorkflowRunper-phase progress. - TDD: events emitted in order; skip emits
StepSkipped; failure carries phase + reason;WorkflowRunprogress matches the stream.
P8 — robustness¶
- Per-step timeout + per-run wall-clock cap; per-run max-steps; optional token
budget → abort to
error. Failure notify: pull (status) + in-app notify owner. - TDD: step timeout aborts; max-steps trips; notify fires on error.
P9 — lifecycle & resources¶
- Sandbox released on terminal (
close_session+turn_engine.forget); per-element sandboxes torn down; TTL / keep-last-K retention sweep for API-created items; global concurrency cap (runs queue when full). - TDD: terminal releases sandbox; cap queues excess; TTL prunes; terminal does NOT auto-close the item.
P10 — Stop & take over¶
POST .../runs/{run_id}/cancel(or reuse Stop) →cancel_current→ run terminal (cancelled); item opens to interactive; parallel in-flight elements cancelled, completed kept. Free chat opens post-terminal.- TDD: stop mid-run → cancelled + partial kept; chat usable after; re-run resumes from artifacts.
P11 — human_gate + decisions¶
human_gate(...): suspend the run (statusawaiting_human), writepending_decisionon theWorkflowRun, release the sandbox; the run task exits. Decision recorded as artifactstep_<gate>/decision.json.POST .../runs/{run_id}/decisions→ write the decision artifact → resume (re-run; completed steps skip; the gate reads the decision artifact and continues).- Outcomes the body sees:
approve/reject(→ terminal + interactive) /revise(+input). (Retry/rewind stays the §9 file mechanism, not a gate outcome.) - In scope because both use cases gate before their irreversible commit (manual §10, §21): produce → review → commit.
- TDD: gate suspends →
awaiting_human; decision resumes; approve continues; reject → terminal + interactive takeover; sandbox released on pause + recreated on resume.
P12 — Frontend — /tdd + vitest¶
- Discover/Run: on a workflow-profile item show Run workflow; prepare inputs
via the existing file UI (drop into
inputs/, editinput.json); Run →POST …/run; hooks via TanStack Query (keys inqueryKeys.ts). - Run view: phase diagram (skeleton from MANIFEST.phases + live events;
current/passed/failed/skipped states),
WorkflowRunstatus, Stop button, per-element batch sub-logs. ReuseAgentEntryViewfor agent-node reasoning/tool cards (the chat stream is the same SSE). - Decision card on
awaiting_human: renderpending_decision, post approve / reject / revise(+input) to the decisions endpoint, resume the view. - Run list: per-item run history (status, started/ended, result/failures), link to a past run.
- TDD: Run button gated on has-workflow; diagram renders skeleton + applies events;
stop wired; batch sub-logs; decision card posts + resumes; provider-wrapped
(
renderWithQuery).
P13 — v1 gate¶
- Full backend suite + 100% coverage (no pipe-mask, read
N failed);ruff;ty(changed files clean); FE typecheck +pnpm build+ vitest. Commit local only. - Live canned check (DoD, per
feedback_llm_features_need_live_checks) against local Ollama: the full produce → review → commit path — trigger → classify →awaiting_human→ approve → ingest → polldone— plus arejectleaving nothing committed.
Deferred (not in v1)¶
- Steer-and-resume — queue a human note mid-run, injected into the next node's context at the boundary. (Not needed by the two use cases.)
- Plus the manual §21 non-goals: declarative-DAG / visual authoring; control-flow branching primitives; outbound webhook callbacks; module-level version pinning; real SSO authz.
Risks / watch-list (from the grill)¶
- Determinism of step identity (manual §3/§9): the sharpest author footgun. Lint
/ document: control flow reads only
inputs+ artifacts; inputs passed as args. - Parallel for-each resource use — per-element sandboxes multiply; the cap is the only thing between a big batch and resource exhaustion. Make it real, not nominal.
- Phase skeleton vs dynamic execution (§12) — keep phases coarse; mark skipped.
- input.json has no platform validation (§14) — bad input fails inside
run(); acceptable, but the worked workflows should gate their own inputs early.