Agentic
Opt-in building blocks for LLM-agent workflows. See the Agentic Workflows guide for usage.
Streaming
Streaming surface for live workflow progress (agentic ergonomics).
LangGraph exposes stream/astream to feed live values, state updates,
and LLM tokens to a caller. Stabilize already has the plumbing — an
event-sourced log plus a thread-safe EventBus
— so this module only adds the missing consumer surface:
emit_progress()lets a task push a custom progress event (a step narration, an LLM token chunk, a percentage) from insideexecute.WorkflowStreamlets a caller consume a workflow’s events, either live (subscribe to the bus) or by replaying the durable log, or both (replay history, then follow live) — all additive and opt-in.
Nothing here changes existing behavior: workflows that never call
emit_progress and callers that never open a WorkflowStream are
unaffected.
- class stabilize.streaming.StreamItem(event_type, entity_type, entity_id, workflow_id, data, sequence, source)[source]
Bases:
objectOne item yielded by a
WorkflowStream.- Parameters:
event_type (str)
entity_type (str)
entity_id (str)
workflow_id (str)
data (dict[str, Any])
sequence (int)
source (str)
- event_type: str
- entity_type: str
- entity_id: str
- workflow_id: str
- data: dict[str, Any]
- sequence: int
- source: str
- stabilize.streaming.emit_progress(stage, message='', *, recorder=None, **data)[source]
Emit a custom progress event for a running stage.
Publishes a CUSTOM event carrying
messageand any extra keyword data to the event bus (and, if a recorder is given, the durable log). Callers watching aWorkflowStreamsee it live. Safe to call from a task’sexecute— if event sourcing is not configured, this is a cheap no-op against the always-available global bus.- Parameters:
stage (StageExecution) – The stage emitting progress (must have an execution).
message (str) – Human-readable progress line.
recorder (EventRecorder | None) – Optional recorder to also persist the event durably.
**data (Any) – Extra structured fields (e.g. token=”…”, percent=42).
- Return type:
None
- class stabilize.streaming.WorkflowStream(execution_id, event_store=None, subscription_id=None)[source]
Bases:
objectConsume a workflow’s events live and/or from the durable log.
Example (live follow until the workflow ends):
stream = WorkflowStream(execution_id, event_store=store) for item in stream.follow(): print(item.event_type, item.data.get("message", ""))
Example (callback form, non-blocking):
stream = WorkflowStream(execution_id) stream.on_event(lambda item: print(item)) ... # run the workflow stream.close()
- Parameters:
execution_id (str)
event_store (EventStore | None)
subscription_id (str | None)
- replay()[source]
Yield all durably-recorded events for this workflow, in order.
- Return type:
Iterator[StreamItem]
- on_event(callback, *, mode=SubscriptionMode.SYNC)[source]
Register a callback invoked with each live
StreamItem.Non-blocking. Call
close()to unsubscribe.- Parameters:
callback (Any)
mode (SubscriptionMode)
- Return type:
None
- follow(include_history=False, timeout=None)[source]
Yield events for this workflow as they occur, until it ends.
- Parameters:
include_history (bool) – First replay the durable log (requires an event_store), then continue live. Events already replayed are de-duplicated against the live feed by sequence.
timeout (float | None) – Stop after this many seconds of no new events (None waits indefinitely, but always stops on a workflow-terminal event).
- Return type:
Iterator[StreamItem]
Human-in-the-Loop
Human-in-the-loop (HITL) helpers (agentic ergonomics).
LangGraph’s interrupt pauses a graph and returns the human’s value inline
on resume. Stabilize already has the durable machinery — TaskResult.suspend
sets a stage SUSPENDED and SignalStage(persistent=True) buffers a signal
sent before the stage suspends, all surviving a crash via the atomic queue.
This module wraps that into a friendly, one-call API plus a builtin task:
ApprovalTask— a ready-made task that suspends until anapprove/rejectsignal arrives, then routes accordingly.approve()/reject()/send_signal()— send the resume signal (with a payload) to a suspended stage via an orchestrator/queue.get_signal()— read the delivered signal name/data inside a task.
All additive: existing suspend/signal users are unaffected.
- class stabilize.hitl.ApprovalTask[source]
Bases:
TaskA task that waits for a human approval signal, then routes on it.
Behavior: - First execution (no signal yet): returns
TaskResult.suspend(); thestage becomes SUSPENDED and waits.
Resumed by an
approvesignal: succeeds, exposing the approver’s payload in outputs underapproval.Resumed by a
rejectsignal: fails. By default this is terminal; setcontext['approval_reject_continues'] = Trueto continue the pipeline (FAILED_CONTINUE) instead.
Register it and use it like any task:
registry.register("approval", ApprovalTask) StageExecution(ref_id="gate", type="approval", tasks=[...])
Send the decision with
approve()/reject().- aliases = ['human_approval', 'hitl_approval']
- execute(stage)[source]
Execute the task.
- Parameters:
stage (StageExecution) – The stage execution context
- Returns:
TaskResult indicating status and any outputs
- Raises:
Exception – Any exception will be caught and handled by the runner
- Return type:
- stabilize.hitl.send_signal(queue, execution_id, stage_id, signal_name, signal_data=None, *, execution_type='PIPELINE', persistent=True)[source]
Send a resume signal to a (possibly not-yet-)suspended stage.
- Parameters:
queue (Queue) – The workflow queue.
execution_id (str) – Workflow id.
stage_id (str) – The suspended stage’s id.
signal_name (str) – Signal name the task checks for (e.g. “approve”).
signal_data (dict[str, Any] | None) – Payload delivered to the task as
_signal_data.execution_type (str) – Workflow type value (default “PIPELINE”).
persistent (bool) – Buffer the signal if the stage has not suspended yet (WCP-24). True by default so an early approval is not lost.
- Return type:
None
- stabilize.hitl.approve(queue, execution_id, stage_id, data=None, *, execution_type='PIPELINE')[source]
Approve a stage waiting on an
ApprovalTask.- Parameters:
queue (Queue)
execution_id (str)
stage_id (str)
data (dict[str, Any] | None)
execution_type (str)
- Return type:
None
- stabilize.hitl.reject(queue, execution_id, stage_id, data=None, *, execution_type='PIPELINE')[source]
Reject a stage waiting on an
ApprovalTask.- Parameters:
queue (Queue)
execution_id (str)
stage_id (str)
data (dict[str, Any] | None)
execution_type (str)
- Return type:
None
- stabilize.hitl.get_signal(stage)[source]
Return
(signal_name, signal_data)delivered to a resumed stage.Use inside a task’s
executeto branch on which signal woke it. Returns(None, {})when the stage was not resumed by a signal.- Parameters:
stage (StageExecution)
- Return type:
tuple[str | None, dict[str, Any]]
Fan-in Reducers
Declarative fan-in reducers for parallel branches (agentic ergonomics).
LangGraph channels take a reducer (e.g. operator.add) so parallel nodes
APPEND to a shared list instead of overwriting it. Stabilize’s default
ancestor-output merge is last-write-wins for scalar keys (list-valued keys
already concatenate), so N parallel branches each writing {"result": ...}
silently lose all but one.
This module adds opt-in, declarative reducers applied when a join stage
collects its upstream branches’ outputs. A stage sets output_reducers:
- StageExecution(
ref_id=”gather”, join_type=JoinType.AND, requisite_stage_ref_ids={“gen_1”, “gen_2”, “gen_3”}, output_reducers={“candidate”: “collect”, “score”: “sum”},
)
Each upstream branch that produced candidate contributes to a list under
that key in the join stage’s context; score values are summed. Keys
without a reducer keep the existing last-write-wins behavior, so this is
fully backward compatible.
Built-in reducers (name -> behavior):
- collect / append: gather each branch’s value into a list (a
branch’s list value is extended, a scalar is appended).
extend: flatten list values from all branches into one list.sum: numeric sum across branches.max/min: extremum across branches.merge: shallow-merge dict values across branches.first/last: first/last branch’s value (last == current default).
Custom reducers can be registered with register_reducer().
- stabilize.reducers.register_reducer(name, reducer)[source]
Register a custom named reducer usable in
output_reducers.- Parameters:
name (str)
reducer (Callable[[list[Any]], Any])
- Return type:
None
- stabilize.reducers.get_reducer(name)[source]
Look up a reducer by name (custom overrides built-in), or None.
- Parameters:
name (str)
- Return type:
Callable[[list[Any]], Any] | None
- stabilize.reducers.apply_output_reducers(reducers, branch_outputs)[source]
Combine per-branch outputs for the reducer-controlled keys.
- Parameters:
reducers (dict[str, str]) – Mapping of output key -> reducer name.
branch_outputs (list[dict[str, Any]]) – Each upstream branch’s outputs dict.
- Returns:
A dict of reduced values for exactly the keys named in
reducersthat at least one branch produced. Keys not named here are left to the caller’s normal merge.- Return type:
dict[str, Any]
LLM Toolkit
Dependency-light chat client for OpenAI-compatible / Ollama endpoints.
Uses only the standard library (urllib) so the LLM toolkit adds no runtime dependency and runs on a laptop. The endpoint and credentials are injectable so the same code targets Ollama (local or ollama.com cloud), OpenAI, or any OpenAI-compatible gateway.
- class stabilize.llm.client.ChatMessage(role, content=None, tool_calls=None, tool_call_id=None, name=None)[source]
Bases:
objectOne chat message.
tool_calls/tool_call_idsupport tool loops.- Parameters:
role (str)
content (str | None)
tool_calls (list[dict[str, Any]] | None)
tool_call_id (str | None)
name (str | None)
- role: str
- content: str | None = None
- tool_calls: list[dict[str, Any]] | None = None
- tool_call_id: str | None = None
- name: str | None = None
- class stabilize.llm.client.ChatResponse(content, tool_calls=<factory>, raw=<factory>)[source]
Bases:
objectA model response: assistant text and any requested tool calls.
- Parameters:
content (str)
tool_calls (list[dict[str, Any]])
raw (dict[str, Any])
- content: str
- tool_calls: list[dict[str, Any]]
- raw: dict[str, Any]
- property has_tool_calls: bool
- class stabilize.llm.client.LLMClient(model, base_url=None, api_key=None, api='openai', timeout=120.0, default_options=None)[source]
Bases:
objectMinimal OpenAI-compatible / Ollama chat client (stdlib only).
Two wire formats are supported: -
api="openai"(default): POST{base_url}/chat/completionswiththe OpenAI schema. Works for OpenAI and OpenAI-compatible gateways, including ollama.com cloud’s OpenAI endpoint.
api="ollama": POST{base_url}/api/chatwith the native Ollama schema (for a localollama serve).
Credentials default to the
OLLAMA_API_KEY/OPENAI_API_KEYenv vars; never hard-code keys.- Parameters:
model (str)
base_url (str | None)
api_key (str | None)
api (str)
timeout (float)
default_options (dict[str, Any] | None)
- chat(messages, tools=None, temperature=None, **options)[source]
Send a chat request and return the assistant response.
- Parameters:
messages (list[ChatMessage]) – Conversation so far.
tools (list[dict[str, Any]] | None) – Optional OpenAI-style tool/function schemas to expose.
temperature (float | None) – Sampling temperature (falls back to default_options).
**options (Any) – Extra provider options merged into the request.
- Return type:
- exception stabilize.llm.client.LLMError[source]
Bases:
ExceptionRaised when an LLM chat request fails.
Tool definitions for LLM tool-calling loops.
The @tool decorator turns a plain Python function into a callable tool with
an auto-generated OpenAI-style JSON schema (derived from its signature and
type hints). A ToolRegistry collects tools, exposes their schemas to
the model, and dispatches the model’s tool calls back to the functions.
- class stabilize.llm.tools.ToolSpec(func, name, description)[source]
Bases:
objectA registered tool: the callable plus its generated JSON schema.
- Parameters:
func (Callable[..., Any])
name (str)
description (str)
- stabilize.llm.tools.tool(_func=None, *, name=None, description=None)[source]
Decorate a function as an LLM tool.
The schema is derived from the signature and docstring:
@tool def get_weather(city: str) -> str: "Return the weather for a city." ...
- Parameters:
_func (Callable[[...], Any] | None)
name (str | None)
description (str | None)
- Return type:
Any
- class stabilize.llm.tools.ToolRegistry[source]
Bases:
objectCollects tools and dispatches model tool calls to them.
- add(func)[source]
Register a @tool-decorated function (or a plain callable).
- Parameters:
func (Callable[[...], Any])
- Return type:
LLM tasks: a plain chat task and a ReAct tool-calling agent loop.
Built on the engine’s existing primitives so they inherit its durability:
- LLMTask calls the model once and returns the completion.
- AgentLoopTask runs a bounded ReAct loop (model -> tool calls ->
results -> model …) inside a single task execution, so the whole agent turn is one durable, retryable unit of work.
The model client is injectable (constructor or the LLMTask.build_client
hook) so these stay dependency-light and target Ollama or OpenAI-compatible
endpoints without hard-coding anything.
- class stabilize.llm.tasks.LLMTask(client=None)[source]
Bases:
TaskCall an LLM once with a prompt/messages and return the completion.
Context keys (all optional unless noted): -
prompt(str): user prompt. Required unlessmessagesis given. -system(str): system prompt. -messages(list): full message list (overrides prompt/system). -model(str): model id (else the injected client’s model). -temperature(float): sampling temperature. -output_key(str): outputs key for the completion (default"completion").Outputs:
{output_key: <text>, "llm_raw": <provider response>}.- Parameters:
client (LLMClient | None)
- aliases = ['llm', 'chat']
- build_client(stage)[source]
Return the LLM client. Override to construct one from context, or pass a client to the constructor.
- Parameters:
stage (StageExecution)
- Return type:
- execute(stage)[source]
Execute the task.
- Parameters:
stage (StageExecution) – The stage execution context
- Returns:
TaskResult indicating status and any outputs
- Raises:
Exception – Any exception will be caught and handled by the runner
- Return type:
- class stabilize.llm.tasks.AgentLoopTask(client=None, tools=None)[source]
Bases:
LLMTaskA bounded ReAct tool-calling loop inside one task execution.
Each iteration: send the conversation (plus tool schemas) to the model; if it returns tool calls, dispatch them via the
ToolRegistry, append the results, and loop; otherwise return the final answer. Bounded bymax_iterationsso a misbehaving model cannot loop forever.Provide the tools by passing a
ToolRegistryto the constructor or by overridingbuild_tools().Context keys add to
LLMTask: -max_iterations(int): loop cap (default 8). -output_key(str): outputs key for the final answer (default"answer").Outputs:
{output_key: <final text>, "tool_invocations": [...], "iterations": <n>}.- Parameters:
client (LLMClient | None)
tools (ToolRegistry | None)
- aliases = ['agent', 'react_agent', 'agent_loop']
- build_tools(stage)[source]
Return the tool registry for this agent. Override or pass to ctor.
- Parameters:
stage (StageExecution)
- Return type:
- execute(stage)[source]
Execute the task.
- Parameters:
stage (StageExecution) – The stage execution context
- Returns:
TaskResult indicating status and any outputs
- Raises:
Exception – Any exception will be caught and handled by the runner
- Return type: