Skip to content
Back to skills

563 Pydantic Removal Contract Be7a38c8

ASecurity

This document defines the minimal internal contracts that must remain stable while replacing pydantic-ai: request loop responsibilities, canonical message shapes, tool lifecycle handling, persistence format, and orchestration IDs. It is the checklist for removing pydantic-ai without losing state, history, or tool behavior.

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
ai-agentsbashnoderefactoring

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 563-pydantic-removal-contract_be7a38c8 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 563 Pydantic Removal Contract Be7a38c8?

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

Security grade badge for 563 Pydantic Removal Contract Be7a38c8
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-563-pydantic-removal-contract-be7a38c8/badge)](https://www.skillsdirectory.com/skills/tools-only-563-pydantic-removal-contract-be7a38c8)

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

SKILL.md
---
title: Pydantic Removal Contract
link: pydantic-removal-contract
type: doc
path: docs/refactoring/pydantic-removal-contract.md
depth: 2
seams: [A, M, S, D]
ontological_relations:
  - relates_to: [[message-flow-map]]
  - affects: [[request-loop]]
  - affects: [[message-serialization]]
  - affects: [[tool-call-lifecycle]]
tags:
  - pydantic-ai
  - request-loop
  - serialization
  - tools
created_at: 2026-01-31T21:25:27-06:00
updated_at: 2026-01-31T21:25:27-06:00
uuid: 216ac580-5ee8-4882-a73e-39ac1f3794ae
---

# Pydantic Removal Contract

## Summary
This document defines the minimal internal contracts that must remain stable while replacing pydantic-ai: request loop responsibilities, canonical message shapes, tool lifecycle handling, persistence format, and orchestration IDs. It is the checklist for removing pydantic-ai without losing state, history, or tool behavior.

## Context
pydantic-ai currently owns the request loop and its message types. A prior runtime removal attempt failed because the agent loop expects `ModelRequest`/`ModelResponse` objects; when history was stored as wire dicts, `_clean_message_history` dropped prior turns and context vanished. Streaming is no longer used, so it is explicitly out of scope for this contract.

## Changes
- Mapped request loop responsibilities and node expectations that must be preserved.
- Documented canonical message and tool call shapes, including serialization formats.
- Enumerated orchestration IDs and invariants that must survive persistence and loop replacement.
- Captured retry/usage/tool lifecycle semantics that are currently pydantic-shaped.

## Contract Surface Map

| Surface | Files | Contract Summary |
| --- | --- | --- |
| Request loop | `src/tunacode/core/agents/main.py` | Request lifecycle, cleanup, iteration, abort handling, message persistence. |
| Node processing | `src/tunacode/core/agents/agent_components/orchestrator/` | Response parsing, tool dispatch, usage updates, empty response detection. |
| Canonical types | `src/tunacode/types/canonical.py` | Authoritative message + part definitions and tool call record shape. |
| Message adapter | `src/tunacode/utils/messaging/adapter.py` | Conversion to/from canonical forms; parts are single source of truth. |
| Persistence | `src/tunacode/core/state.py` | Session file schema and message serialization/deserialization. |
| Tool lifecycle | `src/tunacode/core/types/tool_registry.py`, `tools/decorators.py`, `agent_components/tool_executor.py` | Tool call tracking, retries, and error contracts. |
| Provider + tools | `src/tunacode/core/agents/agent_components/agent_config.py` | Provider selection + tool schema registration. |
| UI dependencies | `src/tunacode/ui/app.py`, `src/tunacode/ui/headless/output.py` | Current UI assumes `ModelResponse` for latest assistant message. |

## Request Loop Contract (Core Orchestrator)

**Owner:** `RequestOrchestrator` in `src/tunacode/core/agents/main.py`.

**Required sequence (must be preserved or replaced 1:1):**

1. **Context + ID**
   - Generate `request_id` (`uuid4` prefix length 8) and assign to `session.runtime.request_id`.
2. **Runtime reset**
   - Clear runtime counters (`current_iteration`, `iteration_count`, `batch_counter`).
   - Clear `runtime.tool_registry`.
   - Reset `consecutive_empty_responses`.
   - Reset `task.original_query` unless already set.
3. **History preparation**
   - Prune old tool outputs via `prune_old_tool_outputs` (mutates `ToolReturnPart.content`).
   - Run `run_cleanup_loop` to remove dangling tool calls, empty responses, and consecutive requests.
   - If history ends with a request, drop it before adding a new request.
   - Run `sanitize_history_for_resume` (strip system prompts, clear `run_id`).
4. **Iterative loop**
   - `agent.iter(message, message_history=sanitized_history)`.
   - For each node:
     - Update iteration counters.
     - Process node via `process_node(...)` (usage updates, tool dispatch, thought capture).
     - Empty response detection + intervention (`EmptyResponseHandler`).
     - Break when `response_state.task_completed` is true.
5. **Finalize**
   - Flush buffered tool tasks (`_finalize_buffered_tasks`).
   - Persist authoritative run messages (`agent_run.all_messages()` plus external additions).
6. **Abort/timeout handling**
   - On abort: append an `[INTERRUPTED]` `ModelResponse` containing partial stream text.
   - Clean dangling tool calls, empty responses, consecutive requests.
   - Invalidate agent cache after abort or timeout.

**Critical side effects:**
- `session.conversation.messages` must be updated with authoritative run history.
- `session.update_token_count()` must run after message mutations.

## Node Shape Contract (Process Node)

`process_node()` expects a node object with the following attributes:

| Attribute | Used By | Purpose |
| --- | --- | --- |
| `node.request` | `_emit_tool_returns` | Extracts `ToolReturnPart` parts; updates tool registry + UI callbacks. |
| `node.model_response` | `dispatch_tools` + usage tracker | Reads `parts` for tool calls; reads `usage` for token/cost. |
| `node.thought` | `record_thought` | Captures assistant reasoning into session thoughts. |
| `node.result.output` | request loop | Indicates visible response to set `response_state.has_user_response`. |

Any replacement loop must either keep this shape or adapt `process_node` to the new shape.

## Canonical Message Contract

**Authoritative types:** `CanonicalMessage`, `TextPart`, `ToolCallPart`, `ToolReturnPart`, `RetryPromptPart`, `SystemPromptPart`, `ThoughtPart` in `src/tunacode/types/canonical.py`.

**Rules:**
- `parts` is the single source of truth. **Never** rely on a separate `tool_calls` list.
- `tool_call_id` is required on tool call/return parts.
- Order of `parts` must be preserved.
- Legacy dict messages (`{"content": ...}` and `{"thought": ...}`) remain loadable until session migration is complete.

**Wire format for persistence (current default):**
```json
{
  "kind": "request|response",
  "parts": [
    {"part_kind": "text", "content": "..."},
    {"part_kind": "tool-call", "tool_call_id": "tc_1", "tool_name": "bash", "args": {"command": "ls"}},
    {"part_kind": "tool-return", "tool_call_id": "tc_1", "content": "..."}
  ]
}
```

**Sanitization rules (resume safety):**
- Strip `system-prompt` parts.
- Clear `run_id` attribute when present (pydantic-specific guard).

## Tool Call Lifecycle Contract

**Registry:** `ToolCallRegistry` (runtime) is the single source of truth for tool call status.

**Lifecycle steps:**
1. **Register** on tool call parsing (`record_tool_call_args`).
2. **Start** when tool execution begins (`_mark_tool_calls_running`).
3. **Complete** when a tool return part is emitted (`_emit_tool_returns`).
4. **Fail/Cancel** on error or abort (`_record_tool_failure`).

**Execution semantics:**
- `execute_tools_parallel` retries up to `TOOL_MAX_RETRIES`.
- `ToolRetryError` is converted to `pydantic_ai.ModelRetry` by decorators; retry signaling is part of the tool contract.
- `NON_RETRYABLE_ERRORS` include `ModelRetry`, `ToolExecutionError`, `UserAbortError`.

**Fallback parsing:**
- If no structured tool calls exist, `_extract_fallback_tool_calls` parses text parts and builds tool calls. This requires a `ToolCallPart`-compatible shape (`tool_call_id`, `tool_name`, `args`).

## Persistence Contract

**Session file keys (must remain stable):**
- `version`, `session_id`, `project_id`, `created_at`, `last_modified`, `working_directory`
- `current_model`, `total_tokens`, `session_total_usage`
- `thoughts`, `messages`

**Current serializer:** `StateManager._serialize_messages()` uses `pydantic.TypeAdapter(ModelMessage)`.

**Removal requirement:** new serializer must preserve:
- `kind`, `parts`, `part_kind`, `tool_call_id`, `tool_name`, `args`, `content`
- Legacy dict forms (`content`, `thought`) for backward compatibility

## Usage Tracking Contract

`update_usage()` expects a response `usage` object with attributes:
- `request_tokens`, `response_tokens`, `cached_tokens`

Providers must expose equivalent fields or be normalized to this shape.

## Orchestration IDs & Invariants

| ID | Stored In | Used By | Notes |
| --- | --- | --- | --- |
| `session_id` | `SessionState.session_id` | Persistence | Part of session filename. |
| `request_id` | `RuntimeState.request_id` | Logging | Per-request correlation. |
| `tool_call_id` | `ToolCallPart` + `ToolReturnPart` + registry | Tool lifecycle | Required for pairing calls/returns. |
| `run_id` | Message attribute (pydantic) | Resume sanitization | Cleared before reuse; new loop may drop it. |
| `batch_counter` | `RuntimeState.batch_counter` | Logs/UI | Increments per tool batch. |

**Invariants:**
- No dangling tool calls before sending a request.
- No consecutive request messages in history.
- System prompt parts are never persisted in history.
- Tool registry and message parts must reference the same `tool_call_id` values.

## Out of Scope
- **Streaming protocol** (`agent_components/streaming.py`) is not used and is excluded from this contract.

## Verification Checklist

- History survives round-trip serialization with identical `parts` and `tool_call_id` values.
- Session load preserves `thoughts`, `messages`, and `session_total_usage`.
- Tool registry records complete/fail/cancel updates in sync with tool return parts.
- Request loop still prunes and sanitizes history before a run.
- Abort path appends an `[INTERRUPTED]` response and cleans dangling tool calls.
- UI can still extract the latest assistant response without depending on `ModelResponse`.

## Behavioral Impact

**What readers gain:**
- A single, testable contract for replacing pydantic-ai without losing orchestration state.

**What doesn’t change:**
- Existing user-visible behavior (tool calls, history, persistence) remains the baseline.

## Related Cards
- [[message-flow-map]]

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.