Skip to content

teff.flow.harness

teff.flow.harness

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

:class:HarnessBuilder builds the ReAct agent loop (LLM ↔ tools) used by Flow.harness / Flow.react. :class:Flow owns an instance and delegates to it.

Classes:

Name Description
HarnessBuilder

Build a ReAct agent loop on a :class:~teff.flow.Flow.

HarnessBuilder

Build a ReAct agent loop on a :class:~teff.flow.Flow.

Parameters:

Name Type Description Default
flow Flow

The owning Flow whose graph state is mutated.

required

Methods:

Name Description
harness

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

react

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

Source code in teff/flow/harness.py
 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
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
class HarnessBuilder:
    """Build a ReAct agent loop on a :class:`~teff.flow.Flow`.

    Args:
        flow: The owning ``Flow`` whose graph state is mutated.
    """

    def __init__(self, flow: "Flow"):
        self.flow = flow

    def harness(
        self,
        model=None,
        system: str = "",
        *,
        agent=None,
        input_key: str = "input",
        output_key: str = "output",
        messages_key: str = "messages",
        memory=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=None,
        skills: list | None = None,
        skill_dir: str = "skills",
        id: str | None = None,
        **config,
    ):
        """Build a ReAct-style agent loop (LLM ↔ tools) inside this flow.

        Creates an agent node and a tool executor wired in a cycle.  The
        agent calls the LLM; if the LLM requests tools, they are signalled
        to the executor, which runs them **all in parallel** and loops back
        to the agent.  When the LLM answers without a tool call, the output
        is stored at *output_key* and execution continues after the loop
        (any node chained with ``step()``/``branch()`` after this call).

        Multiple tools can be requested in a single round (e.g. read from
        RAG *and* compute at once); the executor fans them out with
        ``asyncio.gather``.

        *id* names the two nodes created by this helper as ``{id}/agent``
        and ``{id}/tool``; when omitted they keep the auto-generated
        ``{type}_{n}`` ids.

        The agent node is a :class:`~teff.node.agent.ReActAgent`.  Pass a
        pre-built instance or a subclass to override its behaviour::

            flow.react(agent=MyAgent(model="gpt-4", system="..."))
            flow.react(agent=MyAgentClass, model="gpt-4", system="...")

        With an instance, *model*/*system* and the other agent knobs are
        ignored (the instance is used as-is); with a subclass they are
        forwarded to its constructor.  When *agent* is omitted the
        ``ReActAgent`` class is used and *model* is required.

        Args:
            model: LLM model name (e.g. ``gpt-4``).  Required unless
                *agent* is given.
            system: Optional system prompt.
            agent: A ``ReActAgent`` instance or subclass to use instead of
                building the default one.
            input_key: State key for user input (default ``"input"``).
            output_key: State key for final response (default ``"output"``).
            messages_key: State key for conversation (default ``"messages"``).
            memory: Long-term memory injection — a
                :class:`~teff.memory.context.MemoryConfig` or config dict.
            max_tool_rounds: Max model calls per graph visit.
            tool_error_mode: ``"message"`` (default) or ``"raise"`` — when
                ``"raise"`` a tool failure routes to the graph's error path.
            parse_text_tool_calls: Decode tool calls embedded in plain text.
            temperature / max_tokens / response_format: Sampling knobs.
            use_tools: ``None``/``[]`` (no tools, default), ``"all"`` (everything
                the pool offers), or a list of tool names to allow.  The
                bool shorthands ``True``/``False`` are supported for
                compatibility but an explicit list is preferred.
            skills: Skills to mount on the agent — names resolved against
                *skill_dir*, skill paths, or :class:`~teff.skill.Skill`
                objects.  Their instructions go into the system prompt and
                their ``allowed-tools``/``disallowed-tools`` narrow the
                agent's tool set.
            skill_dir: Directory to resolve bare skill names from
                (default ``"skills"``).
            **config: Extra kwargs passed to :class:`ReActAgent` /
                :class:`ToolExec` config.

        Remember to pass ``max_iterations`` to ``graph.run()``::

            result = await graph.run(state, tools=tools, max_iterations=20)
        """
        from teff.graph import Edge
        from teff.node.agent import ReActAgent, ToolExec

        target = self.flow
        target._check_continuation()

        agent_cfg = {
            "model": model,
            "system": system,
            "input_key": input_key,
            "output_key": output_key,
            "messages_key": messages_key,
            "max_tool_rounds": max_tool_rounds,
            "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,
            "memory": memory,
            **config,
        }
        if agent is None:
            if model is None and target._default_model is None:
                raise TypeError(
                    "harness() requires a model (or a default_model on the "
                    "flow) when no agent instance is given"
                )
            agent_node: ReActAgent = ReActAgent(**agent_cfg)
        elif isinstance(agent, type):
            if not issubclass(agent, ReActAgent):
                raise TypeError(
                    "harness() agent must be a ReActAgent instance or subclass"
                )
            agent_node = agent(**agent_cfg)
        else:
            if not isinstance(agent, ReActAgent):
                raise TypeError(
                    "harness() agent must be a ReActAgent instance or subclass"
                )
            agent_node = agent
        tool_exec = ToolExec(
            messages_key=messages_key,
            tool_call_key=str(
                agent_node.config.get("tool_call_key") or "_tool_call_name"
            ),
            tool_error_mode=tool_error_mode,
            use_tools=use_tools,
            skills=skills,
            skill_dir=skill_dir,
            **config,
        )

        target._nodes.append(agent_node)
        agent_id = target._next_id(agent_node, f"{id}/agent" if id else None)
        target._node_ids.append(agent_id)

        target._nodes.append(tool_exec)
        tool_id = target._next_id(tool_exec, f"{id}/tool" if id else None)
        target._node_ids.append(tool_id)

        target._edges.append(
            Edge(
                agent_id,
                tool_id,
                f"{agent_node.config.get('tool_call_key', '_tool_call_name')}!=",
            )
        )
        target._edges.append(Edge(tool_id, agent_id))

        if target._last_added is not None:
            target._edges.append(Edge(target._last_added, agent_id))

        target._last_added = agent_id
        target._branch_ends = []
        target._guarded_step = None
        return target

    def react(
        self,
        model=None,
        system: str = "",
        *,
        agent=None,
        input_key: str = "input",
        output_key: str = "output",
        messages_key: str = "messages",
        memory=None,
        **config,
    ):
        """Alias for :meth:`harness` (ReAct agent loop)."""
        return self.harness(
            model,
            system,
            agent=agent,
            input_key=input_key,
            output_key=output_key,
            messages_key=messages_key,
            memory=memory,
            **config,
        )

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.

Creates an agent node and a tool executor wired in a cycle. The agent calls the LLM; if the LLM requests tools, they are signalled to the executor, which runs them all in parallel and loops back to the agent. When the LLM answers without a tool call, the output is stored at output_key and execution continues after the loop (any node chained with step()/branch() after this call).

Multiple tools can be requested in a single round (e.g. read from RAG and compute at once); the executor fans them out with asyncio.gather.

id names the two nodes created by this helper as {id}/agent and {id}/tool; when omitted they keep the auto-generated {type}_{n} ids.

The agent node is a :class:~teff.node.agent.ReActAgent. Pass a pre-built instance or a subclass to override its behaviour::

flow.react(agent=MyAgent(model="gpt-4", system="..."))
flow.react(agent=MyAgentClass, model="gpt-4", system="...")

With an instance, model/system and the other agent knobs are ignored (the instance is used as-is); with a subclass they are forwarded to its constructor. When agent is omitted the ReActAgent class is used and model is required.

Parameters:

Name Type Description Default
model

LLM model name (e.g. gpt-4). Required unless agent is given.

None
system str

Optional system prompt.

''
agent

A ReActAgent instance or subclass to use instead of building the default one.

None
input_key str

State key for user input (default "input").

'input'
output_key str

State key for final response (default "output").

'output'
messages_key str

State key for conversation (default "messages").

'messages'
memory

Long-term memory injection — a :class:~teff.memory.context.MemoryConfig or config dict.

None
max_tool_rounds int

Max model calls per graph visit.

10
tool_error_mode str

"message" (default) or "raise" — when "raise" a tool failure routes to the graph's error path.

'message'
parse_text_tool_calls bool

Decode tool calls embedded in plain text.

True
temperature / max_tokens / response_format

Sampling knobs.

required
use_tools

None/[] (no tools, default), "all" (everything the pool offers), or a list of tool names to allow. The bool shorthands True/False are supported for compatibility but an explicit list is preferred.

None
skills list | None

Skills to mount on the agent — names resolved against skill_dir, skill paths, or :class:~teff.skill.Skill objects. Their instructions go into the system prompt and their allowed-tools/disallowed-tools narrow the agent's tool set.

None
skill_dir str

Directory to resolve bare skill names from (default "skills").

'skills'
**config

Extra kwargs passed to :class:ReActAgent / :class:ToolExec config.

{}

Remember to pass max_iterations to graph.run()::

result = await graph.run(state, tools=tools, max_iterations=20)
Source code in teff/flow/harness.py
 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
179
180
181
182
183
184
185
def harness(
    self,
    model=None,
    system: str = "",
    *,
    agent=None,
    input_key: str = "input",
    output_key: str = "output",
    messages_key: str = "messages",
    memory=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=None,
    skills: list | None = None,
    skill_dir: str = "skills",
    id: str | None = None,
    **config,
):
    """Build a ReAct-style agent loop (LLM ↔ tools) inside this flow.

    Creates an agent node and a tool executor wired in a cycle.  The
    agent calls the LLM; if the LLM requests tools, they are signalled
    to the executor, which runs them **all in parallel** and loops back
    to the agent.  When the LLM answers without a tool call, the output
    is stored at *output_key* and execution continues after the loop
    (any node chained with ``step()``/``branch()`` after this call).

    Multiple tools can be requested in a single round (e.g. read from
    RAG *and* compute at once); the executor fans them out with
    ``asyncio.gather``.

    *id* names the two nodes created by this helper as ``{id}/agent``
    and ``{id}/tool``; when omitted they keep the auto-generated
    ``{type}_{n}`` ids.

    The agent node is a :class:`~teff.node.agent.ReActAgent`.  Pass a
    pre-built instance or a subclass to override its behaviour::

        flow.react(agent=MyAgent(model="gpt-4", system="..."))
        flow.react(agent=MyAgentClass, model="gpt-4", system="...")

    With an instance, *model*/*system* and the other agent knobs are
    ignored (the instance is used as-is); with a subclass they are
    forwarded to its constructor.  When *agent* is omitted the
    ``ReActAgent`` class is used and *model* is required.

    Args:
        model: LLM model name (e.g. ``gpt-4``).  Required unless
            *agent* is given.
        system: Optional system prompt.
        agent: A ``ReActAgent`` instance or subclass to use instead of
            building the default one.
        input_key: State key for user input (default ``"input"``).
        output_key: State key for final response (default ``"output"``).
        messages_key: State key for conversation (default ``"messages"``).
        memory: Long-term memory injection — a
            :class:`~teff.memory.context.MemoryConfig` or config dict.
        max_tool_rounds: Max model calls per graph visit.
        tool_error_mode: ``"message"`` (default) or ``"raise"`` — when
            ``"raise"`` a tool failure routes to the graph's error path.
        parse_text_tool_calls: Decode tool calls embedded in plain text.
        temperature / max_tokens / response_format: Sampling knobs.
        use_tools: ``None``/``[]`` (no tools, default), ``"all"`` (everything
            the pool offers), or a list of tool names to allow.  The
            bool shorthands ``True``/``False`` are supported for
            compatibility but an explicit list is preferred.
        skills: Skills to mount on the agent — names resolved against
            *skill_dir*, skill paths, or :class:`~teff.skill.Skill`
            objects.  Their instructions go into the system prompt and
            their ``allowed-tools``/``disallowed-tools`` narrow the
            agent's tool set.
        skill_dir: Directory to resolve bare skill names from
            (default ``"skills"``).
        **config: Extra kwargs passed to :class:`ReActAgent` /
            :class:`ToolExec` config.

    Remember to pass ``max_iterations`` to ``graph.run()``::

        result = await graph.run(state, tools=tools, max_iterations=20)
    """
    from teff.graph import Edge
    from teff.node.agent import ReActAgent, ToolExec

    target = self.flow
    target._check_continuation()

    agent_cfg = {
        "model": model,
        "system": system,
        "input_key": input_key,
        "output_key": output_key,
        "messages_key": messages_key,
        "max_tool_rounds": max_tool_rounds,
        "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,
        "memory": memory,
        **config,
    }
    if agent is None:
        if model is None and target._default_model is None:
            raise TypeError(
                "harness() requires a model (or a default_model on the "
                "flow) when no agent instance is given"
            )
        agent_node: ReActAgent = ReActAgent(**agent_cfg)
    elif isinstance(agent, type):
        if not issubclass(agent, ReActAgent):
            raise TypeError(
                "harness() agent must be a ReActAgent instance or subclass"
            )
        agent_node = agent(**agent_cfg)
    else:
        if not isinstance(agent, ReActAgent):
            raise TypeError(
                "harness() agent must be a ReActAgent instance or subclass"
            )
        agent_node = agent
    tool_exec = ToolExec(
        messages_key=messages_key,
        tool_call_key=str(
            agent_node.config.get("tool_call_key") or "_tool_call_name"
        ),
        tool_error_mode=tool_error_mode,
        use_tools=use_tools,
        skills=skills,
        skill_dir=skill_dir,
        **config,
    )

    target._nodes.append(agent_node)
    agent_id = target._next_id(agent_node, f"{id}/agent" if id else None)
    target._node_ids.append(agent_id)

    target._nodes.append(tool_exec)
    tool_id = target._next_id(tool_exec, f"{id}/tool" if id else None)
    target._node_ids.append(tool_id)

    target._edges.append(
        Edge(
            agent_id,
            tool_id,
            f"{agent_node.config.get('tool_call_key', '_tool_call_name')}!=",
        )
    )
    target._edges.append(Edge(tool_id, agent_id))

    if target._last_added is not None:
        target._edges.append(Edge(target._last_added, agent_id))

    target._last_added = agent_id
    target._branch_ends = []
    target._guarded_step = None
    return target

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).

Source code in teff/flow/harness.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
def react(
    self,
    model=None,
    system: str = "",
    *,
    agent=None,
    input_key: str = "input",
    output_key: str = "output",
    messages_key: str = "messages",
    memory=None,
    **config,
):
    """Alias for :meth:`harness` (ReAct agent loop)."""
    return self.harness(
        model,
        system,
        agent=agent,
        input_key=input_key,
        output_key=output_key,
        messages_key=messages_key,
        memory=memory,
        **config,
    )