Diagrams — SDD-13: tau


Diagram 1 — tau's Three-Layer Architecture

flowchart TB
    subgraph TAU_AI["tau_ai — provider/model streaming layer"]
        PROV[ModelProvider<br/>OpenAI · Anthropic · OpenRouter<br/>HF · Google · Mistral · local]
        PEVT[Provider events:<br/>ResponseStart · TextDelta<br/>ThinkingDelta · ResponseEnd<br/>Error · Retry]
        PROV --> PEVT
    end

    subgraph TAU_AGENT["tau_agent — the portable brain (zero UI/CLI deps)"]
        H[AgentHarness<br/>298 LOC<br/>owns transcript + queues + listeners]
        L[run_agent_loop<br/>276 LOC<br/>pure stateless provider/tool loop]
        T[AgentTool<br/>78 LOC<br/>frozen dataclass + async executor]
        E[14 AgentEvents<br/>134 LOC<br/>the layer contract]
        S[Session JSONL<br/>append-only · parent_id branching<br/>CompactionEntry · LeafEntry]
        H -->|delegates| L
        L -->|emits| E
        L -->|calls| T
        L -->|appends to| S
    end

    subgraph TAU_CODING["tau_coding — the coding app"]
        CT[read · write · edit · bash<br/>glob · grep<br/>1,057 LOC of tools]
        TUI[TUI · Textual<br/>CLI · slash commands<br/>skills · on-disk sessions]
        CT --> TUI
    end

    PEVT -->|provider.stream_response| L
    E -->|AgentEvent stream<br/>the contract| TUI

    style TAU_AI fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style TAU_AGENT fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TAU_CODING fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style H fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style L fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style T fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style S fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style PROV fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style PEVT fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style CT fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style TUI fill:#1a1015,stroke:#9494a0,color:#e4e4e8

Reading: tau's load-bearing design decision is the boundary between tau_agent (the portable brain) and tau_coding (the coding app). The brain depends only on tau_ai and Pydantic — it has zero knowledge of Textual, Rich, local config paths, slash commands, or rendering. A frontend consumes the AgentEvent stream. This separation is what makes the brain reusable: tau_coding wraps it as a coding agent; tau_security wraps the same brain as a security agent. The numbers are the actual source line counts — the entire brain (harness + loop + tools + events) is ~960 lines; the coding tools alone are 1,057. Read the layers top-down: providers emit neutral events, the loop translates them into 14 agent events, the harness fans events to listeners and owns state, the app is one consumer of the stream.


Diagram 2 — The Agent Loop (Provider Stream → Events → Tool Execution → Events)

flowchart TB
    START([prompt called]) --> AST[AgentStartEvent]
    AST --> TURN[TurnStartEvent turn=N]
    TURN --> STREAM[async for provider_event in<br/>provider.stream_response]
    STREAM --> TRANSLATE{translate provider event}
    TRANSLATE -->|ResponseStart| MS[MessageStartEvent]
    TRANSLATE -->|TextDelta| MD[MessageDeltaEvent]
    TRANSLATE -->|ThinkingDelta| TD[ThinkingDeltaEvent]
    TRANSLATE -->|Retry| RE[RetryEvent]
    TRANSLATE -->|ResponseEnd| ME[MessageEndEvent<br/>append assistant msg]
    TRANSLATE -->|Error| ERR[ErrorEvent recoverable=false]

    ME --> CHECK{assistant has<br/>tool_calls?}
    CHECK -->|no| DRAIN1[drain steering queue]
    DRAIN1 --> Q1{queued msgs?}
    Q1 -->|yes| TURN
    Q1 -->|no| DRAIN2[drain follow_up queue]
    DRAIN2 --> Q2{queued msgs?}
    Q2 -->|yes| TURN
    Q2 -->|no| STOP

    CHECK -->|yes| EXEC[for each ToolCall:]
    EXEC --> TES[ToolExecutionStartEvent<br/>carries ToolCall]
    TES --> CALL[await tool.execute arguments, signal]
    CALL --> RESULT{result}
    RESULT -->|exception caught| FAIL[AgentToolResult ok=false<br/>isolation boundary — loop never crashes]
    RESULT -->|success| OK[AgentToolResult ok=true]
    FAIL --> APPEND[append ToolResultMessage<br/>to transcript]
    OK --> APPEND
    APPEND --> TEE[ToolExecutionEndEvent<br/>carries AgentToolResult]
    TEE --> NEXT{more tool calls?}
    NEXT -->|yes| EXEC
    NEXT -->|no| TE[TurnEndEvent turn=N]
    TE --> DRAIN3[drain steering queue]
    DRAIN3 --> BUDGET{max_turns reached?}
    BUDGET -->|no| TURN
    BUDGET -->|yes| MAXERR[ErrorEvent recoverable=true<br/>max_turns budget hit]
    MAXERR --> STOP
    STOP([AgentEndEvent])

    style AST fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TURN fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style STREAM fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style MS fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style MD fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style ME fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TES fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TEE fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style CALL fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style FAIL fill:#1a1015,stroke:#9494a0,color:#9494a0
    style OK fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style ERR fill:#1a1015,stroke:#9494a0,color:#9494a0
    style MAXERR fill:#1a1015,stroke:#9494a0,color:#9494a0
    style STOP fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TE fill:#0d1b2a,stroke:#5eead4,color:#5eead4

Reading: run_agent_loop is a pure stateless async generator (276 lines). The caller (the harness) owns the transcript list; the loop appends assistant and tool-result messages to it. Each turn translates the provider's neutral event stream into the 14 typed agent events. The tool-execution path is the load-bearing part for security: every ToolCall yields a ToolExecutionStartEvent (carrying the requested name and arguments), then await tool.execute(arguments, signal), then a ToolExecutionEndEvent (carrying the AgentToolResult). Tool exceptions are caught at the loop boundary — the # noqa: BLE001 - tools are an isolation boundary comment is explicit: a failing tool returns ok=false and never crashes the loop. This is where scope enforcement lives in tau-security: the executor calls assert_in_scope before any work, and out-of-scope calls return a blocked result that rides the same ToolExecutionEndEvent path. The max_turns budget is the safety valve. The steering/follow-up queues drain at the turn boundary — that is the human-in-the-loop injection point.


Diagram 3 — The AgentTool Lifecycle (Frozen Dataclass → Schema → Executor → Result)

flowchart LR
    subgraph DEF["1. Definition (author writes a factory)"]
        FACT[create_port_scan_tool ctx<br/>closes over Scope + EvidenceChain]
        FACT --> BUILD[AgentTool name=port_scan<br/>description=...<br/>input_schema=host, ports<br/>executor=execute]
    end

    subgraph MODEL["2. The model sees"]
        SCHEMA[input_schema JSON:<br/>properties.host type string<br/>required host]
        DESC[description string:<br/>Scan ports on an in-scope host...]
    end

    subgraph EXEC["3. The loop calls (per ToolCall)"]
        TES[ToolExecutionStartEvent<br/>carries ToolCall: name + arguments]
        TES --> CALL[await tool.execute arguments, signal]
        CALL --> GATE{executor body}
        GATE --> S1[1. extract target from arguments]
        S1 --> S2[2. assert_in_scope scope, host, port_scan]
        S2 --> SCOPE{in scope?}
        SCOPE -->|no| BLK[return _blocked result<br/>ok=false · never executes nmap]
        SCOPE -->|yes, rate ok| S3[3. execute nmap subprocess]
        S3 --> S4[4. _capture_evidence → EvidenceChain]
        S4 --> S5[5. return _success result<br/>ok=true · evidence_id · finding_id]
    end

    subgraph RESULT["4. The loop receives"]
        BLK --> RES[AgentToolResult<br/>tool_call_id · name · ok<br/>content · data · error · details]
        S5 --> RES
        RES --> TEE[ToolExecutionEndEvent<br/>carries AgentToolResult]
        TEE --> APPEND[append ToolResultMessage<br/>to transcript]
    end

    BUILD --> SCHEMA
    BUILD --> DESC
    BUILD --> CALL

    style DEF fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style MODEL fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style EXEC fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style RESULT fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style FACT fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style BUILD fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style CALL fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style GATE fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style S2 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style SCOPE fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style BLK fill:#1a1015,stroke:#9494a0,color:#9494a0
    style S3 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style S4 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style RES fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TEE fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TES fill:#0d1b2a,stroke:#5eead4,color:#5eead4

Reading: AgentTool is a @dataclass(frozen=True, slots=True) — 78 lines total. The frozen property means the executor cannot be reassigned at runtime (a security property: scope enforcement, once wired in, cannot be swapped out). The lifecycle has four stages. (1) The author writes a factory function that closes over shared context (the Scope and EvidenceChain in tau-security) and returns an AgentTool. (2) The model sees only the description and input_schema — it does not see the executor. (3) The loop calls await tool.execute(arguments, signal) per ToolCall, bracketed by ToolExecutionStartEvent and ToolExecutionEndEvent. The executor body is entirely author-controlled — this is why scope enforcement is a first-class concern rather than a framework hook: the author writes assert_in_scope as the second line of every security tool's executor, and the model cannot bypass it because the check is in code, not the prompt. (4) The result is a structured AgentToolResult (Pydantic, extra="forbid") that flows back through the event and into the transcript. Blocked calls and successful calls ride the same path — the only difference is ok=false and a BLOCKED content string.


Diagram 4 — The Event Stream Contract (Provider → Loop → Harness → Frontend)

flowchart TB
    subgraph PROVIDER["tau_ai — provider layer"]
        P[ModelProvider.stream_response]
        P --> PE1[ProviderResponseStartEvent]
        P --> PE2[ProviderTextDeltaEvent]
        P --> PE3[ProviderThinkingDeltaEvent]
        P --> PE4[ProviderRetryEvent]
        P --> PE5[ProviderResponseEndEvent]
        P --> PE6[ProviderErrorEvent]
    end

    subgraph LOOP["tau_agent/loop.py — translates"]
        PE1 --> AE1[MessageStartEvent]
        PE2 --> AE2[MessageDeltaEvent]
        PE3 --> AE3[ThinkingDeltaEvent]
        PE4 --> AE4[RetryEvent]
        PE5 --> AE5[MessageEndEvent]
        PE6 --> AE6[ErrorEvent]
        AE7[ToolExecutionStartEvent]
        AE8[ToolExecutionEndEvent]
        AE9[TurnStartEvent]
        AE10[TurnEndEvent]
        AE11[AgentStartEvent]
        AE12[AgentEndEvent]
        AE13[QueueUpdateEvent]
        AE14[ToolExecutionUpdateEvent]
    end

    subgraph HARN["tau_agent/harness.py — fans out"]
        NOTIFY[_notify event<br/>for listener in _listeners:<br/>await listener event]
        YIELD[yield event to caller<br/>of harness.prompt]
    end

    subgraph FRONTENDS["consumers — the brain knows none of these"]
        TUI[tau_coding TUI<br/>renders deltas to terminal]
        SEC[tau_security listener<br/>captures ToolExecutionEndEvent<br/>→ EvidenceChain]
        CLI[any async-for consumer<br/>async for event in harness.prompt x]
    end

    AE1 --> NOTIFY
    AE2 --> NOTIFY
    AE7 --> NOTIFY
    AE8 --> NOTIFY
    AE13 --> NOTIFY
    NOTIFY --> YIELD
    YIELD --> TUI
    YIELD --> SEC
    YIELD --> CLI

    style PROVIDER fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style LOOP fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style HARN fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style FRONTENDS fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style P fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style NOTIFY fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style YIELD fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TUI fill:#1a1015,stroke:#9494a0,color:#9494a0
    style SEC fill:#1a1015,stroke:#5eead4,color:#5eead4
    style CLI fill:#1a1015,stroke:#9494a0,color:#9494a0
    style AE8 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style AE13 fill:#0d1b2a,stroke:#5eead4,color:#5eead4

Reading: The 14 typed events are the contract that makes tau's brain portable. The provider layer (tau_ai) emits provider-neutral events; the loop translates each into one of the 14 agent events; the harness's _notify fans every event to registered listeners (sync or async) and also yields it to the caller of harness.prompt(). Three different consumers can subscribe to the same brain without the brain knowing any of them exists: the tau_coding TUI renders deltas to the terminal; a tau-security listener inspects ToolExecutionEndEvent instances and captures evidence; any caller can async for event in harness.prompt(...) and react. The two events that matter most for security are ToolExecutionStartEvent (the ToolCall — what the model requested) and ToolExecutionEndEvent (the AgentToolResult — what happened). Together they are the complete audit record of every action the agent took. QueueUpdateEvent (steering/follow-up tuples) is the autonomy-gate event made visible to the UI. Every event is ConfigDict(extra="forbid") — no schema drift, no accidental fields.


Diagram 5 — tau vs tau_coding vs tau_security (Three Siblings, One Brain)

flowchart TB
    BRAIN[AgentHarness + run_agent_loop<br/>+ AgentTool + 14 events + Session JSONL<br/>tau_agent · ~960 LOC · zero UI deps]

    BRAIN --> CODING
    BRAIN --> SECURITY

    subgraph CODING["tau_coding — the coding app"]
        CTOOLS[tools: read · write · edit<br/>bash · glob · grep]
        CPROMPT[system prompt:<br/>coding assistant]
        CSPROMPT[scope: none]
        CSES[on-disk session:<br/>conversation history]
        COUT[output: code changes]
        CTUI[TUI · CLI · slash commands<br/>skills]
    end

    subgraph SECURITY["tau_security — the security app"]
        STOOLS[tools: port_scan · http_probe<br/>code_scan · record_finding<br/>generate_report]
        SPROMPT[system prompt:<br/>offensive state machine<br/>Recon→Hypothesis→Exploit<br/>→Evidence→Triage→Report]
        SSCOPE[scope: hard-wired<br/>assert_in_scope in every executor<br/>for_bug_bounty · for_ctf · for_appsec]
        SEVID[evidence: tamper-evident<br/>SHA-256 hash chain<br/>from ToolExecutionEndEvent]
        STRIAGE[triage: candidate→confirmed<br/>dedup · FP filter · confidence]
        SOUT[output: client-ready report<br/>JSON or HTML]
        SAUTO[autonomy: advisory · gated<br/>autonomous + max_turns budget]
    end

    style BRAIN fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style CODING fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style SECURITY fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style CTOOLS fill:#1a1015,stroke:#9494a0,color:#9494a0
    style STOOLS fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style SSCOPE fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style SEVID fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style SPROMPT fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style CPROMPT fill:#1a1015,stroke:#9494a0,color:#9494a0
    style SOUT fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style COUT fill:#1a1015,stroke:#9494a0,color:#9494a0
    style SAUTO fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style STRIAGE fill:#0d1b2a,stroke:#5eead4,color:#5eead4

Reading: tau-security is a sibling package to tau_coding, not a fork. Both wrap the same AgentHarness brain — the same loop, the same 14 events, the same append-only JSONL sessions, the same steering/follow-up queues. What differs is the four things any app-layer wrapper controls: the tools (coding's read/write/edit/bash vs security's port_scan/http_probe/code_scan/record_finding/generate_report), the system prompt (coding assistant vs the six-phase offensive state machine), the scope (none vs hard-wired assert_in_scope in every executor), and the output layer (code changes vs a tamper-evident evidence chain + triage pipeline + client-ready report). The SecurityHarness class is ~285 lines, almost all of which is the security system prompt and the tool wiring — the brain is inherited. This diagram is the course thesis made visible: the harness is 98.4% of the system, the model is 1.6%, and the security transformation localizes to the tools, the prompt, and an event listener — not the brain. Build a security harness by swapping tools, not by forking the loop.

# Diagrams — SDD-13: tau

---

## Diagram 1 — tau's Three-Layer Architecture

```mermaid
flowchart TB
    subgraph TAU_AI["tau_ai — provider/model streaming layer"]
        PROV[ModelProvider<br/>OpenAI · Anthropic · OpenRouter<br/>HF · Google · Mistral · local]
        PEVT[Provider events:<br/>ResponseStart · TextDelta<br/>ThinkingDelta · ResponseEnd<br/>Error · Retry]
        PROV --> PEVT
    end

    subgraph TAU_AGENT["tau_agent — the portable brain (zero UI/CLI deps)"]
        H[AgentHarness<br/>298 LOC<br/>owns transcript + queues + listeners]
        L[run_agent_loop<br/>276 LOC<br/>pure stateless provider/tool loop]
        T[AgentTool<br/>78 LOC<br/>frozen dataclass + async executor]
        E[14 AgentEvents<br/>134 LOC<br/>the layer contract]
        S[Session JSONL<br/>append-only · parent_id branching<br/>CompactionEntry · LeafEntry]
        H -->|delegates| L
        L -->|emits| E
        L -->|calls| T
        L -->|appends to| S
    end

    subgraph TAU_CODING["tau_coding — the coding app"]
        CT[read · write · edit · bash<br/>glob · grep<br/>1,057 LOC of tools]
        TUI[TUI · Textual<br/>CLI · slash commands<br/>skills · on-disk sessions]
        CT --> TUI
    end

    PEVT -->|provider.stream_response| L
    E -->|AgentEvent stream<br/>the contract| TUI

    style TAU_AI fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style TAU_AGENT fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TAU_CODING fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style H fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style L fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style T fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style E fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style S fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style PROV fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style PEVT fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style CT fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style TUI fill:#1a1015,stroke:#9494a0,color:#e4e4e8
```

**Reading**: tau's load-bearing design decision is the boundary between `tau_agent` (the portable brain) and `tau_coding` (the coding app). The brain depends only on `tau_ai` and Pydantic — it has zero knowledge of Textual, Rich, local config paths, slash commands, or rendering. A frontend consumes the `AgentEvent` stream. This separation is what makes the brain reusable: `tau_coding` wraps it as a coding agent; `tau_security` wraps the same brain as a security agent. The numbers are the actual source line counts — the entire brain (harness + loop + tools + events) is ~960 lines; the coding tools alone are 1,057. Read the layers top-down: providers emit neutral events, the loop translates them into 14 agent events, the harness fans events to listeners and owns state, the app is one consumer of the stream.

---

## Diagram 2 — The Agent Loop (Provider Stream → Events → Tool Execution → Events)

```mermaid
flowchart TB
    START([prompt called]) --> AST[AgentStartEvent]
    AST --> TURN[TurnStartEvent turn=N]
    TURN --> STREAM[async for provider_event in<br/>provider.stream_response]
    STREAM --> TRANSLATE{translate provider event}
    TRANSLATE -->|ResponseStart| MS[MessageStartEvent]
    TRANSLATE -->|TextDelta| MD[MessageDeltaEvent]
    TRANSLATE -->|ThinkingDelta| TD[ThinkingDeltaEvent]
    TRANSLATE -->|Retry| RE[RetryEvent]
    TRANSLATE -->|ResponseEnd| ME[MessageEndEvent<br/>append assistant msg]
    TRANSLATE -->|Error| ERR[ErrorEvent recoverable=false]

    ME --> CHECK{assistant has<br/>tool_calls?}
    CHECK -->|no| DRAIN1[drain steering queue]
    DRAIN1 --> Q1{queued msgs?}
    Q1 -->|yes| TURN
    Q1 -->|no| DRAIN2[drain follow_up queue]
    DRAIN2 --> Q2{queued msgs?}
    Q2 -->|yes| TURN
    Q2 -->|no| STOP

    CHECK -->|yes| EXEC[for each ToolCall:]
    EXEC --> TES[ToolExecutionStartEvent<br/>carries ToolCall]
    TES --> CALL[await tool.execute arguments, signal]
    CALL --> RESULT{result}
    RESULT -->|exception caught| FAIL[AgentToolResult ok=false<br/>isolation boundary — loop never crashes]
    RESULT -->|success| OK[AgentToolResult ok=true]
    FAIL --> APPEND[append ToolResultMessage<br/>to transcript]
    OK --> APPEND
    APPEND --> TEE[ToolExecutionEndEvent<br/>carries AgentToolResult]
    TEE --> NEXT{more tool calls?}
    NEXT -->|yes| EXEC
    NEXT -->|no| TE[TurnEndEvent turn=N]
    TE --> DRAIN3[drain steering queue]
    DRAIN3 --> BUDGET{max_turns reached?}
    BUDGET -->|no| TURN
    BUDGET -->|yes| MAXERR[ErrorEvent recoverable=true<br/>max_turns budget hit]
    MAXERR --> STOP
    STOP([AgentEndEvent])

    style AST fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TURN fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style STREAM fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style MS fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style MD fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style ME fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TES fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TEE fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style CALL fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style FAIL fill:#1a1015,stroke:#9494a0,color:#9494a0
    style OK fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style ERR fill:#1a1015,stroke:#9494a0,color:#9494a0
    style MAXERR fill:#1a1015,stroke:#9494a0,color:#9494a0
    style STOP fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TE fill:#0d1b2a,stroke:#5eead4,color:#5eead4
```

**Reading**: `run_agent_loop` is a pure stateless async generator (276 lines). The caller (the harness) owns the transcript list; the loop appends assistant and tool-result messages to it. Each turn translates the provider's neutral event stream into the 14 typed agent events. The tool-execution path is the load-bearing part for security: every `ToolCall` yields a `ToolExecutionStartEvent` (carrying the requested name and arguments), then `await tool.execute(arguments, signal)`, then a `ToolExecutionEndEvent` (carrying the `AgentToolResult`). Tool exceptions are caught at the loop boundary — the `# noqa: BLE001 - tools are an isolation boundary` comment is explicit: a failing tool returns `ok=false` and never crashes the loop. This is where scope enforcement lives in `tau-security`: the executor calls `assert_in_scope` before any work, and out-of-scope calls return a blocked result that rides the same `ToolExecutionEndEvent` path. The `max_turns` budget is the safety valve. The steering/follow-up queues drain at the turn boundary — that is the human-in-the-loop injection point.

---

## Diagram 3 — The AgentTool Lifecycle (Frozen Dataclass → Schema → Executor → Result)

```mermaid
flowchart LR
    subgraph DEF["1. Definition (author writes a factory)"]
        FACT[create_port_scan_tool ctx<br/>closes over Scope + EvidenceChain]
        FACT --> BUILD[AgentTool name=port_scan<br/>description=...<br/>input_schema=host, ports<br/>executor=execute]
    end

    subgraph MODEL["2. The model sees"]
        SCHEMA[input_schema JSON:<br/>properties.host type string<br/>required host]
        DESC[description string:<br/>Scan ports on an in-scope host...]
    end

    subgraph EXEC["3. The loop calls (per ToolCall)"]
        TES[ToolExecutionStartEvent<br/>carries ToolCall: name + arguments]
        TES --> CALL[await tool.execute arguments, signal]
        CALL --> GATE{executor body}
        GATE --> S1[1. extract target from arguments]
        S1 --> S2[2. assert_in_scope scope, host, port_scan]
        S2 --> SCOPE{in scope?}
        SCOPE -->|no| BLK[return _blocked result<br/>ok=false · never executes nmap]
        SCOPE -->|yes, rate ok| S3[3. execute nmap subprocess]
        S3 --> S4[4. _capture_evidence → EvidenceChain]
        S4 --> S5[5. return _success result<br/>ok=true · evidence_id · finding_id]
    end

    subgraph RESULT["4. The loop receives"]
        BLK --> RES[AgentToolResult<br/>tool_call_id · name · ok<br/>content · data · error · details]
        S5 --> RES
        RES --> TEE[ToolExecutionEndEvent<br/>carries AgentToolResult]
        TEE --> APPEND[append ToolResultMessage<br/>to transcript]
    end

    BUILD --> SCHEMA
    BUILD --> DESC
    BUILD --> CALL

    style DEF fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style MODEL fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style EXEC fill:#0d1b2a,stroke:#5eead4,color:#e4e4e8
    style RESULT fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style FACT fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style BUILD fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style CALL fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style GATE fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style S2 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style SCOPE fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style BLK fill:#1a1015,stroke:#9494a0,color:#9494a0
    style S3 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style S4 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style RES fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TEE fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TES fill:#0d1b2a,stroke:#5eead4,color:#5eead4
```

**Reading**: `AgentTool` is a `@dataclass(frozen=True, slots=True)` — 78 lines total. The `frozen` property means the executor cannot be reassigned at runtime (a security property: scope enforcement, once wired in, cannot be swapped out). The lifecycle has four stages. (1) The author writes a factory function that closes over shared context (the `Scope` and `EvidenceChain` in `tau-security`) and returns an `AgentTool`. (2) The model sees only the `description` and `input_schema` — it does not see the executor. (3) The loop calls `await tool.execute(arguments, signal)` per `ToolCall`, bracketed by `ToolExecutionStartEvent` and `ToolExecutionEndEvent`. The executor body is entirely author-controlled — this is why scope enforcement is a first-class concern rather than a framework hook: the author writes `assert_in_scope` as the second line of every security tool's executor, and the model cannot bypass it because the check is in code, not the prompt. (4) The result is a structured `AgentToolResult` (Pydantic, `extra="forbid"`) that flows back through the event and into the transcript. Blocked calls and successful calls ride the same path — the only difference is `ok=false` and a `BLOCKED` content string.

---

## Diagram 4 — The Event Stream Contract (Provider → Loop → Harness → Frontend)

```mermaid
flowchart TB
    subgraph PROVIDER["tau_ai — provider layer"]
        P[ModelProvider.stream_response]
        P --> PE1[ProviderResponseStartEvent]
        P --> PE2[ProviderTextDeltaEvent]
        P --> PE3[ProviderThinkingDeltaEvent]
        P --> PE4[ProviderRetryEvent]
        P --> PE5[ProviderResponseEndEvent]
        P --> PE6[ProviderErrorEvent]
    end

    subgraph LOOP["tau_agent/loop.py — translates"]
        PE1 --> AE1[MessageStartEvent]
        PE2 --> AE2[MessageDeltaEvent]
        PE3 --> AE3[ThinkingDeltaEvent]
        PE4 --> AE4[RetryEvent]
        PE5 --> AE5[MessageEndEvent]
        PE6 --> AE6[ErrorEvent]
        AE7[ToolExecutionStartEvent]
        AE8[ToolExecutionEndEvent]
        AE9[TurnStartEvent]
        AE10[TurnEndEvent]
        AE11[AgentStartEvent]
        AE12[AgentEndEvent]
        AE13[QueueUpdateEvent]
        AE14[ToolExecutionUpdateEvent]
    end

    subgraph HARN["tau_agent/harness.py — fans out"]
        NOTIFY[_notify event<br/>for listener in _listeners:<br/>await listener event]
        YIELD[yield event to caller<br/>of harness.prompt]
    end

    subgraph FRONTENDS["consumers — the brain knows none of these"]
        TUI[tau_coding TUI<br/>renders deltas to terminal]
        SEC[tau_security listener<br/>captures ToolExecutionEndEvent<br/>→ EvidenceChain]
        CLI[any async-for consumer<br/>async for event in harness.prompt x]
    end

    AE1 --> NOTIFY
    AE2 --> NOTIFY
    AE7 --> NOTIFY
    AE8 --> NOTIFY
    AE13 --> NOTIFY
    NOTIFY --> YIELD
    YIELD --> TUI
    YIELD --> SEC
    YIELD --> CLI

    style PROVIDER fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style LOOP fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style HARN fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style FRONTENDS fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style P fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style NOTIFY fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style YIELD fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style TUI fill:#1a1015,stroke:#9494a0,color:#9494a0
    style SEC fill:#1a1015,stroke:#5eead4,color:#5eead4
    style CLI fill:#1a1015,stroke:#9494a0,color:#9494a0
    style AE8 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style AE13 fill:#0d1b2a,stroke:#5eead4,color:#5eead4
```

**Reading**: The 14 typed events are the contract that makes tau's brain portable. The provider layer (`tau_ai`) emits provider-neutral events; the loop translates each into one of the 14 agent events; the harness's `_notify` fans every event to registered listeners (sync or async) and also yields it to the caller of `harness.prompt()`. Three different consumers can subscribe to the same brain without the brain knowing any of them exists: the `tau_coding` TUI renders deltas to the terminal; a `tau-security` listener inspects `ToolExecutionEndEvent` instances and captures evidence; any caller can `async for event in harness.prompt(...)` and react. The two events that matter most for security are `ToolExecutionStartEvent` (the `ToolCall` — what the model requested) and `ToolExecutionEndEvent` (the `AgentToolResult` — what happened). Together they are the complete audit record of every action the agent took. `QueueUpdateEvent` (steering/follow-up tuples) is the autonomy-gate event made visible to the UI. Every event is `ConfigDict(extra="forbid")` — no schema drift, no accidental fields.

---

## Diagram 5 — tau vs tau_coding vs tau_security (Three Siblings, One Brain)

```mermaid
flowchart TB
    BRAIN[AgentHarness + run_agent_loop<br/>+ AgentTool + 14 events + Session JSONL<br/>tau_agent · ~960 LOC · zero UI deps]

    BRAIN --> CODING
    BRAIN --> SECURITY

    subgraph CODING["tau_coding — the coding app"]
        CTOOLS[tools: read · write · edit<br/>bash · glob · grep]
        CPROMPT[system prompt:<br/>coding assistant]
        CSPROMPT[scope: none]
        CSES[on-disk session:<br/>conversation history]
        COUT[output: code changes]
        CTUI[TUI · CLI · slash commands<br/>skills]
    end

    subgraph SECURITY["tau_security — the security app"]
        STOOLS[tools: port_scan · http_probe<br/>code_scan · record_finding<br/>generate_report]
        SPROMPT[system prompt:<br/>offensive state machine<br/>Recon→Hypothesis→Exploit<br/>→Evidence→Triage→Report]
        SSCOPE[scope: hard-wired<br/>assert_in_scope in every executor<br/>for_bug_bounty · for_ctf · for_appsec]
        SEVID[evidence: tamper-evident<br/>SHA-256 hash chain<br/>from ToolExecutionEndEvent]
        STRIAGE[triage: candidate→confirmed<br/>dedup · FP filter · confidence]
        SOUT[output: client-ready report<br/>JSON or HTML]
        SAUTO[autonomy: advisory · gated<br/>autonomous + max_turns budget]
    end

    style BRAIN fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style CODING fill:#1a1015,stroke:#9494a0,color:#e4e4e8
    style SECURITY fill:#1a1015,stroke:#5eead4,color:#e4e4e8
    style CTOOLS fill:#1a1015,stroke:#9494a0,color:#9494a0
    style STOOLS fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style SSCOPE fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style SEVID fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style SPROMPT fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style CPROMPT fill:#1a1015,stroke:#9494a0,color:#9494a0
    style SOUT fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style COUT fill:#1a1015,stroke:#9494a0,color:#9494a0
    style SAUTO fill:#0d1b2a,stroke:#5eead4,color:#5eead4
    style STRIAGE fill:#0d1b2a,stroke:#5eead4,color:#5eead4
```

**Reading**: `tau-security` is a sibling package to `tau_coding`, not a fork. Both wrap the same `AgentHarness` brain — the same loop, the same 14 events, the same append-only JSONL sessions, the same steering/follow-up queues. What differs is the four things any app-layer wrapper controls: the tools (coding's read/write/edit/bash vs security's port_scan/http_probe/code_scan/record_finding/generate_report), the system prompt (coding assistant vs the six-phase offensive state machine), the scope (none vs hard-wired `assert_in_scope` in every executor), and the output layer (code changes vs a tamper-evident evidence chain + triage pipeline + client-ready report). The `SecurityHarness` class is ~285 lines, almost all of which is the security system prompt and the tool wiring — the brain is inherited. This diagram is the course thesis made visible: the harness is 98.4% of the system, the model is 1.6%, and the security transformation localizes to the tools, the prompt, and an event listener — not the brain. Build a security harness by swapping tools, not by forking the loop.