Skip to content

[feat] Add manual run controls and human gates #1081

Open

Description

@hsuchifeng

Parent: #818

Goal

Add the human-facing controls omitted from the minimal runtime and support workflow-declared approvals without owning questions inside agent sessions.

Scope

  • Add teamai workflow retry, cancel, and respond.
  • Retry a leased or stopped step by freeing any lease and starting a new attempt with a new revision.
  • Cancel pending or leased work while rejecting later stale reports.
  • Add approve, choose, and interactive.
  • Return gates from next without leasing their guarded step.
  • Store choice, note, answer time, channel, and whether an agent was in the path.
  • Add member auto_approve, defaulting to false.
  • Define optional, required, defaulted, and unattended behavior.
  • Permit required approval only through channels with no agent in the path.
  • Require human capability for interactive steps.
  • Initially support direct CLI/TTY responses.

Out of scope

Git-provider approval, dashboard UI, usage limits, automatic lease release on timeout, mid-step agent questions, event waits, and triggers.

Dependencies

Acceptance criteria

  • Retrying a leased or stopped step frees any lease and creates a fresh attempt.
  • Cancel stops pending or leased work and invalidates open leases.
  • Refusing approval skips the guarded step.
  • Choices expose choice and note.
  • Required gates cannot be auto-approved or agent-mediated.
  • Repeated and stale responses are rejected.
  • Mid-step questions remain a driver concern.

Verification

Focused retry, cancel, lease, gate, unattended, and channel-policy tests; build/typecheck/lint; one real-CLI E2E that pauses and responds or cancels from another process. Regenerate commands.md and update both workflow guides.

Activity

  1. SaulMoro commented on Oct 13, 2026

    @SaulMoro
    Collaborator

    In #818, retry also frees a stuck lease by hand ("stuck → teamai workflow retry … → frees the lease by hand"). Here it only retries a stopped step. Without a timeout, a lease held by a driver that died could then only be cancelled. Maybe retry could cover a leased step too?

  2. hsuchifeng commented on Oct 14, 2026

    @hsuchifeng
    CollaboratorAuthor

    retry covers a leased step as well as a stopped step. It frees the lease and starts a new attempt. That is the manual path when a driver has died and no timeout exists yet.

    cancel still stops the work. Automatic release on timeout stays in #1082.

  3. SaulMoro commented on Oct 14, 2026

    @SaulMoro
    Collaborator

    Notes from #1078 (#1129) that affect this issue:

    • Expression roots are a closed list: inputs.* and steps.<id>.outcome | end | outputs.*. choose needs steps.<id>.choice and .note added to the grammar, to the pull check that only ancestors are read, and to the readiness check (canRun).
    • Retry. stopped → pending is not enough on its own. The steps that were skipped because that step stopped (and any join skipped because all its dependencies were) must go back to pending, so the graph settles again.
    • "Stopped" means failed today. Expressions compare outcome with any string, so steps.x.outcome == 'cancelled' already parses, but nothing produces that value yet. cancel should make them stopped states in the engine's stopped() so the recovery rule and the run outcome treat them like failed.
    • Gates settle inside next. Like skips and end, they do not bump revision, so an open lease's report still matches.
    • Ends are reached eagerly. A ready end-only step settles before any lease. An approve or choose on a path to an end should keep that ordering, or change it on purpose.

    The engine rules that span files are in docs/dev/workflow-engine.md, added by #1129.

  4. SaulMoro commented on Oct 15, 2026

    @SaulMoro
    Collaborator

    Notes from checking this against #818 and the parallel leases in #1079:

    • No command continues a run on the same machine. In Discussion: Workflow Runtime Integration #818, a member continues tomorrow with teamai workflow resume r1, which runs the driver again and gets its open lease back. resume is in [feat] Publish and resume complete workflow state across machines #1084, which covers handoff between machines and waits for [feat] Add workflow workspaces, branches, and session continuation #1080. Today start only creates a new run. After the local driver stops, the only way back is next and report by hand. This issue could own resume from local state, and [feat] Publish and resume complete workflow state across machines #1084 could extend it to snapshots. Driver identity also uses the working directory as given, so a resume from a subdirectory does not match. The repository's top-level directory would.
    • Retry after the run ended. The usual case is a run that already failed. next answers done for any run that is not running (engine.ts:542), so retry has to reopen the run, not only the step. [feat] Publish privacy-preserving workflow run records and stats #1083 already requires that a retry does not duplicate records.
    • When respond counts as a channel with no agent. The skill driver ([feat] Add the session skill driver and invocable workflows #1086) answers gates by running teamai workflow respond from the agent's shell, the same command a person types. A conservative rule: respond counts as a TTY answer only when stdin is a TTY, and any other call answers optional gates only. That does not prove a person answered, because an agent can get a PTY, and Discussion: Workflow Runtime Integration #818 already accepts that limit. It does stop an ordinary agent shell call from answering a required gate.
    • Gates and parallel leases. With [feat] Add bounded loops and parallel for_each #1079, a run can have a gate open while other steps are ready. If next returns the gate first, the driver waits for a person while other items could keep building. next could return a lease when one is ready, and the gate when nothing else can run.
    • When a running step learns it was cancelled. The engine never calls a driver. A driver that waits for its tool before it talks to the engine again finds out at report, while the agent keeps spending tokens. The local driver works that way today. A driver can instead poll its lease while the tool runs and stop the tool. The help could say so, and the local driver could do it.
    • Who takes an interactive step. Only a driver that declares human takes it, and no built-in driver declares it yet. In implement-task, choosing pair would leave the run waiting with no error. Either the local driver runs the tool interactively in the step's workspace when it has a TTY, or next answers that no driver here can take the step.
  5. SaulMoro commented on Oct 15, 2026

    @SaulMoro
    Collaborator

    #1079 takes three things from this issue, so they don't get built twice. Details are in the plan on #1079.

    • The cancelled status. [feat] Add bounded loops and parallel for_each #1079 adds it for leases cut off by an end or break, with lease_cancelled on a late report. Manual cancel here can reuse the status and the report answer.
    • Reading the previous pass. In a back_to: plan loop, plan can read steps.decide, and it gets the last pass, null on the first. Passes are archived, so re-pending decide keeps its note. This issue only needs to add choice and note to what a pass keeps.
    • Telling a running driver it was cancelled. next takes the attempts a driver is running and answers with the ones that are no longer leased. canTakeLease: false asks for that without taking new work, so a driver can poll while its tool runs and stop the tool. The local driver stops cancelled commands this way.
  6. SaulMoro commented on Oct 16, 2026

    @SaulMoro
    Collaborator

    Three things from #1148 that retry and cancel have to decide:

    • Retry and pass numbers. A loop keeps a finished pass in the step's passes (repend) or moves old children to run.archive (archiveUnder), and the next pass gets a new number. If retry does the same, the failed attempt keeps its pass and its tokens in the run record, and { run, step, pass } stays unique for [feat] Add workflow scorers and recorded quality labels #1085. The cost is that steps.<id>.passes lists the failed attempt, so length(steps.review.passes) < 3 counts it. Resetting the step in place drops the attempt from the record instead.
    • Retry of an exhausted loop fails again. Step transitions keep iteration, and a pending loop owner with iteration >= max_iterations fails loop_exhausted before it runs (exhaustedAtStart). A workflow loop keeps its count in run.round. Retry has to reset these or say that it does not.
    • Cancel of pending work. cancel() in step-state.ts takes only a leased or running step, and an end skips pending steps. As skipped, a step's dependents still run and the run can succeed. As cancelled, it counts as stopped, so its dependents are skipped and the run fails, and the run record has no failure code because it takes the code from a failed step.
  7. self-assigned this
    on Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions