Skip to content

teff.flow

teff.flow

Modules:

Name Description
agent

High-level agent helpers built on :class:~teff.flow.Flow.

base

Linear chain builders for :class:~teff.flow.Flow.

case

Branch case for conditional routing.

compile

Compilation and serialization builders for :class:~teff.flow.Flow.

compiler

Declarative flow.yaml compiler — the authoring layer.

control

Branching, looping and routing builders for :class:~teff.flow.Flow.

flow

Fluid flow builder for constructing graphs.

harness

ReAct agent harness builders for :class:~teff.flow.Flow.

sub_flow

SubFlow — a node that executes a nested graph.

team

Supervised team builder for :class:~teff.flow.Flow.

Classes:

Name Description
AgentRole

One routed agent role for :meth:teff.flow.Flow.team.

Case

A single branch case used with Flow.branch().

Flow

Fluid builder for constructing graphs with branching.

SubFlow

A node that executes a sub-graph with optional key mapping.

Functions:

Name Description
agent_step

One routed agent: context builder → ReAct harness → append to conversation.

AgentRole

One routed agent role for :meth:teff.flow.Flow.team.

Describes how a role performs its slot: the system prompt, the state key that receives its final answer, and optional model/provider/tool knobs. :meth:build turns it into a :class:~teff.flow.SubFlow (the agent_step recipe) that plugs straight into the team's supervisor route loop::

flow.team(
    "Route to the coder, then finish.",
    roles={
        "coder": AgentRole("You write code.", output_key="code"),
        "planner": AgentRole(
            "You plan.", output_key="plan", use_tools=["web_search"]
        ),
    },
)

Parameters:

Name Type Description Default
system str

System prompt for the role.

''
output_key str

State key that receives the role's final answer.

required
model str | None

Model override; when omitted the team/flow default is used.

None
provider str | None

Provider override; when omitted the team/flow default is used.

None
sections dict[str, str] | None

Shared state key → label mapping rendered into the agent's context (defaults to {output_key: Capitalized}).

None
messages_key str

State key holding the shared conversation.

'messages'
use_tools str | list[str] | None

None/[] (no tools), "all", or an allowlist of tool names the role may call.

None
stream bool

Emit tokens as stream events (live rendering).

True
**config

Extra kwargs forwarded to the ReAct harness.

{}

Methods:

Name Description
build

Render the role as a routed agent_step SubFlow.

from_mapping

Build a role from a {system, output_key, ...} mapping.

Source code in teff/flow/agent.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
class AgentRole:
    """One routed agent role for :meth:`teff.flow.Flow.team`.

    Describes *how* a role performs its slot: the system prompt, the state
    key that receives its final answer, and optional model/provider/tool
    knobs.  :meth:`build` turns it into a :class:`~teff.flow.SubFlow`
    (the ``agent_step`` recipe) that plugs straight into the team's
    supervisor route loop::

        flow.team(
            "Route to the coder, then finish.",
            roles={
                "coder": AgentRole("You write code.", output_key="code"),
                "planner": AgentRole(
                    "You plan.", output_key="plan", use_tools=["web_search"]
                ),
            },
        )

    Args:
        system: System prompt for the role.
        output_key: State key that receives the role's final answer.
        model: Model override; when omitted the team/flow default is used.
        provider: Provider override; when omitted the team/flow default is
            used.
        sections: Shared state key → label mapping rendered into the agent's
            context (defaults to ``{output_key: Capitalized}``).
        messages_key: State key holding the shared conversation.
        use_tools: ``None``/``[]`` (no tools), ``"all"``, or an allowlist of
            tool names the role may call.
        stream: Emit tokens as stream events (live rendering).
        **config: Extra kwargs forwarded to the ReAct harness.
    """

    def __init__(
        self,
        system: str = "",
        *,
        output_key: str,
        model: str | None = None,
        provider: str | None = None,
        sections: dict[str, str] | None = None,
        messages_key: str = "messages",
        use_tools: str | list[str] | None = None,
        stream: bool = True,
        **config,
    ):
        self.system = system
        self.output_key = output_key
        self.model = model
        self.provider = provider
        self.sections = sections
        self.messages_key = messages_key
        self.use_tools = use_tools
        self.stream = stream
        self.config = dict(config)

    def build(
        self,
        *,
        model: str | None = None,
        provider: str | None = None,
        id: str = "",
    ) -> SubFlow:
        """Render the role as a routed ``agent_step`` SubFlow.

        *model*/*provider* fall back from the arguments (the team/flow
        defaults) to the role's own overrides.  Raises ``ValueError`` when
        neither provides them.
        """
        model = model or self.model
        provider = provider or self.provider
        if not model or not provider:
            raise ValueError(
                f"AgentRole {self.output_key!r} needs model and provider "
                "(set them on the role or on the team/flow)"
            )
        return agent_step(
            self.system,
            self.output_key,
            model=model,
            provider=provider,
            sections=self.sections,
            messages_key=self.messages_key,
            use_tools=self.use_tools,
            stream=self.stream,
            id=id,
            **self.config,
        )

    @classmethod
    def from_mapping(cls, data: dict, *, name: str = "") -> "AgentRole":
        """Build a role from a ``{system, output_key, ...}`` mapping.

        Normalises the YAML ``team.roles`` entries (and plain dicts passed
        to :meth:`Flow.team`) into an :class:`AgentRole`.  ``tools:`` is
        accepted as the legacy alias for ``use_tools:``; any other keys are
        kept and forwarded to the ReAct harness via :meth:`build`.
        """
        data = dict(data)
        out_key = data.get("output_key") or name
        if not out_key:
            raise ValueError("a team role needs an `output_key` (or a role name)")
        system = data.pop("system", "") or ""
        known = {
            "model",
            "provider",
            "sections",
            "messages_key",
            "use_tools",
            "stream",
        }
        kwargs = {k: data.pop(k) for k in list(data) if k in known}
        if "tools" in data and "use_tools" not in kwargs:
            kwargs["use_tools"] = data.pop("tools")
        if data:
            kwargs["config"] = data
        return cls(system, output_key=out_key, **kwargs)

build

build(*, model=None, provider=None, id='')

Render the role as a routed agent_step SubFlow.

model/provider fall back from the arguments (the team/flow defaults) to the role's own overrides. Raises ValueError when neither provides them.

Source code in teff/flow/agent.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
def build(
    self,
    *,
    model: str | None = None,
    provider: str | None = None,
    id: str = "",
) -> SubFlow:
    """Render the role as a routed ``agent_step`` SubFlow.

    *model*/*provider* fall back from the arguments (the team/flow
    defaults) to the role's own overrides.  Raises ``ValueError`` when
    neither provides them.
    """
    model = model or self.model
    provider = provider or self.provider
    if not model or not provider:
        raise ValueError(
            f"AgentRole {self.output_key!r} needs model and provider "
            "(set them on the role or on the team/flow)"
        )
    return agent_step(
        self.system,
        self.output_key,
        model=model,
        provider=provider,
        sections=self.sections,
        messages_key=self.messages_key,
        use_tools=self.use_tools,
        stream=self.stream,
        id=id,
        **self.config,
    )

from_mapping classmethod

from_mapping(data, *, name='')

Build a role from a {system, output_key, ...} mapping.

Normalises the YAML team.roles entries (and plain dicts passed to :meth:Flow.team) into an :class:AgentRole. tools: is accepted as the legacy alias for use_tools:; any other keys are kept and forwarded to the ReAct harness via :meth:build.

Source code in teff/flow/agent.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
@classmethod
def from_mapping(cls, data: dict, *, name: str = "") -> "AgentRole":
    """Build a role from a ``{system, output_key, ...}`` mapping.

    Normalises the YAML ``team.roles`` entries (and plain dicts passed
    to :meth:`Flow.team`) into an :class:`AgentRole`.  ``tools:`` is
    accepted as the legacy alias for ``use_tools:``; any other keys are
    kept and forwarded to the ReAct harness via :meth:`build`.
    """
    data = dict(data)
    out_key = data.get("output_key") or name
    if not out_key:
        raise ValueError("a team role needs an `output_key` (or a role name)")
    system = data.pop("system", "") or ""
    known = {
        "model",
        "provider",
        "sections",
        "messages_key",
        "use_tools",
        "stream",
    }
    kwargs = {k: data.pop(k) for k in list(data) if k in known}
    if "tools" in data and "use_tools" not in kwargs:
        kwargs["use_tools"] = data.pop("tools")
    if data:
        kwargs["config"] = data
    return cls(system, output_key=out_key, **kwargs)

Case

A single branch case used with Flow.branch().

Methods:

Name Description
add

Add a node to this case branch.

Source code in teff/flow/case.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Case:
    """A single branch case used with ``Flow.branch()``."""

    def __init__(self, value: str):
        self.value = value
        self._nodes: list[Node] = []
        self._ids: list[str | None] = []

    def add(self, node: Node, id: str | None = None) -> "Case":
        """Add a node to this case branch.

        *id* optionally names the node in the compiled graph instead of
        the auto-generated ``{type}_{n}``.
        """
        self._nodes.append(node)
        self._ids.append(id)
        return self

add

add(node, id=None)

Add a node to this case branch.

id optionally names the node in the compiled graph instead of the auto-generated {type}_{n}.

Source code in teff/flow/case.py
14
15
16
17
18
19
20
21
22
def add(self, node: Node, id: str | None = None) -> "Case":
    """Add a node to this case branch.

    *id* optionally names the node in the compiled graph instead of
    the auto-generated ``{type}_{n}``.
    """
    self._nodes.append(node)
    self._ids.append(id)
    return self

Flow

Fluid builder for constructing graphs with branching.

Parameters:

Name Type Description Default
name str

Optional flow name.

''
default_provider str | None

Optional default provider name used by LLM nodes that don't set provider themselves. Must be declared in providers.

None
default_model str | None

Optional default model name used by LLM nodes that don't set model themselves. model= on a node always wins.

None
providers dict | ProviderRegistry | None

The {name: Provider} map, :class:~teff.provider.ProviderRegistry, or YAML-style list of preset names threaded into the compiled graph. Every provider the graph references must be declared here.

None

Usage::

flow = Flow(
    "my-flow",
    providers=ProviderRegistry.from_presets("ollama"),
    default_provider="ollama",
)
flow.step(LLM(model="llama3.1:8b"))
flow.branch("status", Case("ok").add(ok_node), default=err_node)
graph = flow.compile()

The graph-building methods are implemented by dedicated builders:

  • :mod:teff.flow.basestep/llm/transform & co. (linear chain).
  • :mod:teff.flow.teamteam (supervised agent team).
  • :mod:teff.flow.controlparallel/map/branch/ interrupt/loop/route/command (control flow).
  • :mod:teff.flow.harnessharness/react (ReAct agent loop).
  • :mod:teff.flow.compilecompile/label/to_yaml (serialization).

Methods:

Name Description
add_flow

Embed a sub-flow as a single node (SubFlow). See

append_assistant

Add a :class:~teff.node.context.AppendAssistant node. See

branch

Add conditional branching from the last added node. See

command

Add a declarative command node that routes by state. See

compile

Compile the flow into a Graph ready for execution. See

context_builder

Add a :class:~teff.node.context.ContextBuilder node. See

converge

Merge all branch ends into a single node. See

default

Add a fallback node for the most recent guarded step(). See

harness

Build a ReAct-style agent loop (LLM ↔ tools) inside this flow.

interrupt

Pause the flow for human input at this point. See

interrupt_loop

Ask the human through an interrupt and re-ask until the answer

label

Attach a route name to the most recently added node. See

label_target

Resolve a declarative goto against labels to a real node id.

llm

Add an :class:~teff.node.llm.LLM chat node. See

loop

Run a chain repeatedly until state[key] equals until. See

map

Dynamically fan a state list out across parallel branches. See

parallel

Run several branch chains concurrently from the last node. See

react

Alias for :meth:harness (ReAct agent loop). See

route

Route between agent chains under a supervisor decider. See

step

Append a node to the linear chain. See

supervisor

Add a :class:~teff.node.supervisor.Supervisor decider node. See

team

Compose a supervised agent team in one call. See

to_yaml

Export the compiled flow as a workflow.yaml document. See

transform

Add a :class:~teff.node.transform.Transform node. See

Source code in teff/flow/flow.py
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
class Flow:
    """Fluid builder for constructing graphs with branching.

    Args:
        name: Optional flow name.
        default_provider: Optional default provider name used by LLM nodes
            that don't set ``provider`` themselves.  Must be declared in
            ``providers``.
        default_model: Optional default model name used by LLM nodes that
            don't set ``model`` themselves.  ``model=`` on a node always
            wins.
        providers: The ``{name: Provider}`` map,
            :class:`~teff.provider.ProviderRegistry`, or YAML-style list
            of preset names threaded into the compiled graph.  Every
            provider the graph references must be declared here.

    Usage::

        flow = Flow(
            "my-flow",
            providers=ProviderRegistry.from_presets("ollama"),
            default_provider="ollama",
        )
        flow.step(LLM(model="llama3.1:8b"))
        flow.branch("status", Case("ok").add(ok_node), default=err_node)
        graph = flow.compile()

    The graph-building methods are implemented by dedicated builders:

    - :mod:`teff.flow.base` — ``step``/``llm``/``transform`` &
      co. (linear chain).
    - :mod:`teff.flow.team` — ``team`` (supervised agent team).
    - :mod:`teff.flow.control` — ``parallel``/``map``/``branch``/
      ``interrupt``/``loop``/``route``/``command`` (control flow).
    - :mod:`teff.flow.harness` — ``harness``/``react`` (ReAct agent loop).
    - :mod:`teff.flow.compile` — ``compile``/``label``/``to_yaml``
      (serialization).
    """

    def __init__(
        self,
        name: str = "",
        *,
        providers: "dict | ProviderRegistry | None" = None,
        default_provider: str | None = None,
        default_model: str | None = None,
    ):
        self._name = name
        self._default_provider = default_provider
        self._default_model = default_model
        self._providers = providers
        self._nodes: list[Node] = []
        self._node_ids: list[str] = []
        self._edges: list[Edge] = []
        self._counter = 0
        self._last_added: str | None = None
        self._branch_ends: list[str] = []
        self._route_terminates = False
        self._guarded_step: str | None = None
        self._loop_labels: dict[str, str] = {}
        self._loop_decider: str | None = None

        self._base = BaseBuilder(self)
        self._team = TeamBuilder(self)
        self._control = ControlBuilder(self)
        self._harness = HarnessBuilder(self)
        self._compile = CompileBuilder(self)

    def _next_id(self, node: Node, id_hint: str | None = None) -> str:
        self._counter += 1
        nid = id_hint or f"{node.type}_{self._counter}"
        if nid in self._node_ids:
            raise ValueError(f"duplicate node id: {nid}")
        return nid

    def _existing_id(self, node: Node) -> str | None:
        """The id *node* was registered under, if this instance is already added.

        Loop bodies re-reference nodes that were added earlier in the flow
        (a planner, an interrupt, its classifier/validate).  Re-adding the
        same instance under a fresh auto-generated id would duplicate it in
        the compiled graph (``llm_chat_7``, ``subflow_9``, …) — instead the
        chain should route through the node's first registration.
        """
        for idx, existing in enumerate(self._nodes):
            if existing is node:
                return self._node_ids[idx]
        return None

    def _check_continuation(self) -> None:
        """Raise if the last route() terminated the flow (finish=None)."""
        if self._route_terminates:
            raise ValueError(
                "route() with finish=None terminates the flow when the decider "
                "returns 'finish'; pass finish=<chain> before adding more nodes"
            )

    @staticmethod
    def _as_chain(node_or_chain) -> list[Node]:
        if node_or_chain is None:
            return []
        if isinstance(node_or_chain, Node):
            return [node_or_chain]

        def to_node(item):
            if isinstance(item, Node):
                return item
            if callable(item):
                return make_function_node(item)
            raise TypeError(
                f"expected Node or callable in chain, got {type(item).__name__}"
            )

        return [to_node(item) for item in node_or_chain]

    # ------------------------------------------------------------------
    # Linear chain (see teff.flow.base.BaseBuilder)
    # ------------------------------------------------------------------

    def step(
        self,
        node: Node | FunctionNode,
        id: str | None = None,
        *,
        when: str | Callable[[dict], bool] | None = None,
    ) -> "Flow":
        """Append a node to the linear chain.  See
        :meth:`teff.flow.base.BaseBuilder.step`."""
        return self._base.step(node, id=id, when=when)

    def llm(
        self,
        node: LLM | None = None,
        id: str | None = None,
        *,
        memory: MemoryConfig | dict | None = None,
        **config,
    ) -> "Flow":
        """Add an :class:`~teff.node.llm.LLM` chat node.  See
        :meth:`teff.flow.base.BaseBuilder.llm`."""
        return self._base.llm(node, id=id, memory=memory, **config)

    def transform(
        self, node: Transform | None = None, id: str | None = None, **config
    ) -> "Flow":
        """Add a :class:`~teff.node.transform.Transform` node.  See
        :meth:`teff.flow.base.BaseBuilder.transform`."""
        return self._base.transform(node, id=id, **config)

    def context_builder(
        self,
        node: "ContextBuilder | None" = None,
        id: str | None = None,
        **config,
    ) -> "Flow":
        """Add a :class:`~teff.node.context.ContextBuilder` node.  See
        :meth:`teff.flow.base.BaseBuilder.context_builder`."""
        return self._base.context_builder(node, id=id, **config)

    def append_assistant(
        self,
        node: "AppendAssistant | None" = None,
        id: str | None = None,
        **config,
    ) -> "Flow":
        """Add a :class:`~teff.node.context.AppendAssistant` node.  See
        :meth:`teff.flow.base.BaseBuilder.append_assistant`."""
        return self._base.append_assistant(node, id=id, **config)

    def supervisor(
        self,
        node: "Supervisor | None" = None,
        id: str | None = None,
        **config,
    ) -> "Flow":
        """Add a :class:`~teff.node.supervisor.Supervisor` decider node.  See
        :meth:`teff.flow.base.BaseBuilder.supervisor`."""
        return self._base.supervisor(node, id=id, **config)

    def team(
        self,
        system: str = "",
        *,
        roles: dict[str, RoleSpec],
        model: str | None = None,
        provider: str | None = None,
        messages_key: str = "messages",
        sections: dict[str, str] | None = None,
        route_keys: dict[str, str] | None = None,
        done_keys: list[str] | None = None,
        done_mode: str = "all",
        fallback: str = "",
        max_rounds: int = 6,
        finish: "Node | list[Node] | None" = None,
        id: str | None = None,
    ) -> "Flow":
        """Compose a supervised agent team in one call.  See
        :meth:`teff.flow.team.TeamBuilder.team`."""
        return self._team.team(
            system,
            roles=roles,
            model=model,
            provider=provider,
            messages_key=messages_key,
            sections=sections,
            route_keys=route_keys,
            done_keys=done_keys,
            done_mode=done_mode,
            fallback=fallback,
            max_rounds=max_rounds,
            finish=finish,
            id=id,
        )

    # ------------------------------------------------------------------
    # Control flow (see teff.flow.control.ControlBuilder)
    # ------------------------------------------------------------------

    def add_flow(self, flow: "Flow", id: str | None = None, **kw) -> "Flow":
        """Embed a sub-flow as a single node (SubFlow).  See
        :meth:`teff.flow.control.ControlBuilder.add_flow`."""
        return self._control.add_flow(flow, id=id, **kw)

    def parallel(self, *branches, id: str | None = None) -> "Flow":
        """Run several branch chains concurrently from the last node.  See
        :meth:`teff.flow.control.ControlBuilder.parallel`."""
        return self._control.parallel(*branches, id=id)

    def map(
        self,
        processor: Node | list[Node] | dict | list[dict],
        *,
        input_keys: str | list[str] = "",
        output_key: str = "",
        chunk_size: int | None = None,
        max_concurrency: int | None = None,
        id: str | None = None,
        **kwargs,
    ) -> "Flow":
        """Dynamically fan a state list out across parallel branches.  See
        :meth:`teff.flow.control.ControlBuilder.map`."""
        return self._control.map(
            processor,
            input_keys=input_keys,
            output_key=output_key,
            chunk_size=chunk_size,
            max_concurrency=max_concurrency,
            id=id,
            **kwargs,
        )

    def branch(self, key: str, *cases: "Case", default: Node | None = None) -> "Flow":
        """Add conditional branching from the last added node.  See
        :meth:`teff.flow.control.ControlBuilder.branch`."""
        return self._control.branch(key, *cases, default=default)

    def default(self, node: Node, id: str | None = None) -> "Flow":
        """Add a fallback node for the most recent guarded ``step()``.  See
        :meth:`teff.flow.control.ControlBuilder.default`."""
        return self._control.default(node, id=id)

    def converge(self, node: Node, id: str | None = None) -> "Flow":
        """Merge all branch ends into a single node.  See
        :meth:`teff.flow.control.ControlBuilder.converge`."""
        return self._control.converge(node, id=id)

    def interrupt(
        self,
        key: str,
        prompt: str = "",
        *,
        accept: "Ask | None" = None,
        id: str | None = None,
    ) -> "Flow":
        """Pause the flow for human input at this point.  See
        :meth:`teff.flow.control.ControlBuilder.interrupt`."""
        return self._control.interrupt(key, prompt=prompt, accept=accept, id=id)

    def loop(
        self,
        key: str,
        until: str,
        done: Node | list[Node],
        body: Node | list[Node],
        *,
        max_rounds: int | None = None,
    ) -> "Flow":
        """Run a chain repeatedly until ``state[key]`` equals *until*.  See
        :meth:`teff.flow.control.ControlBuilder.loop`."""
        return self._control.loop(key, until, done, body, max_rounds=max_rounds)

    def interrupt_loop(
        self,
        key: str,
        *,
        accept: "Ask",
        body: Node | list[Node],
        done: Node | list[Node],
        prompt: str = "",
        id: str | None = None,
    ) -> "Flow":
        """Ask the human through an interrupt and re-ask until the answer
        passes.  See :meth:`teff.flow.control.ControlBuilder.interrupt_loop`."""
        return self._control.interrupt_loop(
            key,
            accept=accept,
            body=body,
            done=done,
            prompt=prompt,
            id=id,
        )

    def route(
        self,
        key: str,
        *,
        finish: Node | list[Node] | None = None,
        **agents,
    ) -> "Flow":
        """Route between agent chains under a supervisor decider.  See
        :meth:`teff.flow.control.ControlBuilder.route`."""
        return self._control.route(key, finish=finish, **agents)

    def command(
        self,
        *,
        routes: dict | None = None,
        goto: str | None = None,
        update: dict | None = None,
        id: str | None = None,
    ) -> "Flow":
        """Add a declarative ``command`` node that routes by state.  See
        :meth:`teff.flow.control.ControlBuilder.command`."""
        return self._control.command(routes=routes, goto=goto, update=update, id=id)

    # ------------------------------------------------------------------
    # Agent harness (see teff.flow.harness.HarnessBuilder)
    # ------------------------------------------------------------------

    def harness(
        self,
        model: str | None = None,
        system: str = "",
        *,
        agent: "ReActAgent | type[ReActAgent] | None" = None,
        input_key: str = "input",
        output_key: str = "output",
        messages_key: str = "messages",
        memory: "MemoryConfig | dict | None" = None,
        max_tool_rounds: int = 10,
        tool_error_mode: str = "message",
        parse_text_tool_calls: bool = True,
        temperature: float | None = None,
        max_tokens: int | None = None,
        response_format: dict | None = None,
        use_tools: str | list[str] | None = None,
        skills: list | None = None,
        skill_dir: str = "skills",
        id: str | None = None,
        **config,
    ) -> "Flow":
        """Build a ReAct-style agent loop (LLM ↔ tools) inside this flow.
        See :meth:`teff.flow.harness.HarnessBuilder.harness`."""
        return self._harness.harness(
            model,
            system,
            agent=agent,
            input_key=input_key,
            output_key=output_key,
            messages_key=messages_key,
            memory=memory,
            max_tool_rounds=max_tool_rounds,
            tool_error_mode=tool_error_mode,
            parse_text_tool_calls=parse_text_tool_calls,
            temperature=temperature,
            max_tokens=max_tokens,
            response_format=response_format,
            use_tools=use_tools,
            skills=skills,
            skill_dir=skill_dir,
            id=id,
            **config,
        )

    def react(
        self,
        model: str | None = None,
        system: str = "",
        *,
        agent: "ReActAgent | type[ReActAgent] | None" = None,
        input_key: str = "input",
        output_key: str = "output",
        messages_key: str = "messages",
        memory: "MemoryConfig | dict | None" = None,
        **config,
    ) -> "Flow":
        """Alias for :meth:`harness` (ReAct agent loop).  See
        :meth:`teff.flow.harness.HarnessBuilder.react`."""
        return self._harness.react(
            model,
            system,
            agent=agent,
            input_key=input_key,
            output_key=output_key,
            messages_key=messages_key,
            memory=memory,
            **config,
        )

    # ------------------------------------------------------------------
    # Compilation (see teff.flow.compile.CompileBuilder)
    # ------------------------------------------------------------------

    def compile(self) -> Graph:
        """Compile the flow into a ``Graph`` ready for execution.  See
        :meth:`teff.flow.compile.CompileBuilder.compile`."""
        return self._compile.compile()

    def label(self, name: str) -> "Flow":
        """Attach a route *name* to the most recently added node.  See
        :meth:`teff.flow.compile.CompileBuilder.label`."""
        return self._compile.label(name)

    def label_target(self, goto: str) -> str:
        """Resolve a declarative ``goto`` against labels to a real node id.
        See :meth:`teff.flow.compile.CompileBuilder.label_target`."""
        return self._compile.label_target(goto)

    def to_yaml(
        self,
        *,
        tools: list | None = None,
        initial: dict | None = None,
        reducers: dict | None = None,
    ) -> str:
        """Export the compiled flow as a ``workflow.yaml`` document.  See
        :meth:`teff.flow.compile.CompileBuilder.to_yaml`."""
        return self._compile.to_yaml(tools=tools, initial=initial, reducers=reducers)

add_flow

add_flow(flow, id=None, **kw)

Embed a sub-flow as a single node (SubFlow). See :meth:teff.flow.control.ControlBuilder.add_flow.

Source code in teff/flow/flow.py
263
264
265
266
def add_flow(self, flow: "Flow", id: str | None = None, **kw) -> "Flow":
    """Embed a sub-flow as a single node (SubFlow).  See
    :meth:`teff.flow.control.ControlBuilder.add_flow`."""
    return self._control.add_flow(flow, id=id, **kw)

append_assistant

append_assistant(node=None, id=None, **config)

Add a :class:~teff.node.context.AppendAssistant node. See :meth:teff.flow.base.BaseBuilder.append_assistant.

Source code in teff/flow/flow.py
204
205
206
207
208
209
210
211
212
def append_assistant(
    self,
    node: "AppendAssistant | None" = None,
    id: str | None = None,
    **config,
) -> "Flow":
    """Add a :class:`~teff.node.context.AppendAssistant` node.  See
    :meth:`teff.flow.base.BaseBuilder.append_assistant`."""
    return self._base.append_assistant(node, id=id, **config)

branch

branch(key, *cases, default=None)

Add conditional branching from the last added node. See :meth:teff.flow.control.ControlBuilder.branch.

Source code in teff/flow/flow.py
296
297
298
299
def branch(self, key: str, *cases: "Case", default: Node | None = None) -> "Flow":
    """Add conditional branching from the last added node.  See
    :meth:`teff.flow.control.ControlBuilder.branch`."""
    return self._control.branch(key, *cases, default=default)

command

command(*, routes=None, goto=None, update=None, id=None)

Add a declarative command node that routes by state. See :meth:teff.flow.control.ControlBuilder.command.

Source code in teff/flow/flow.py
368
369
370
371
372
373
374
375
376
377
378
def command(
    self,
    *,
    routes: dict | None = None,
    goto: str | None = None,
    update: dict | None = None,
    id: str | None = None,
) -> "Flow":
    """Add a declarative ``command`` node that routes by state.  See
    :meth:`teff.flow.control.ControlBuilder.command`."""
    return self._control.command(routes=routes, goto=goto, update=update, id=id)

compile

compile()

Compile the flow into a Graph ready for execution. See :meth:teff.flow.compile.CompileBuilder.compile.

Source code in teff/flow/flow.py
458
459
460
461
def compile(self) -> Graph:
    """Compile the flow into a ``Graph`` ready for execution.  See
    :meth:`teff.flow.compile.CompileBuilder.compile`."""
    return self._compile.compile()

context_builder

context_builder(node=None, id=None, **config)

Add a :class:~teff.node.context.ContextBuilder node. See :meth:teff.flow.base.BaseBuilder.context_builder.

Source code in teff/flow/flow.py
194
195
196
197
198
199
200
201
202
def context_builder(
    self,
    node: "ContextBuilder | None" = None,
    id: str | None = None,
    **config,
) -> "Flow":
    """Add a :class:`~teff.node.context.ContextBuilder` node.  See
    :meth:`teff.flow.base.BaseBuilder.context_builder`."""
    return self._base.context_builder(node, id=id, **config)

converge

converge(node, id=None)

Merge all branch ends into a single node. See :meth:teff.flow.control.ControlBuilder.converge.

Source code in teff/flow/flow.py
306
307
308
309
def converge(self, node: Node, id: str | None = None) -> "Flow":
    """Merge all branch ends into a single node.  See
    :meth:`teff.flow.control.ControlBuilder.converge`."""
    return self._control.converge(node, id=id)

default

default(node, id=None)

Add a fallback node for the most recent guarded step(). See :meth:teff.flow.control.ControlBuilder.default.

Source code in teff/flow/flow.py
301
302
303
304
def default(self, node: Node, id: str | None = None) -> "Flow":
    """Add a fallback node for the most recent guarded ``step()``.  See
    :meth:`teff.flow.control.ControlBuilder.default`."""
    return self._control.default(node, id=id)

harness

harness(
    model=None,
    system="",
    *,
    agent=None,
    input_key="input",
    output_key="output",
    messages_key="messages",
    memory=None,
    max_tool_rounds=10,
    tool_error_mode="message",
    parse_text_tool_calls=True,
    temperature=None,
    max_tokens=None,
    response_format=None,
    use_tools=None,
    skills=None,
    skill_dir="skills",
    id=None,
    **config,
)

Build a ReAct-style agent loop (LLM ↔ tools) inside this flow. See :meth:teff.flow.harness.HarnessBuilder.harness.

Source code in teff/flow/flow.py
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
def harness(
    self,
    model: str | None = None,
    system: str = "",
    *,
    agent: "ReActAgent | type[ReActAgent] | None" = None,
    input_key: str = "input",
    output_key: str = "output",
    messages_key: str = "messages",
    memory: "MemoryConfig | dict | None" = None,
    max_tool_rounds: int = 10,
    tool_error_mode: str = "message",
    parse_text_tool_calls: bool = True,
    temperature: float | None = None,
    max_tokens: int | None = None,
    response_format: dict | None = None,
    use_tools: str | list[str] | None = None,
    skills: list | None = None,
    skill_dir: str = "skills",
    id: str | None = None,
    **config,
) -> "Flow":
    """Build a ReAct-style agent loop (LLM ↔ tools) inside this flow.
    See :meth:`teff.flow.harness.HarnessBuilder.harness`."""
    return self._harness.harness(
        model,
        system,
        agent=agent,
        input_key=input_key,
        output_key=output_key,
        messages_key=messages_key,
        memory=memory,
        max_tool_rounds=max_tool_rounds,
        tool_error_mode=tool_error_mode,
        parse_text_tool_calls=parse_text_tool_calls,
        temperature=temperature,
        max_tokens=max_tokens,
        response_format=response_format,
        use_tools=use_tools,
        skills=skills,
        skill_dir=skill_dir,
        id=id,
        **config,
    )

interrupt

interrupt(key, prompt='', *, accept=None, id=None)

Pause the flow for human input at this point. See :meth:teff.flow.control.ControlBuilder.interrupt.

Source code in teff/flow/flow.py
311
312
313
314
315
316
317
318
319
320
321
def interrupt(
    self,
    key: str,
    prompt: str = "",
    *,
    accept: "Ask | None" = None,
    id: str | None = None,
) -> "Flow":
    """Pause the flow for human input at this point.  See
    :meth:`teff.flow.control.ControlBuilder.interrupt`."""
    return self._control.interrupt(key, prompt=prompt, accept=accept, id=id)

interrupt_loop

interrupt_loop(key, *, accept, body, done, prompt='', id=None)

Ask the human through an interrupt and re-ask until the answer passes. See :meth:teff.flow.control.ControlBuilder.interrupt_loop.

Source code in teff/flow/flow.py
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
def interrupt_loop(
    self,
    key: str,
    *,
    accept: "Ask",
    body: Node | list[Node],
    done: Node | list[Node],
    prompt: str = "",
    id: str | None = None,
) -> "Flow":
    """Ask the human through an interrupt and re-ask until the answer
    passes.  See :meth:`teff.flow.control.ControlBuilder.interrupt_loop`."""
    return self._control.interrupt_loop(
        key,
        accept=accept,
        body=body,
        done=done,
        prompt=prompt,
        id=id,
    )

label

label(name)

Attach a route name to the most recently added node. See :meth:teff.flow.compile.CompileBuilder.label.

Source code in teff/flow/flow.py
463
464
465
466
def label(self, name: str) -> "Flow":
    """Attach a route *name* to the most recently added node.  See
    :meth:`teff.flow.compile.CompileBuilder.label`."""
    return self._compile.label(name)

label_target

label_target(goto)

Resolve a declarative goto against labels to a real node id. See :meth:teff.flow.compile.CompileBuilder.label_target.

Source code in teff/flow/flow.py
468
469
470
471
def label_target(self, goto: str) -> str:
    """Resolve a declarative ``goto`` against labels to a real node id.
    See :meth:`teff.flow.compile.CompileBuilder.label_target`."""
    return self._compile.label_target(goto)

llm

llm(node=None, id=None, *, memory=None, **config)

Add an :class:~teff.node.llm.LLM chat node. See :meth:teff.flow.base.BaseBuilder.llm.

Source code in teff/flow/flow.py
175
176
177
178
179
180
181
182
183
184
185
def llm(
    self,
    node: LLM | None = None,
    id: str | None = None,
    *,
    memory: MemoryConfig | dict | None = None,
    **config,
) -> "Flow":
    """Add an :class:`~teff.node.llm.LLM` chat node.  See
    :meth:`teff.flow.base.BaseBuilder.llm`."""
    return self._base.llm(node, id=id, memory=memory, **config)

loop

loop(key, until, done, body, *, max_rounds=None)

Run a chain repeatedly until state[key] equals until. See :meth:teff.flow.control.ControlBuilder.loop.

Source code in teff/flow/flow.py
323
324
325
326
327
328
329
330
331
332
333
334
def loop(
    self,
    key: str,
    until: str,
    done: Node | list[Node],
    body: Node | list[Node],
    *,
    max_rounds: int | None = None,
) -> "Flow":
    """Run a chain repeatedly until ``state[key]`` equals *until*.  See
    :meth:`teff.flow.control.ControlBuilder.loop`."""
    return self._control.loop(key, until, done, body, max_rounds=max_rounds)

map

map(
    processor,
    *,
    input_keys="",
    output_key="",
    chunk_size=None,
    max_concurrency=None,
    id=None,
    **kwargs,
)

Dynamically fan a state list out across parallel branches. See :meth:teff.flow.control.ControlBuilder.map.

Source code in teff/flow/flow.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
def map(
    self,
    processor: Node | list[Node] | dict | list[dict],
    *,
    input_keys: str | list[str] = "",
    output_key: str = "",
    chunk_size: int | None = None,
    max_concurrency: int | None = None,
    id: str | None = None,
    **kwargs,
) -> "Flow":
    """Dynamically fan a state list out across parallel branches.  See
    :meth:`teff.flow.control.ControlBuilder.map`."""
    return self._control.map(
        processor,
        input_keys=input_keys,
        output_key=output_key,
        chunk_size=chunk_size,
        max_concurrency=max_concurrency,
        id=id,
        **kwargs,
    )

parallel

parallel(*branches, id=None)

Run several branch chains concurrently from the last node. See :meth:teff.flow.control.ControlBuilder.parallel.

Source code in teff/flow/flow.py
268
269
270
271
def parallel(self, *branches, id: str | None = None) -> "Flow":
    """Run several branch chains concurrently from the last node.  See
    :meth:`teff.flow.control.ControlBuilder.parallel`."""
    return self._control.parallel(*branches, id=id)

react

react(
    model=None,
    system="",
    *,
    agent=None,
    input_key="input",
    output_key="output",
    messages_key="messages",
    memory=None,
    **config,
)

Alias for :meth:harness (ReAct agent loop). See :meth:teff.flow.harness.HarnessBuilder.react.

Source code in teff/flow/flow.py
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
def react(
    self,
    model: str | None = None,
    system: str = "",
    *,
    agent: "ReActAgent | type[ReActAgent] | None" = None,
    input_key: str = "input",
    output_key: str = "output",
    messages_key: str = "messages",
    memory: "MemoryConfig | dict | None" = None,
    **config,
) -> "Flow":
    """Alias for :meth:`harness` (ReAct agent loop).  See
    :meth:`teff.flow.harness.HarnessBuilder.react`."""
    return self._harness.react(
        model,
        system,
        agent=agent,
        input_key=input_key,
        output_key=output_key,
        messages_key=messages_key,
        memory=memory,
        **config,
    )

route

route(key, *, finish=None, **agents)

Route between agent chains under a supervisor decider. See :meth:teff.flow.control.ControlBuilder.route.

Source code in teff/flow/flow.py
357
358
359
360
361
362
363
364
365
366
def route(
    self,
    key: str,
    *,
    finish: Node | list[Node] | None = None,
    **agents,
) -> "Flow":
    """Route between agent chains under a supervisor decider.  See
    :meth:`teff.flow.control.ControlBuilder.route`."""
    return self._control.route(key, finish=finish, **agents)

step

step(node, id=None, *, when=None)

Append a node to the linear chain. See :meth:teff.flow.base.BaseBuilder.step.

Source code in teff/flow/flow.py
164
165
166
167
168
169
170
171
172
173
def step(
    self,
    node: Node | FunctionNode,
    id: str | None = None,
    *,
    when: str | Callable[[dict], bool] | None = None,
) -> "Flow":
    """Append a node to the linear chain.  See
    :meth:`teff.flow.base.BaseBuilder.step`."""
    return self._base.step(node, id=id, when=when)

supervisor

supervisor(node=None, id=None, **config)

Add a :class:~teff.node.supervisor.Supervisor decider node. See :meth:teff.flow.base.BaseBuilder.supervisor.

Source code in teff/flow/flow.py
214
215
216
217
218
219
220
221
222
def supervisor(
    self,
    node: "Supervisor | None" = None,
    id: str | None = None,
    **config,
) -> "Flow":
    """Add a :class:`~teff.node.supervisor.Supervisor` decider node.  See
    :meth:`teff.flow.base.BaseBuilder.supervisor`."""
    return self._base.supervisor(node, id=id, **config)

team

team(
    system="",
    *,
    roles,
    model=None,
    provider=None,
    messages_key="messages",
    sections=None,
    route_keys=None,
    done_keys=None,
    done_mode="all",
    fallback="",
    max_rounds=6,
    finish=None,
    id=None,
)

Compose a supervised agent team in one call. See :meth:teff.flow.team.TeamBuilder.team.

Source code in teff/flow/flow.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
def team(
    self,
    system: str = "",
    *,
    roles: dict[str, RoleSpec],
    model: str | None = None,
    provider: str | None = None,
    messages_key: str = "messages",
    sections: dict[str, str] | None = None,
    route_keys: dict[str, str] | None = None,
    done_keys: list[str] | None = None,
    done_mode: str = "all",
    fallback: str = "",
    max_rounds: int = 6,
    finish: "Node | list[Node] | None" = None,
    id: str | None = None,
) -> "Flow":
    """Compose a supervised agent team in one call.  See
    :meth:`teff.flow.team.TeamBuilder.team`."""
    return self._team.team(
        system,
        roles=roles,
        model=model,
        provider=provider,
        messages_key=messages_key,
        sections=sections,
        route_keys=route_keys,
        done_keys=done_keys,
        done_mode=done_mode,
        fallback=fallback,
        max_rounds=max_rounds,
        finish=finish,
        id=id,
    )

to_yaml

to_yaml(*, tools=None, initial=None, reducers=None)

Export the compiled flow as a workflow.yaml document. See :meth:teff.flow.compile.CompileBuilder.to_yaml.

Source code in teff/flow/flow.py
473
474
475
476
477
478
479
480
481
482
def to_yaml(
    self,
    *,
    tools: list | None = None,
    initial: dict | None = None,
    reducers: dict | None = None,
) -> str:
    """Export the compiled flow as a ``workflow.yaml`` document.  See
    :meth:`teff.flow.compile.CompileBuilder.to_yaml`."""
    return self._compile.to_yaml(tools=tools, initial=initial, reducers=reducers)

transform

transform(node=None, id=None, **config)

Add a :class:~teff.node.transform.Transform node. See :meth:teff.flow.base.BaseBuilder.transform.

Source code in teff/flow/flow.py
187
188
189
190
191
192
def transform(
    self, node: Transform | None = None, id: str | None = None, **config
) -> "Flow":
    """Add a :class:`~teff.node.transform.Transform` node.  See
    :meth:`teff.flow.base.BaseBuilder.transform`."""
    return self._base.transform(node, id=id, **config)

SubFlow

Bases: Node

A node that executes a sub-graph with optional key mapping.

Parameters:

Name Type Description Default
graph Graph

Compiled sub-graph to run.

required
input_map dict[str, str] | None

Parent key → sub-graph key (default: passthrough).

None
output_map dict[str, str] | None

Sub-graph key → parent key (default: passthrough).

None
max_iterations int | None

Max node executions inside the sub-graph (passed to graph.run()). None means unlimited.

None
Source code in teff/flow/sub_flow.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
class SubFlow(Node):
    """A node that executes a sub-graph with optional key mapping.

    Args:
        graph: Compiled sub-graph to run.
        input_map: Parent key → sub-graph key (default: passthrough).
        output_map: Sub-graph key → parent key (default: passthrough).
        max_iterations: Max node executions inside the sub-graph
            (passed to ``graph.run()``).  ``None`` means unlimited.
    """

    type = "subflow"

    def __init__(
        self,
        graph: Graph,
        input_map: dict[str, str] | None = None,
        output_map: dict[str, str] | None = None,
        max_iterations: int | None = None,
        *,
        id_prefix: str = "",
    ):
        super().__init__(
            input_map=input_map or {},
            output_map=output_map or {},
            max_iterations=max_iterations,
            id_prefix=id_prefix,
        )
        self._graph = graph
        self._input_map = input_map or {}
        self._output_map = output_map or {}
        self._max_iterations = max_iterations
        self._id_prefix = id_prefix
        if id_prefix:
            self._graph = self._prefix_graph(graph, id_prefix)

    async def execute(self, ctx, state: dict) -> dict:
        reducers = getattr(ctx, "reducers", None)
        if self._input_map:
            sub_state = {}
            for parent_key, sub_key in self._input_map.items():
                sub_state[sub_key] = copy.deepcopy(state.get(parent_key))
        else:
            sub_state = copy.deepcopy(state)
        input_snapshot = copy.deepcopy(sub_state)

        checkpointer = getattr(ctx, "checkpointer", None)
        checkpoint_id = getattr(ctx, "checkpoint_id", None)
        nested_checkpoint_id = None
        if checkpointer is not None and checkpoint_id:
            nested_checkpoint_id = f"{checkpoint_id}:sub:{ctx.node_id or 'subflow'}"

        run_kwargs: dict = dict(
            tools=list(ctx.tools.values()),
            reducers=getattr(ctx, "reducers", None),
            hooks=getattr(ctx, "hooks", None),
            node_timeout=getattr(ctx, "node_timeout", None),
            max_iterations=self._max_iterations,
            emit=self._forward(ctx.emit),
            providers=getattr(ctx, "providers", None),
            default_provider=getattr(ctx, "default_provider", None),
            default_model=getattr(ctx, "default_model", None),
            on_llm_payload=getattr(ctx, "on_llm_payload", None),
            checkpointer=checkpointer,
            checkpoint_id=nested_checkpoint_id,
            owner=getattr(ctx, "owner", None),
            resume=getattr(ctx, "resume", None),
        )

        try:
            result = await self._graph.run(sub_state, **run_kwargs)
        except GraphInterrupt as exc:
            exc.node_id = ctx.node_id
            exc.nested_checkpoint_id = nested_checkpoint_id
            raise

        if nested_checkpoint_id is not None and checkpointer is not None:
            await checkpointer.delete(nested_checkpoint_id, owner=ctx.owner)

        out = {}
        if self._output_map:
            for sub_key, parent_key in self._output_map.items():
                out[parent_key] = result.get(sub_key)
        else:
            out = self._passthrough_delta(input_snapshot, result, reducers)
        return out

    @staticmethod
    def _passthrough_delta(
        input_state: dict, result: dict, reducers: "dict | None"
    ) -> dict:
        """Return only what the sub-graph changed, under the parent's reducers.

        In passthrough mode the parent merges the returned value through its
        own reducers, but the nested run already applied them inside
        ``sub_state`` — so returning the whole accumulated state would apply
        an ``append`` reducer twice.  To keep the parent's single merge
        correct we hand back a *delta*:

        * append-style keys (`reducer_appends`)  → the newly appended items
          gathered inside the sub-graph (`result[key][len(input):]`), so the
          parent appends them exactly once;
        * override keys → the new value (only when it actually changed), so
          untouched keys are not clobbered on the way out.
        """
        out: dict = {}
        for key, value in result.items():
            old = input_state.get(key)
            if reducer_appends((reducers or {}).get(key)):
                nv = value
                if isinstance(old, list) and isinstance(value, list):
                    prefix = value[: len(old)]
                    if prefix == old:
                        nv = value[len(old) :]
                if nv:
                    out[key] = nv
            elif old != value:
                out[key] = value
        return out

    @staticmethod
    def _prefix_graph(graph: Graph, prefix: str) -> Graph:
        """Rename every node in *graph* to ``prefix/<original>``."""
        nodes = {f"{prefix}/{nid}": node for nid, node in graph.nodes.items()}
        edges = [
            Edge(
                source_id=f"{prefix}/{e.source_id}",
                target_id=f"{prefix}/{e.target_id}",
                condition=e.condition,
            )
            for e in graph.edges
        ]
        return Graph(
            nodes=nodes,
            edges=edges,
            entry_point=f"{prefix}/{graph.entry_point}",
            providers=graph.providers,
            default_provider=graph.default_provider,
            default_model=graph.default_model,
        )

    @staticmethod
    def _forward(
        emit: "Callable[[StreamEvent], Awaitable[None]] | None",
    ) -> "Callable[[StreamEvent], Awaitable[None]] | None":
        """Wrap an outer emit sink, dropping the nested run's bookkeeping.

        The inner run emits its own ``run_start``/``run_end`` lifecycle
        events; those belong to the top-level stream, so they are
        stripped while node/token/llm/edge events stream through.
        ``interrupt``/``interrupt_resume`` are also stripped: ``SubFlow``
        re-raises :class:`~teff.node.interrupt.GraphInterrupt` to the
        enclosing run, which emits those events itself (with the sub-flow's
        node id), so emitting them here too would duplicate them.
        """
        if emit is None:
            return None

        _STRIPPED = ("run_start", "run_end", "interrupt", "interrupt_resume")

        async def forward(event: StreamEvent) -> None:
            if event.type in _STRIPPED:
                return
            await emit(event)

        return forward

agent_step

agent_step(
    system,
    output_key,
    *,
    model,
    provider,
    sections=None,
    messages_key="messages",
    use_tools=None,
    stream=True,
    id=None,
    **config,
)

One routed agent: context builder → ReAct harness → append to conversation.

Builds a small Flow wrapped as a :class:~teff.flow.SubFlow::

ContextBuilder ──► ReAct harness ──► AppendAssistant
  • The context builder composes a plain-text input from the shared state sections (plus the latest user message) and resets the agent's scratch keys, so each run starts clean.
  • The harness runs the model against that input with use_tools, writing its final answer to output_key.
  • AppendAssistant copies that answer into the shared conversation.

The agent's scratch conversation lives in a private _<output_key>_messages state slot (reset by the context builder); only the final reply reaches messages_key.

Parameters:

Name Type Description Default
system str

System prompt for the agent.

required
output_key str

State key that receives the agent's final answer.

required
model str

LLM model name (e.g. llama3.1:8b).

required
provider str

Provider name (e.g. ollama). Must be declared in the providers of the enclosing flow/workflow — this sub-flow inherits its provider set from the parent at run time.

required
sections dict[str, str] | None

Shared state key → label mapping rendered into the agent's context. Defaults to {output_key: output_key.capitalize()}.

None
messages_key str

State key holding the shared conversation.

'messages'
use_tools str | list[str] | None

None/[] (no tools, default), "all" (everything the pool offers), or a list of tool names the agent may call. Prefer an explicit allowlist; the True/False bool shorthands are kept only for backwards compatibility.

None
stream bool

Emit tokens as stream events (live rendering).

True
**config

Extra kwargs for the ReAct harness / ToolExec.

{}

Returns:

Type Description
SubFlow

A SubFlow node usable with :meth:teff.flow.Flow.route /

SubFlow

meth:teff.flow.Flow.step.

Source code in teff/flow/agent.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def agent_step(
    system: str,
    output_key: str,
    *,
    model: str,
    provider: str,
    sections: dict[str, str] | None = None,
    messages_key: str = "messages",
    use_tools: str | list[str] | None = None,
    stream: bool = True,
    id: str | None = None,
    **config,
) -> SubFlow:
    """One routed agent: context builder → ReAct harness → append to conversation.

    Builds a small ``Flow`` wrapped as a :class:`~teff.flow.SubFlow`::

        ContextBuilder ──► ReAct harness ──► AppendAssistant

    * The context builder composes a plain-text ``input`` from the shared
      state sections (plus the latest user message) and resets the agent's
      scratch keys, so each run starts clean.
    * The harness runs the model against that ``input`` with *use_tools*,
      writing its final answer to *output_key*.
    * ``AppendAssistant`` copies that answer into the shared conversation.

    The agent's scratch conversation lives in a private ``_<output_key>_messages``
    state slot (reset by the context builder); only the final reply reaches
    *messages_key*.

    Args:
        system: System prompt for the agent.
        output_key: State key that receives the agent's final answer.
        model: LLM model name (e.g. ``llama3.1:8b``).
        provider: Provider name (e.g. ``ollama``).  Must be declared in the
            ``providers`` of the enclosing flow/workflow — this sub-flow
            inherits its provider set from the parent at run time.
        sections: Shared state key → label mapping rendered into the agent's
            context.  Defaults to ``{output_key: output_key.capitalize()}``.
        messages_key: State key holding the shared conversation.
        use_tools: ``None``/``[]`` (no tools, default), ``"all"`` (everything
            the pool offers), or a list of tool names the agent may call.
            Prefer an explicit allowlist; the ``True``/``False`` bool
            shorthands are kept only for backwards compatibility.
        stream: Emit tokens as stream events (live rendering).
        **config: Extra kwargs for the ReAct harness / ``ToolExec``.

    Returns:
        A ``SubFlow`` node usable with :meth:`teff.flow.Flow.route` /
        :meth:`teff.flow.Flow.step`.
    """
    scratch_key = f"_{output_key}_messages"
    inner = Flow(f"agent-{output_key}")
    inner.step(
        ContextBuilder(
            sections=sections or {output_key: output_key.capitalize()},
            reset_keys=(output_key, "input", scratch_key),
        )
    )
    inner.harness(
        model=model,
        system=system,
        input_key="input",
        output_key=output_key,
        messages_key=scratch_key,
        use_tools=use_tools,
        provider=provider,
        stream=stream,
        **config,
    )
    inner.step(AppendAssistant(output_key=output_key, messages_key=messages_key))
    return SubFlow(inner.compile(), id_prefix=id or "")