PtcRunner.SubAgent.Loop (PtcRunner v0.4.1)
View SourceCore agentic loop that manages LLM↔tool cycles.
The loop repeatedly calls the LLM, parses PTC-Lisp from the response,
executes it, and continues until return/fail is called or max_turns is exceeded.
Flow
- Build LLM input with system prompt, messages, and tool names
- Call LLM to get response (resolving atoms via
llm_registryif needed) - Parse PTC-Lisp code from response (code blocks or raw s-expressions)
- Execute code via
Lisp.run/2 - Check for return/fail or continue to next turn
- Build trace entry and update message history
- Merge execution results into context for next turn
Termination Conditions
The loop terminates when any of these occur:
| Condition | Result | Reason |
|---|---|---|
(return value) called | {:ok, step} | Normal completion |
(fail error) called | {:error, step} | Explicit failure |
max_turns exceeded | {:error, step} | :max_turns_exceeded |
max_depth exceeded | {:error, step} | :max_depth_exceeded |
turn_budget exhausted | {:error, step} | :turn_budget_exhausted |
mission_timeout exceeded | {:error, step} | :mission_timeout |
| LLM error after retries | {:error, step} | :llm_error |
Memory Handling
Memory persists across turns within a single run/2 call. After each successful
Lisp execution:
Lisp.run/2applies the memory contract (seePtcRunner.Lispfor details)step.memorycontains the updated memory state- Loop updates
state.memoryfor the next turn - Memory is merged into context via
state.context
The memory contract determines how return values affect memory:
- Non-map returns: no memory update
- Map without
:return: merged into memory - Map with
:return: rest merged,:returnvalue returned
See PtcRunner.Lisp.run/2 for the authoritative memory contract documentation.
LLM Inheritance
Child SubAgents inherit the llm_registry from their parent, enabling atom-based
LLM references (like :haiku or :sonnet) to work throughout the agent hierarchy.
The registry only needs to be provided once at the top-level SubAgent.run/2 call.
Resolution order for LLM selection:
agent.llm- Set in SubAgent structas_tool(..., llm:)- Bound at tool creation- Parent's LLM - Inherited from calling agent
- Required at top level
This is an internal module called by SubAgent.run/2.
Summary
Functions
Execute a SubAgent in loop mode (multi-turn with tools).
Functions
@spec run( PtcRunner.SubAgent.t(), keyword() ) :: {:ok, PtcRunner.Step.t()} | {:error, PtcRunner.Step.t()}
Execute a SubAgent in loop mode (multi-turn with tools).
Parameters
agent- A%SubAgent{}structopts- Keyword list with:llm- Required. LLM callback functioncontext- Initial context map (default: %{})debug- Enable debug mode (default: false). When enabled, trace entries store exact message contents:llm_response- The assistant message (LLM output, stored as-is)llm_feedback- The user message (execution feedback, after truncation) UseSubAgent.Debug.print_trace(step, messages: true)to view the conversation.
trace- Trace filtering: true (always), false (never), :on_error (only on failure) (default: true)llm_retry- Optional retry configuration map with:max_attempts- Maximum number of retry attempts (default: 1, meaning no retries unless explicitly configured)backoff- Backoff strategy: :exponential, :linear, or :constant (default: :exponential)base_delay- Base delay in milliseconds (default: 1000)retryable_errors- List of error types to retry (default: [:rate_limit, :timeout, :server_error])
Returns
{:ok, Step.t()}on success (whenreturnis called){:error, Step.t()}on failure (whenfailis called or max_turns exceeded)
Examples
iex> agent = PtcRunner.SubAgent.new(prompt: "Add {{x}} and {{y}}", tools: %{}, max_turns: 2)
iex> llm = fn %{messages: _} -> {:ok, "```clojure\n(return {:result (+ ctx/x ctx/y)})\n```"} end
iex> {:ok, step} = PtcRunner.SubAgent.Loop.run(agent, llm: llm, context: %{x: 5, y: 3})
iex> step.return
%{result: 8}