Agent / parallel / pipeline API visual plan

Status: implemented on branch api-agent-parallel-pipeline Date: 2026-06-12 Companion grill doc: Agent / parallel / pipeline API grill Related issue: #69

The picture in one sentence

Make workflow authoring look like a durable Python harness that coordinates agents, parallel fan-out, pipelines, and human approvals; keep waits, signals, handoffs, leases, replay, and outbox machinery below the floorboards.

Implementation status

Implemented on branch api-agent-parallel-pipeline in one coherent PR-sized change, not split across timid fragments.

Implemented surface:

research = await agent("research", prompt="Research typed workflows", input=brief, returns=ResearchPacket)
sections = await parallel([agent("draft_section", prompt=f"Draft {s}", input=s, key_by=s.slug, returns=SectionDraft) for s in sections])
final_sections = await pipeline(sections, humanize_section, evidence_check_section, limit=4)
await ask("Review final draft", key="review_final", input=draft, returns=ReviewDecision)

Durability rule: saved outputs replay only when the stored request fingerprint still matches the current rendered prompt, input, context hashes, return schema, and runner options.

Verification on this branch: 271 passed, 2 skipped.

flowchart TB
    subgraph Author["Author-facing workflow language"]
        W["@workflow async def blog_post(...)"]
        A["agent('research')"]
        P["parallel([...], limit=4)"]
        L["pipeline(items, stages...)"]
        H["ask"]
        S["step(local_python)"]
        W --> A
        W --> P
        W --> L
        W --> H
        W --> S
    end

    subgraph PublicGraph["Public run graph"]
        G1["agent step"]
        G2["fan-out block"]
        G3["pipeline stage"]
        G4["approval gate"]
        G5["local step"]
    end

    subgraph Runtime["Runtime substrate — not normal author language"]
        R1["event history"]
        R2["memoized replay"]
        R3["outbox commands"]
        R4["worker leases"]
        R5["signals / waits"]
        R6["approval provenance"]
    end

    A --> G1
    P --> G2
    L --> G3
    H --> G4
    S --> G5

    G1 --> R1
    G2 --> R1
    G3 --> R1
    G4 --> R6
    G5 --> R1
    R1 --> R2
    R3 --> R4
    R5 --> R2

    classDef author fill:#e0f2fe,stroke:#0284c7,color:#0f172a
    classDef graph fill:#dcfce7,stroke:#16a34a,color:#0f172a
    classDef runtime fill:#fee2e2,stroke:#dc2626,color:#0f172a
    class W,A,P,L,H,S author
    class G1,G2,G3,G4,G5 graph
    class R1,R2,R3,R4,R5,R6 runtime

Visual vocabulary

Use these words in docs, code comments, dashboard labels, and issue titles.

mindmap
  root((Hermes Workflow))
    prompt builders
      typed inputs
      rendered prompts
      returns AgentCall
    agent
      required prompt
      subagent/session runner
      typed output
      durable replay
      provenance
    parallel
      fan-out
      fan-in
      concurrency limit
      failure policy
    pipeline
      staged work
      item identity
      per-stage progress
      replay-safe resume
    approval
      human gate
      feedback loop
      provenance
      side-effect boundary
    step
      deterministic local Python
      memoized
      typed return
    advanced internals
      ctx
      signal
      wait
      outbox
      lease

Blog workflow topology we want authors to see

flowchart LR
    Start([topic]) --> Research["agent: research"]
    Research --> Angles["agent: angle_options"]
    Angles --> Choose{"approve: choose_angle"}
    Choose --> Outline["agent: outline"]
    Outline --> OutlineApproval{"ask: approve_outline"}

    OutlineApproval --> F["parallel: draft sections"]

    subgraph Fanout["fan-out block"]
        S1["agent: draft_section_intro"]
        S2["agent: draft_section_core"]
        S3["agent: draft_section_tradeoffs"]
        S4["agent: draft_section_close"]
    end

    F --> S1
    F --> S2
    F --> S3
    F --> S4

    S1 --> Pipe
    S2 --> Pipe
    S3 --> Pipe
    S4 --> Pipe

    subgraph Pipe["pipeline: section polish"]
        H1["agent: humanize_section"] --> E1["agent: evidence_check_section"] --> A1{"ask: approve_section"}
    end

    Pipe --> Assemble["agent: assemble_final_draft"]
    Assemble --> Done([markdown draft])

The visible shape is research → approval → parallel drafting → staged polish → approval → final draft.

The invisible shape is command rows, signals, and replay checkpoints. Those are diagnostics, not the authoring API.

Primitive map

Primitive Author sees Runtime records Dashboard should show
agent(...) “Run this agent step with this prompt/input/schema.” Rendered prompt, input snapshot, context digest, output, provenance, artifacts, runner metadata. One agent step card with prompt/context receipt, logs/artifacts/output.
parallel(...) “Run these independent calls together.” A fan-out/fan-in group plus child step events. Block with child statuses and concurrency.
pipeline(...) “Apply stages to items.” Stage/item progress, results, failures, resumable keys. Matrix or swimlane: items × stages.
approve(...) “Human picks/accepts/decides.” Approval request, decision, provenance, idempotency. Approval card with consequence and source.
ask(...) “Loop until accepted.” Attempts, feedback, revision linkage. Approval gate with attempts/feedback history.
step(...) “Run local deterministic Python.” Step request/result/error. Plain local step card.

Layering plan

flowchart TB
    subgraph Surface["Layer 1: author surface"]
        F1["agent"]
        F2["parallel"]
        F3["pipeline"]
        F4["ask"]
        F5["step"]
    end

    subgraph Calls["Layer 2: call objects"]
        C1["AgentCall[T]"]
        C2["StepCall[T]"]
        C3["ApprovalCall[T]"]
        C4["PipelineStage[T,U]"]
    end

    subgraph Engine["Layer 3: runtime engine"]
        E1["WorkflowContext"]
        E2["agent(...) substrate"]
        E3["gather / fan-out"]
        E4["approval client"]
        E5["event replay"]
    end

    subgraph Storage["Layer 4: persisted state"]
        DB[("SQLite history")]
        OC["outbox commands"]
        AR["artifacts / receipts"]
    end

    F1 --> C1 --> E2
    F2 --> C1
    F2 --> C2
    F2 --> E3
    F3 --> C4 --> E3
    F4 --> C3 --> E4
    F5 --> C2 --> E1
    E2 --> DB
    E3 --> DB
    E4 --> DB
    E5 --> DB
    E1 --> OC
    E2 --> AR

Key design decision: top-level functions create awaitable call objects. Awaiting a call executes through the ambient workflow runtime. parallel(...) and pipeline(...) can accept not-yet-awaited calls and launch them with durable fan-out semantics. agent(...) requires a prompt; higher-order prompt-builder helpers are just normal Python functions that format prompts from structured inputs and return AgentCall[T] objects.

Context + memoization shape

flowchart LR
    I["typed input"] --> B["prompt builder"]
    T["template/version"] --> B
    B --> P["rendered prompt"]
    I --> F["fingerprint"]
    P --> F
    R["returns schema"] --> F
    F --> M{"completed output for step key?"}
    M -->|"match"| O["return saved typed output"]
    M -->|"missing"| Q["enqueue agent work"]
    M -->|"mismatch"| X["fail / require explicit invalidation"]

Memoization rule: saved outputs are reused only when the step key and dependency fingerprint match. The fingerprint includes rendered prompt, structured input, return schema, and runner-relevant options. Changed input must not silently reuse stale output.

Lifecycle of one agent(...) call

sequenceDiagram
    participant W as Workflow author code
    participant API as agent(...) helper
    participant E as WorkflowEngine
    participant DB as SQLite history
    participant R as Agent runner

    W->>API: await agent("research", returns=ResearchPacket)
    API->>E: request durable agent step
    E->>DB: check completed step by stable key
    alt completed in history
        DB-->>E: stored typed payload
        E-->>API: rehydrated ResearchPacket
        API-->>W: ResearchPacket
    else not completed
        E->>DB: StepRequested + outbox command
        E-->>API: suspend workflow as waiting
        R->>DB: claim command lease
        R->>R: run provider/subagent/session
        R->>DB: StepCompleted(output + provenance)
        W->>E: trusted resume/re-run workflow
        E->>DB: replay completed step
        E-->>API: rehydrated ResearchPacket
        API-->>W: ResearchPacket
    end

Lifecycle of a parallel(...) fan-out

sequenceDiagram
    participant W as Workflow code
    participant P as parallel(...)
    participant E as WorkflowEngine
    participant DB as SQLite history
    participant R as Workers/runners

    W->>P: await parallel([agent A, agent B, agent C], limit=2)
    P->>E: register fan-out group
    E->>DB: record missing child A command
    E->>DB: record missing child B command
    E->>DB: record missing child C command
    E-->>W: waiting on fan-out group
    R->>DB: claim A/B up to limit
    R->>DB: StepCompleted A/B
    R->>DB: claim C
    R->>DB: StepCompleted C
    W->>E: resume/replay
    E->>DB: read A/B/C results
    E-->>P: ordered results
    P-->>W: [A, B, C]

Lifecycle of a pipeline(...)

flowchart TB
    subgraph Inputs["items"]
        I1["intro"]
        I2["core"]
        I3["tradeoffs"]
    end

    subgraph Stage1["stage 1: humanize"]
        H1["humanize/intro"]
        H2["humanize/core"]
        H3["humanize/tradeoffs"]
    end

    subgraph Stage2["stage 2: evidence check"]
        E1["evidence/intro"]
        E2["evidence/core"]
        E3["evidence/tradeoffs"]
    end

    subgraph Stage3["stage 3: approve until"]
        A1{"approve/intro"}
        A2{"approve/core"}
        A3{"approve/tradeoffs"}
    end

    I1 --> H1 --> E1 --> A1
    I2 --> H2 --> E2 --> A2
    I3 --> H3 --> E3 --> A3

    A1 --> O["approved sections"]
    A2 --> O
    A3 --> O

The dashboard can render this as a matrix: rows are items, columns are stages. That is the visual payoff of making pipeline(...) first-class instead of hiding it as nested loops.

Implementation roadmap

gantt
    title One honest API rehaul PR
    dateFormat  YYYY-MM-DD
    axisFormat  %m/%d

    section Language lock
    Repo grill doc                         :done, a1, 2026-06-12, 1d
    Visual artifact                        :active, a2, 2026-06-12, 1d
    Update issue #69                       :a3, after a2, 1d

    section One PR: author surface
    RED tests for top-level helpers         :b1, after a3, 1d
    AgentCall + required prompt             :b2, after b1, 1d
    prompt-builder examples                 :b3, after b2, 1d
    ask                 :b4, after b3, 1d
    parallel fan-out/fan-in                 :b5, after b4, 2d
    first-pass pipeline                     :b6, after b5, 2d

    section One PR: replay truth
    typed return rehydration                :c1, after b6, 2d
    fingerprint policy                      :c2, after c1, 2d
    mismatch diagnostics / invalidation     :c3, after c2, 1d
    docs + blog workflow smoke              :c4, after c3, 1d

This is one implementation PR, not four product-language fragments. If it starts getting scary, shrink internals — not the shared author vocabulary.

PR shape board

flowchart LR
    P0["Docs + artifact: shared language"] --> P1["One API rehaul PR"]

    P1 --> A["agent(prompt=..., input=..., returns=...)"]
    P1 --> B["prompt builders return AgentCall[T]"]
    P1 --> C["parallel([...])"]
    P1 --> D["pipeline(items, stages...)"]
    P1 --> E["ask"]
    P1 --> F["typed replay"]
    P1 --> G["request fingerprint guard"]

    A --> T1["No visible ctx in happy-path examples"]
    C --> T2["Fan-out starts missing work before waiting"]
    D --> T3["Items × stages visible and resumable"]
    F --> T4["returns=Dataclass rehydrates as Dataclass"]
    G --> T5["Changed prompt/context cannot silently reuse output"]

File touch map

flowchart TB
    subgraph API["Public API"]
        Init["src/hermes_workflows/__init__.py"]
        Author["src/hermes_workflows/authoring.py"]
    end

    subgraph Runtime["Runtime internals"]
        Engine["src/hermes_workflows/engine.py"]
        Values["src/hermes_workflows/workflow_values.py"]
        Runners["src/hermes_workflows/runners.py"]
        Status["src/hermes_workflows/status.py / dashboard APIs"]
    end

    subgraph Tests["Tests"]
        APItests["tests/test_authoring_api.py"]
        ParallelTests["tests/test_parallel_authoring.py"]
        PipelineTests["tests/test_pipeline_authoring.py"]
        TypedTests["tests/test_typed_replay.py"]
    end

    subgraph Docs["Docs/examples"]
        Readme["README.md"]
        Grill["docs/architecture/agent-parallel-pipeline-api-grill.md"]
        Visual["docs/plans/2026-06-12-agent-parallel-pipeline-api-visual-plan.md"]
        Example["examples/agent_parallel_pipeline_blog.py"]
    end

    Init --> Author
    Author --> Engine
    Author --> Values
    Author --> Runners
    Author --> Status
    APItests --> Author
    ParallelTests --> Engine
    PipelineTests --> Status
    TypedTests --> Values
    Readme --> Author
    Example --> Author

Decision checkpoints

flowchart TD
    D0{"Can a normal author write the blog workflow without ctx?"}
    D0 -- no --> FixSurface["Fix authoring surface before runtime polish"]
    D0 -- yes --> D1{"Do agent calls replay as typed values?"}
    D1 -- no --> Typed["Prioritize typed replay before more demos"]
    D1 -- yes --> D2{"Does parallel show true fan-out/fan-in?"}
    D2 -- no --> Parallel["Fix fan-out semantics/topology"]
    D2 -- yes --> D3{"Does pipeline render as items × stages?"}
    D3 -- no --> Pipe["Add stage/item metadata"]
    D3 -- yes --> D4{"Can approval loops revise without terminating?"}
    D4 -- no --> Approval["Fix ask lifecycle"]
    D4 -- yes --> Ship["Promote API in README/examples"]

Design traps to avoid

flowchart LR
    Bad1["Alias low-level handoff plumbing as agent"] --> Lie["Looks nice, still wrong"]
    Bad2["parallel is serial internally"] --> Lie
    Bad3["returns=Dataclass gives dict on replay"] --> Lie
    Bad4["pipeline is just nested loops"] --> Lie
    Bad5["approval loop returns early on rejection"] --> Lie
    Lie --> Stop["Stop. Fix the substrate or document limitation honestly."]

Acceptance snapshot

A first version is good enough when this code is plausible, tested, and documented:

@workflow(name="blog-post")
async def blog_post(topic: str) -> str:
    research = await agent("research", input=topic, returns=ResearchPacket)
    outline = await agent("outline", input=research, returns=Outline)
    outline_review = await ask("Review outline", key="review_outline", input=outline, returns=ReviewDecision)

    drafts = await parallel(
        [agent("draft_section", input=s, key_by=s.slug, returns=SectionDraft) for s in outline.sections],
        limit=4,
    )

    sections = await pipeline(
        drafts,
        agent("humanize", returns=SectionDraft),
        agent("evidence_check", returns=SectionDraft),
        lambda section: ask("Review section", key=f"review_section_{section.id}", input=section, returns=ReviewDecision),
        limit=4,
    )

    return await agent("assemble", input=sections, returns=str)

If the implementation cannot make this honest, it is not an API rehaul yet. It is lipstick on a runtime API, and we should call it that before it metastasizes.