Skip to content
Back to skills

1705 Agent Loop Ontology 019d244a

ASecurity

**Status:** authoritative *as implemented in code* (not aspirational). **Scope:** This document describes the **current** request/iteration loop and the **node orchestrator** (tool dispatch + tool returns + usage/thought recording) so the system can be replaced cleanly. **Source of truth:** code only. Primary code locations: - Request loop (per user message): `src/tunacode/core/agents/main.py` - `RequestOrchestrator` (class) - `process_request(...)` (function) - Node orchestrator (per streame...

  • 9 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added June 1, 2026
ai-agentsnode

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned June 1, 2026

npx -y skills add tools-only/X-Skills --skill 1705-agent-loop-ontology_019d244a --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 1705 Agent Loop Ontology 019d244a?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 1705 Agent Loop Ontology 019d244a
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-1705-agent-loop-ontology-019d244a/badge)](https://www.skillsdirectory.com/skills/tools-only-1705-agent-loop-ontology-019d244a)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# TunaCode Agent Loop Ontology (Single Source of Truth)

**Status:** authoritative *as implemented in code* (not aspirational).

**Scope:** This document describes the **current** request/iteration loop and the **node orchestrator** (tool dispatch + tool returns + usage/thought recording) so the system can be replaced cleanly.

**Source of truth:** code only.

Primary code locations:

- Request loop (per user message): `src/tunacode/core/agents/main.py`
  - `RequestOrchestrator` (class)
  - `process_request(...)` (function)
- Node orchestrator (per streamed node): `src/tunacode/core/agents/agent_components/orchestrator/orchestrator.py`
  - `process_node(...)` (function)
- Tool dispatch pipeline:
  - `dispatch_tools(...)`: `src/tunacode/core/agents/agent_components/orchestrator/tool_dispatcher.py`
  - collection/normalization: `.../orchestrator/_tool_dispatcher_collection.py`, `.../_tool_dispatcher_registry.py`
  - execution + retries: `.../orchestrator/_tool_dispatcher_execution.py`, `src/tunacode/core/agents/agent_components/tool_executor.py`
- Tool return handling: `emit_tool_returns(...)`: `.../orchestrator/tool_returns.py`
- Streaming instrumentation: `stream_model_request_node(...)`: `src/tunacode/core/agents/agent_components/streaming.py`
- State contracts:
  - `StateManagerProtocol`: `src/tunacode/core/types/state.py`
  - runtime structures: `src/tunacode/core/types/state_structures.py`
  - tool lifecycle registry: `src/tunacode/core/types/tool_registry.py`

---

## 0) Terminology (Ontology)

### Concepts (nouns)

**Request**
- A single user message processed by `process_request(...)`.

**Run handle / AgentRun**
- The object returned by `pydantic_ai.Agent.iter(...)` context manager.
- Treated as `Any` via `tunacode.infrastructure.llm_types.AgentRun = Any`.
- Must support at least:
  - async iteration yielding **Nodes**
  - `.ctx` (used for streaming)
  - `.all_messages()` (used for persistence)

**Node**
- One element yielded by `async for node in run_handle:`.
- Structural contract is defined by *attributes accessed by TunaCode* (see §4).

**Session**
- The mutable runtime state behind `StateManagerProtocol.session`.

**Tool registry**
- `session.runtime.tool_registry: ToolCallRegistry`
- The single source of truth for tool call lifecycle tracking.

**ResponseState**
- `tunacode.core.agents.agent_components.response_state.ResponseState`
- A state machine wrapper used for UI/flow tracking (USER_INPUT → ASSISTANT → TOOL_EXECUTION → RESPONSE).

### Processes (verbs)

**Request loop**
- The outer orchestration of an agent run for a single user request.

**Node orchestration**
- The per-node processing step that:
  - consumes tool returns from the node request
  - records thought
  - updates usage
  - dispatches tool calls
  - transitions response state

---

## 1) Public Replacement Surface (what you must preserve)

If you want to replace the agent loop implementation/framework, you must preserve the observable behavior of these public surfaces:

### 1.1 `process_request(...)`
**Location:** `src/tunacode/core/agents/main.py`

```py
async def process_request(
    message: str,
    model: ModelName,
    state_manager: StateManagerProtocol,
    tool_callback: ToolCallback | None = None,
    streaming_callback: StreamingCallback | None = None,
    tool_result_callback: ToolResultCallback | None = None,
    tool_start_callback: ToolStartCallback | None = None,
    notice_callback: NoticeCallback | None = None,
) -> AgentRun:
    ...
```

#### Input contract
- `message: str`
  - user’s prompt for this request.
- `model: ModelName` (`ModelName = str`)
  - model selector string consumed by `get_or_create_agent(...)` (see §2.2).
- `state_manager: StateManagerProtocol`
  - must provide `.session` containing at least the fields referenced in §3.
- callbacks (optional)
  - `tool_callback`: invoked for each tool call batch item (see §5.3)
  - `streaming_callback`: invoked with streaming text deltas (see §5.1)
  - `tool_start_callback`: called once per dispatched batch (best-effort UI signal)
  - `tool_result_callback`: called once per tool return observed in the next request
  - `notice_callback`: called when empty-response intervention triggers

#### Output contract
- Returns an `AgentRun` (framework object) **wrapped** with `.response_state`:
  - actual returned type is `tunacode.core.agents.agent_components.result_wrapper.AgentRunWithState`
  - it delegates all attributes to the underlying run handle and adds:

```py
run.response_state  # ResponseState
```

#### Side effects (authoritative)
- Mutates `state_manager.session.*` (details in §3)
- Updates `session.conversation.messages` to the run’s authoritative messages at end of run
- Updates `session.conversation.thoughts`
- Updates `session.usage.last_call_usage` and accumulates totals
- Updates `session.runtime.tool_registry` during tool call/return lifecycle

#### Exceptions
- May raise (non-exhaustive):
  - `GlobalRequestTimeoutError` (if global timeout enabled)
  - `UserAbortError` (propagated)
  - `asyncio.CancelledError` (propagated)
  - framework/network/tool exceptions not caught by outer loop

### 1.2 Node orchestration entry point: `process_node(...)`
**Location:** `src/tunacode/core/agents/agent_components/orchestrator/orchestrator.py`

```py
async def process_node(
    node: Any,
    tool_callback: ToolCallback | None,
    state_manager: StateManagerProtocol,
    _streaming_callback: StreamingCallback | None = None,
    response_state: ResponseState | None = None,
    tool_result_callback: ToolResultCallback | None = None,
    tool_start_callback: ToolStartCallback | None = None,
) -> tuple[bool, str | None]:
    ...
```

#### Output contract
- Returns `(is_empty: bool, reason: str | None)`.
  - `is_empty=True` means the model response had **no non-empty text** and **no structured tool calls**.
  - `reason` is currently either `"empty"` or `None`.

---

## 2) Execution Model (what happens)

### 2.1 UI → core loop
**Typical caller:** `src/tunacode/ui/app.py` calls `process_request(...)` inside an `asyncio.Task`.

The agent loop is designed so UI can:
- render tool start events (`tool_start_callback`)
- render tool results as they come back (`tool_result_callback`)
- (optionally) stream deltas (`streaming_callback`)—currently passed as `None` by the Textual UI.

### 2.2 Agent creation + caching
**Location:** `src/tunacode/core/agents/agent_components/agent_config.py`

- `get_or_create_agent(model, state_manager)` is the only constructor path.
- Caches agents in:
  - session cache: `session.agents[model]` and `session.agent_versions[model]`
  - module cache: `tunacode.infrastructure.cache.caches.agents`
- Agent version hash is computed from:
  - `settings.max_retries`
  - `settings.tool_strict_validation`
  - request delay
  - `settings.global_request_timeout`

**System prompt composition (as implemented):**
1) load `src/tunacode/prompts/system_prompt.md`
2) append a context file from CWD (default: `AGENTS.md`) via `load_tunacode_context()`

### 2.3 Message history preparation
**Location:** `src/tunacode/core/agents/history_preparer.py`

Before calling into the framework, the loop:
- prunes old tool outputs for token budget (`prune_old_tool_outputs`)
- runs cleanup loop to remove inconsistencies (`run_cleanup_loop`)
- drops a trailing "request" message to avoid consecutive requests
- sanitizes history for resume (`sanitize_history_for_resume`)

Output:
- `message_history: list[Any]` passed to `agent.iter(..., message_history=...)`
- `baseline_message_count: int` used later to preserve messages externally appended during run

### 2.4 Outer request loop (per request)
**Location:** `src/tunacode/core/agents/main.py`

Control flow:

1) `RequestOrchestrator.run()`
   - applies global timeout (if enabled via session config)
2) `_run_impl()`
   - initializes request context + resets per-run fields
   - gets or creates an agent
   - prepares history
   - enters framework iteration context: `async with agent.iter(...) as run_handle:`
3) `_run_agent_iterations(...)`
   - iterates nodes with `async for node in run_handle:`
   - for each node calls `_handle_iteration_node(...)`
4) on completion:
   - persists run messages into `session.conversation.messages`

**Important: `max_iterations` is currently NOT enforced.**
- `RequestContext.max_iterations` is computed and stored but never checked in the loop.

### 2.5 Per-node iteration
**Location:** `src/tunacode/core/agents/main.py` (`_handle_iteration_node`)

Per node:
1) update runtime counters:
   - `runtime.current_iteration = iteration_index`
   - `runtime.iteration_count = iteration_index`
2) optional streaming:
   - if `streaming_callback` and `Agent.is_model_request_node(node)` then
     `stream_model_request_node(...)`
3) node orchestration:
   - calls `process_node(...)`
4) empty response handling:
   - if empty, triggers `handle_empty_response(...)` and `notice_callback(...)`
5) completion:
   - stops early only if `response_state.task_completed` is set `True`

---

## 3) State & Side-Effects Contract (StateManager / Session)

This section is essential for replacement: **the loop is not pure**; it encodes state mutations that callers (UI) rely on.

### 3.1 Fields reset per request
**Location:** `RequestOrchestrator._reset_session_state()`

Mutations:
- `runtime.current_iteration = 0`
- `runtime.iteration_count = 0`
- `runtime.tool_registry.clear()`
- `runtime.batch_counter = 0`
- `runtime.consecutive_empty_responses = 0`
- `session.task.original_query = ""` then immediately set to `message` (see below)

### 3.2 Request identity
- `runtime.request_id` is set to the first 8 chars of a UUID4.

### 3.3 Original query
- Intended to be set once, but as implemented it is effectively set every request:
  - `_reset_session_state()` clears it
  - `_set_original_query_once()` then sets it to the request message

### 3.4 Conversation persistence rules
**Location:** `RequestOrchestrator._persist_run_messages(...)`

At run end:
- `run_messages = list(run_handle.all_messages())`
- `external_messages = conversation.messages[baseline_message_count:]`
- `conversation.messages = run_messages + external_messages`

Meaning:
- the run handle is the *authoritative* source for messages during the run
- any messages appended externally during the run after baseline are preserved

### 3.5 Tool registry lifecycle
**Location:** `core/types/tool_registry.py` + tool dispatcher + tool returns

Observed lifecycle:
1) tool call discovered → `tool_registry.register(tool_call_id, tool_name, args)`
2) about to execute → `tool_registry.start(tool_call_id)`
3) later tool return observed in a subsequent model request → `tool_registry.complete(tool_call_id, result)`
4) on execution failure → `tool_registry.fail(tool_call_id, error)`
5) on user abort during execution → `tool_registry.cancel(tool_call_id, reason)`

Invariant:
- Tool return consumption relies on the args being registered by tool_call_id.
  - tool returns call `consume_tool_call_args(...)`
  - it raises if args are missing.

### 3.6 Usage tracking
**Location:** `.../orchestrator/usage_tracker.py`

- On each node with `model_response`, usage is normalized and:
  - `session.usage.last_call_usage = UsageMetrics(...)`
  - `session.usage.session_total_usage.add(last_call_usage)`

### 3.7 Streaming debug state
**Location:** `.../agent_components/streaming.py`

On stream start:
- `session._debug_raw_stream_accum` reset to `""`
- `session._debug_events` reset to `[]`

Streaming callback is invoked with text deltas; deltas are appended to `_debug_raw_stream_accum`.

---

## 4) Structural Contracts (framework objects treated as `Any`)

Because `pydantic_ai` types are mostly typed as `Any` in `tunacode.infrastructure.llm_types`, TunaCode relies on **structural typing**.

### 4.1 Node: minimum attributes used by TunaCode
A node is any object with some/all of:

- `node.request: Any | None`
  - when present, `request.parts` is iterated for tool-return parts
- `node.model_response: Any | None`
  - when present, `model_response.parts: list[Any]` is read
  - `model_response.usage` is passed to usage normalization
- `node.thought: str | None`
  - when present, appended to `session.conversation.thoughts`
- `node.result: Any | None`
  - when present, `node.result.output` is read to detect user-visible output

Optional streaming capability:
- `node.stream(agent_run_ctx)` must be an async context manager that yields an async iterator of events.

### 4.2 Message parts: minimum attributes used
For response parts (`model_response.parts`) and request parts (`request.parts`):

- `part.part_kind: str`
  - tool calls: `"tool-call"`
  - tool returns: `"tool-return"`
  - text: `"text"`
- `part.content: Any` (text/tool return content)
- Tool call specific:
  - `part.tool_call_id: str | None`
  - `part.tool_name: str | None`
  - `part.args: str | dict | None`

---

## 5) Callback Contracts (inputs/outputs/failure behavior)

### 5.1 `StreamingCallback`
**Type:** `Callable[[str], Awaitable[None]]`

Contract:
- Called with ordered text deltas.
- If it raises, the streaming path raises and the request fails.

### 5.2 `ToolResultCallback`
**Type:** `Callable[[ToolName, str, ToolArgs, str | None, float | None], None]`

Called when:
- tool-return parts are observed in `node.request.parts`.

Arguments:
- `tool_name`: tool name from tool-return part (string)
- `status`: always `"completed"` in current emitter
- `args`: resolved from tool registry by `tool_call_id`
- `result`: stringified tool return content
- `duration_ms`: always `None` in current emitter

Failure behavior:
- Exceptions propagate (no try/except around callback).

### 5.3 `ToolCallback` (tool execution hook)
**Type:** `Callable[[ToolCallPartProtocol, StreamResultProtocol], Awaitable[None]]`

Where called:
- `dispatch_tools(...)` collects tool call parts and then executes them through `execute_tools_parallel(...)`, which calls the callback once per tool call.

Semantics:
- The callback is the “executor” for the tool call part in the context of the current node.
- If it raises:
  - retry logic may retry unless the exception is non-retryable (see `NON_RETRYABLE_ERRORS` in `tool_executor.py`).
  - ultimate failure is recorded in the tool registry (`fail(...)` or `cancel(...)`).

### 5.4 `ToolStartCallback`
**Type:** `Callable[[str], None]`

Called when:
- a batch of tool calls is about to execute.

Argument:
- a display string of up to N tool names, with a suffix if truncated.

### 5.5 `NoticeCallback`
**Type:** `Callable[[str], None]`

Called when:
- an iteration response is detected as empty.

Argument:
- a generated user-facing notice string.

---

## 6) Abort / Timeout / Cleanup Semantics

### 6.1 Global request timeout
**Location:** `RequestOrchestrator.run()`

- timeout is computed via `_coerce_global_request_timeout(session)`.
- when triggered:
  - agent cache is invalidated (`invalidate_agent_cache(model, state_manager)`)
  - raises `GlobalRequestTimeoutError(timeout)`

### 6.2 Abort / cancellation cleanup
**Location:** `RequestOrchestrator._handle_abort_cleanup()`

If `UserAbortError` or `asyncio.CancelledError` bubbles up:

1) If `session._debug_raw_stream_accum` has partial streamed text, append a synthetic interrupted `ModelResponse` to conversation history.
2) Sanitize message history:
   - remove dangling tool calls
   - remove empty responses
   - remove consecutive requests
3) Invalidate the agent cache for the model.

---

## 7) Replaceability Checklist (what a new engine must provide)

To replace the current implementation cleanly, implement an adapter that provides the same *structural contracts* and side effects:

### 7.1 Engine must support
- async context manager “run handle”
- async iteration over “nodes”
- per node:
  - tool return parts reachable at `node.request.parts`
  - response parts reachable at `node.model_response.parts`
  - optional thought string
  - optional final output at `node.result.output`
- run handle must provide `all_messages()` to persist history

### 7.2 Tool loop integration must preserve
- tool call discovery (structured + fallback from text)
- tool registry registration/args normalization keyed by `tool_call_id`
- tool return consumption using `tool_call_id` → args mapping

### 7.3 State updates must preserve
- session runtime counters and request_id
- conversation messages persistence strategy (authoritative run messages + preserve external appends)
- usage metrics updates

---

## 8) Index of Relevant Files (for code navigation)

- `src/tunacode/core/agents/main.py`
  - `RequestOrchestrator`
  - `EmptyResponseHandler`
  - `_handle_iteration_node`
- `src/tunacode/core/agents/history_preparer.py`
- `src/tunacode/core/agents/agent_components/agent_config.py`
- `src/tunacode/core/agents/agent_components/streaming.py`
- `src/tunacode/core/agents/agent_components/orchestrator/orchestrator.py`
- `src/tunacode/core/agents/agent_components/orchestrator/tool_dispatcher.py`
- `src/tunacode/core/agents/agent_components/orchestrator/tool_returns.py`
- `src/tunacode/core/agents/agent_components/tool_executor.py`
- `src/tunacode/core/types/state.py`
- `src/tunacode/core/types/state_structures.py`
- `src/tunacode/core/types/tool_registry.py`

Files in this skill

  • README.md846 B
  • skill.md16.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.