Plan: Durable execution from the CLI
On Sun, 04 Oct 2026, by @lucasdicioccio, 940 words, 9 code snippets, 0 links, 0images.
Generated from todos/durable-workflows-cli-start-plan.md, the repository is the canonical source and may be ahead of this page.
Plan: Durable execution from the CLI
Goal
Make the durable-workflow execution mode accessible from the CLI in one-shot form, so a user can:
- Start a brand-new session with a prompt using
agents session start .... - Have the agent run until it yields (partial turn, deferred tool calls) or completes.
- Inspect pending calls with
agents session pending. - Complete deferred calls with
agents session complete. - Resume with
agents session resume. - Iterate step by step, either letting the LLM continue or injecting tool results on behalf of the user.
This closes the gap between the existing run command (fully synchronous,
runs to completion) and the session pause/resume/... commands (which only
operate on already-existing sessions).
Decisions on open questions
| Question | Decision |
|---|---|
How many steps should session start run? | Zero by default, with an optional --step flag to run exactly one scheduling step. The goal is to forfeit as much scheduling as possible to external processes/users. |
| How is the initial turn created? | Option B: construct the initial UserTurn directly in session start with the prompt and media, then persist the session. |
| Isolated execution? | Out of scope for this milestone. Only RunSync, RunAsync, and Defer are supported. |
| Policy language? | Keep it simple: exact tool-name rules plus a default disposition. |
| Continuation store? | Keep it simple: rely on the file-based SessionStore and the in-memory partial turn. A SQLite continuation store can be added later. |
Current state and gaps
What already works
- The library has full durable-workflow primitives:
ToolCallPolicy,ToolCallDisposition(RunSync,RunAsync,Defer,RunIsolated).runAsyncinSystem.Agents.Session.Loopruns an async agent until it yields aPartialUserTurn.resumeSessionandwakeSessioninSystem.Agents.Session.Wake.SessionBackend,ContinuationStore, and CLI commandssession pause/resume/pending/complete/run-isolated.
- The existing
runcommand already accepts prompts, media, session files, and thinking-output options.
What is missing
-
No CLI command to create a new session in async mode.
runalways usesmainOneShotTextWithThinking, which creates a synchronous agent and callsLoop.runto completion.session pause/resume/...require an existingSessionId; they cannot start from a user prompt.
-
No way to configure a durable policy from the agent JSON file.
System.Agents.Base.Agent(the JSON config type) has no fields forexecutionMode,toolCallPolicy,toolExecutor,continuationStore,deploymentRunner, orsessionBackend.- The CLI’s
withAgentForSessionbuilds the runtime agent withdefaultToolCallPolicy, so even in async mode every call runs synchronously and no yielding occurs.
-
No shared parser for
run-like options in thesessioncommand group.parseOneShotOptionsis only used byrun.session startshould accept the same prompt/media/thinking options.
-
No single-step scheduler command wired to the CLI.
runAsyncexists in the library but is not used by any CLI handler.handlePausecallsrunStepMonce on an existing session, but it does not create the initial user turn from a prompt.
Proposed CLI additions
New command: agents session start
# Create a session with an initial UserTurn, do not call the LLM yet.
agents session start \
--agent-file my-agent.json \
--prompt "Research the weather in Paris and New York"
# Same, but also run one scheduling step (e.g. ask the LLM and possibly
# execute the first batch of tool calls).
agents session start --step \
--agent-file my-agent.json \
--prompt "Research the weather in Paris and New York"Semantics:
- Creates a fresh
SessionwithsessionVersion = Just 2andsessionExecutionMode = Just Asynchronous. - Constructs an initial
UserTurncontaining the prompt (and any media attachments). - Loads the agent, applies the durable configuration from the agent file
(see below), and sets
ctxExecutionMode = Asynchronous. - By default, persists the session immediately without calling the LLM.
- With
--step, callsrunStepMexactly once before persisting. - Prints:
- the new
session-id; - the current state (
UserTurn,LlmTurn,PartialUserTurn, or completed); - continuation tokens for any deferred calls.
- the new
If the session completes after the optional step, the final LLM response is printed as well.
New command: agents session step
agents session step <session-id>Runs exactly one scheduling step on an existing session and persists the result. This is the workhorse for manual iteration:
session start -p "..."→ session withUserTurn.session step <id>→ LLM produces tool calls (LlmTurn).session step <id>→ sync tools run, async/deferred calls yield (PartialUserTurn).session complete <token> result.json→ inject external result.session step <id>→ woken results processed, possibly another LLM turn.- Repeat until completion.
session step is functionally similar to the existing session pause, but
named for iteration rather than interruption. The existing session pause
can be kept for backward compatibility.
Reuse run options
Both session start and session step should accept the same prompt-script
options as run:
agents session start \
--agent-file my-agent.json \
--prompt "Summarize" \
--file report.md \
--media screenshot.png \
--thinking stderrThe cleanest way is to extract a shared parser type, e.g.
OneShotPromptOptions, from System.Agents.CLI.OneShot and reuse it for
run, session start, and (for prompt injection) future commands.
Agent configuration for durable mode
To make durable execution usable without writing Haskell, the agent JSON config needs to expose at least the following fields.
Minimal viable config
{
"slug": "my-durable-agent",
"executionMode": "asynchronous",
"toolCallPolicy": {
"default": "runSync",
"rules": [
{"tool": "bash_command", "disposition": {"tag": "defer", "reason": "approval required"}},
{"tool": "fetch_remote", "disposition": {"tag": "runAsync"}}
]
}
}Proposed JSON schema additions to System.Agents.Base.Agent
data Agent = Agent
{ ...
, executionMode :: Maybe ExecutionMode -- default Synchronous
, toolCallPolicyConfig :: Maybe ToolCallPolicyConfig
}Only executionMode and toolCallPolicyConfig are in scope for this
milestone.
Tool-call policy config design
A declarative policy config that the runtime turns into a ToolCallPolicy:
data ToolCallPolicyConfig = ToolCallPolicyConfig
{ tpcDefaultDisposition :: ToolCallDisposition
, tpcRules :: [ToolCallPolicyRule]
}
data ToolCallPolicyRule = ToolCallPolicyRule
{ tprToolName :: Text -- exact match
, tprDisposition :: ToolCallDisposition
}The runtime combinator:
buildToolCallPolicy :: ToolCallPolicyConfig -> ToolCallPolicy
buildToolCallPolicy cfg _ctx call =
fromMaybe cfg.tpcDefaultDisposition $
find ((== callName call) . tprToolName) cfg.tpcRulesWhere the policy is applied
In System.Agents.CLI.SessionDurable.withAgentForSession, after building the
runtime agent from the OS tree, inspect the JSON Agent config:
let agent' = maybe agent (withExecutionMode Asynchronous) (executionMode jsonAgent)
let agent'' = maybe agent' (withToolCallPolicy . buildToolCallPolicy) (toolCallPolicyConfig jsonAgent)This makes session start, session step, session pause, and
session resume all use the same durable configuration.
Implementation steps
Step 1 — Shared prompt options
Refactor System.Agents.CLI.OneShot so that the prompt/media/thinking
options are reusable:
data PromptScriptOptions = PromptScriptOptions
{ psoPromptScript :: PromptScript
, psoMediaFiles :: [MediaReference]
, psoThinkingOutput :: OneShot.ThinkingOutput
}
parsePromptScriptOptions :: Parser PromptScriptOptionsUpdate OneShotOptions to contain PromptScriptOptions plus
sessionFile. Update parseOneShotOptions accordingly.
Step 2 — Add durable config fields to JSON Agent
In System.Agents.Base.Agent:
- Add
executionMode :: Maybe ExecutionMode. - Add
toolCallPolicyConfig :: Maybe ToolCallPolicyConfigwith JSON instances. - Keep defaults backward-compatible (
Nothing= synchronous, default policy).
Step 3 — Build policy from config
In System.Agents.CLI.SessionDurable:
- Add
buildToolCallPolicy/applyAgentDurableConfig. - Update
withAgentForSessionto read the JSON agent config and apply durable settings to the runtime agent.
Step 4 — Implement session start
In System.Agents.CLI.SessionDurable:
- Add
SessionStart PromptScriptOptions Boolcommand (theBoolis whether to run one step). - Implement
handleStart:- Load agent tree and build runtime agent (with durable config).
- Interpret the prompt script (aliases, files, shell, media).
- Create a fresh
Session(version 2, async mode). - Construct the initial
UserTurnwith the prompt/media. - If
--step, callrunStepMexactly once. - Persist the resulting session.
- Print session id, current turn state, and any deferred tokens.
Step 5 — Implement session step
In System.Agents.CLI.SessionDurable:
- Add
SessionStep SessionIdcommand. - Implement
handleStep:- Load the existing session.
- Load the agent and apply durable config.
- Call
runStepMexactly once. - Persist the resulting session.
- Print the new turn state and any deferred tokens.
This is very close to the existing handlePause; consider sharing the
implementation.
Step 6 — Wire the parsers
In app/Main.hs:
- Add
SessionStartandSessionStepconstructors toSessionDurableCommand. - Add
startandstepsubcommand parsers reusingparsePromptScriptOptions/parseSessionIdArgument. - Update help text:
Operate durable sessions: start, step, pause, resume, ....
Step 7 — Tests
- Unit tests for
ToolCallPolicyConfigJSON parsing. - Unit tests for
buildToolCallPolicy. - Integration test exercising the full iterative flow:
session start -p "..."creates a session with aUserTurn;session step <id>produces anLlmTurnwith tool calls;session step <id>runs sync tools and defers one call (PartialUserTurn);session pending <id>lists the deferred token;session complete <token> result.jsoninjects the result;session step <id>processes the result and produces a final response.
Suggested first milestone
The smallest vertical slice that proves the design:
- Extract shared
PromptScriptOptionsfromrun. - Add
executionModeandtoolCallPolicyConfigto the JSONAgent. - Implement
buildToolCallPolicyand apply it inwithAgentForSession. - Add
agents session startthat creates a session with an initialUserTurnand optionally runs one step. - Add
agents session stepfor single-step iteration. - Write one integration test exercising the full flow above.
Related docs
todos/durable-workflows.md— original design plan.todos/durable-workflows.progress.md— implementation progress.documentation/durable-workflows-howto.md— user-facing how-to.src/System/Agents/CLI/SessionDurable.hs— existing session command handlers.src/System/Agents/Session/Loop.hs—runAsyncandrunStepM.