Skip to content
Back to skills

488 Message Flow Map 308463fa

ASecurity

This document traces message handling from entry to exit, documenting the single source of truth pattern.

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

Works with

  • api

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 488-message-flow-map_308463fa --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 488 Message Flow Map 308463fa?

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

Security grade badge for 488 Message Flow Map 308463fa
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-488-message-flow-map-308463fa/badge)](https://www.skillsdirectory.com/skills/tools-only-488-message-flow-map-308463fa)

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
# Message Flow Behavioral Map

This document traces message handling from entry to exit, documenting the single source of truth pattern.

## Overview

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                           MESSAGE FLOW                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌──────────┐    ┌───────────────┐    ┌────────────────┐    ┌──────────┐  │
│  │ pydantic │ -> │   adapter.py  │ -> │ CanonicalTypes │ -> │  tunacode │  │
│  │   -ai    │    │ to_canonical()│    │                │    │   core    │  │
│  └──────────┘    └───────────────┘    └────────────────┘    └──────────┘  │
│       ▲                                      │                     │       │
│       │              ┌───────────────┐       │                     │       │
│       └──────────────│ from_canonic │<──────┘                     │       │
│                      │    al()      │                              │       │
│                      └───────────────┘                             ▼       │
│                                                            ┌──────────┐    │
│                                                            │   UI     │    │
│                                                            └──────────┘    │
└─────────────────────────────────────────────────────────────────────────────┘
```

## Single Source of Truth: `parts`

**Key insight**: There is only ONE source of truth for message content - the `parts` list.

```
pydantic-ai ModelResponse
├── parts: list[Part]           <-- SINGLE SOURCE OF TRUTH
│   ├── TextPart
│   ├── ToolCallPart            <-- Tool calls live HERE
│   └── ToolReturnPart          <-- Tool returns live HERE
│
└── tool_calls (property)       <-- COMPUTED from parts, not separate data
```

The `tool_calls` property on pydantic-ai objects is a computed property that iterates `parts` and filters for `ToolCallPart`. It is NOT a separate data store.

## Entry Points

### 1. From pydantic-ai Agent

```python
# In core/agent_runner.py (conceptual)
async for node in agent.iter(prompt, message_history=messages):
    response: ModelResponse = node.response

    # Response contains parts with tool calls
    # parts: [TextPart("Let me read that"), ToolCallPart(id="tc_1", ...)]
```

### 2. From Serialized Session (JSON)

```python
# Session loading from JSON
{
    "kind": "response",
    "parts": [
        {"part_kind": "text", "content": "Let me help"},
        {"part_kind": "tool-call", "tool_call_id": "tc_1", "tool_name": "bash", "args": {...}}
    ]
}
```

### 3. From Legacy Dict Formats

```python
# Old format (still supported)
{"content": "Hello world"}
{"thought": "I should think about this"}
```

## Conversion Flow

### Forward: pydantic-ai → Canonical

```
to_canonical(message)
     │
     ▼
┌────────────────────────────────────────────┐
│ 1. Check for legacy formats (thought, content)  │
│    - {"thought": ...} → ThoughtPart             │
│    - {"content": ...} → TextPart                │
└────────────────────────────────────────────┘
     │
     ▼
┌────────────────────────────────────────────┐
│ 2. Determine role from message kind            │
│    - "request" → MessageRole.USER              │
│    - "response" → MessageRole.ASSISTANT        │
└────────────────────────────────────────────┘
     │
     ▼
┌────────────────────────────────────────────┐
│ 3. Extract parts (SINGLE SOURCE OF TRUTH)      │
│    - _get_parts(message) returns parts list    │
│    - Each part converted by _convert_part()    │
└────────────────────────────────────────────┘
     │
     ▼
┌────────────────────────────────────────────┐
│ 4. Convert each part to canonical              │
│    - "text" → TextPart                         │
│    - "user-prompt" → TextPart                  │
│    - "tool-call" → ToolCallPart                │
│    - "tool-return" → ToolReturnPart            │
│    - "system-prompt" → SystemPromptPart        │
└────────────────────────────────────────────┘
     │
     ▼
  CanonicalMessage(role, parts=tuple(...))
```

### Backward: Canonical → pydantic-ai

```
from_canonical(canonical_message)
     │
     ▼
┌────────────────────────────────────────────┐
│ 1. Determine kind from role                    │
│    - USER/SYSTEM → "request"                   │
│    - ASSISTANT → "response"                    │
└────────────────────────────────────────────┘
     │
     ▼
┌────────────────────────────────────────────┐
│ 2. Convert each canonical part to dict         │
│    - TextPart → {"part_kind": "text", ...}     │
│    - ToolCallPart → {"part_kind": "tool-call"} │
│    - etc.                                      │
└────────────────────────────────────────────┘
     │
     ▼
  {"kind": "request|response", "parts": [...]}
```

## Tool Call Lifecycle

### Detection

```python
get_tool_call_ids(message) -> set[str]
    │
    ▼
to_canonical(message)
    │
    ▼
message.get_tool_call_ids()
    │
    ▼
[part.tool_call_id for part in parts if isinstance(part, ToolCallPart)]
```

### Matching Returns

```python
get_tool_return_ids(message) -> set[str]
    │
    ▼
[part.tool_call_id for part in parts if isinstance(part, ToolReturnPart)]
```

### Dangling Detection

A "dangling" tool call is one that has no matching tool return.

```python
find_dangling_tool_calls(messages: list) -> set[str]
    │
    ▼
┌────────────────────────────────────────────┐
│ For each message:                              │
│   call_ids.update(get_tool_call_ids(msg))      │
│   return_ids.update(get_tool_return_ids(msg))  │
└────────────────────────────────────────────┘
    │
    ▼
return call_ids - return_ids  # IDs with no return
```

**When dangling occurs:**
- User aborts mid-tool-execution
- Network failure during tool call
- Session restored from interrupted state

**Resolution:**
```python
# In sanitize.py or abort handler
dangling = find_dangling_tool_calls(messages)
if dangling:
    # Inject synthetic tool returns
    for tool_call_id in dangling:
        messages.append({
            "kind": "request",
            "parts": [{
                "part_kind": "tool-return",
                "tool_call_id": tool_call_id,
                "content": "[Aborted by user]"
            }]
        })
```

## Content Extraction

```python
get_content(message) -> str
    │
    ├── if CanonicalMessage: message.get_text_content()
    │
    └── else: to_canonical(message).get_text_content()
           │
           ▼
    " ".join(
        part.content
        for part in parts
        if isinstance(part, (TextPart, ThoughtPart))
    )
```

## Invariants

These must always be true:

1. **Parts is the only source**: All tool call data comes from `parts`, never from computed properties
2. **Every tool call needs a return**: Before sending to API, all tool calls must have matching returns
3. **Immutable canonical types**: Once created, `CanonicalMessage` and parts are frozen
4. **Role consistency**: `kind="request"` → USER role, `kind="response"` → ASSISTANT role

## Files

| File | Purpose |
|------|---------|
| `src/tunacode/types/canonical.py` | Canonical type definitions |
| `src/tunacode/utils/messaging/adapter.py` | Bidirectional conversion |
| `tests/unit/types/test_adapter.py` | Adapter tests |
| `tests/unit/types/test_canonical.py` | Canonical type tests |

## Tool Call Registry (Chunk 5)

Tool call lifecycle state now lives in `session.runtime.tool_registry`, which stores
`CanonicalToolCall` entries keyed by `tool_call_id`. ReAct guidance and JSON export
consume the registry instead of a standalone `tool_calls` list.

Files in this skill

  • README.md908 B
  • skill.md10 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.