Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
from models.errors import ErrorResponse
from models.export import ExportStateDict
from models.project import ProjectDict, ProjectSessionRowDict, SessionListItemDict
from models.record_data import RecordDataUnion
from models.search import SearchHitDict
from models.session import (
MessageDict,
QuickSessionInfoDict,
SessionDict,
SessionMetadataDict,
ToolUseDict,
)
from models.stats import FilesTouchedDict, SessionStatsDict
from models.tool_results import ToolResultUnion

__all__ = [
"ErrorResponse",
Expand All @@ -25,4 +28,7 @@
"SessionListItemDict",
"SessionMetadataDict",
"SessionStatsDict",
"RecordDataUnion",
"ToolResultUnion",
"ToolUseDict",
]
47 changes: 47 additions & 0 deletions models/record_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""TypedDict shapes for record-level ``data`` payloads on progress messages."""

from typing import Literal, TypedDict, TypeGuard


class BashProgressDataDict(TypedDict):
type: Literal["bash_progress"]
output: str
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated


class HookProgressDataDict(TypedDict, total=False):
type: Literal["hook_progress"]
output: str


class AgentProgressDataDict(TypedDict, total=False):
type: Literal["agent_progress"]
message: str


class SummaryDataDict(TypedDict, total=False):
"""Summary-style progress payloads (when present on progress entries)."""

type: Literal["summary"]
summary: str


class CompactBoundaryDataDict(TypedDict, total=False):
"""Compact-boundary metadata when carried on a data blob."""

type: Literal["compact_boundary"]
trigger: str
pre_tokens: int


RecordDataUnion = (
BashProgressDataDict
| HookProgressDataDict
| AgentProgressDataDict
| SummaryDataDict
| CompactBoundaryDataDict
| dict[str, object]
)


def is_bash_progress_data(data: RecordDataUnion) -> TypeGuard[BashProgressDataDict]:
Comment thread
clean6378-max-it marked this conversation as resolved.
Outdated
return isinstance(data, dict) and data.get("type") == "bash_progress"
32 changes: 26 additions & 6 deletions models/session.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
"""Parsed session shapes from jsonl_parser."""

from typing import Any, NotRequired, TypedDict
from typing import Any, Literal, NotRequired, TypedDict

from models.record_data import RecordDataUnion
from models.tool_results import ToolNameLiteral, ToolResultUnion


class ToolUseDict(TypedDict, total=False):
id: str
name: ToolNameLiteral | str
Comment thread
clean6378-max-it marked this conversation as resolved.
input: dict[str, object]


class MessageUsageDict(TypedDict, total=False):
input_tokens: int
output_tokens: int
cache_read: int
cache_creation: int
service_tier: str | None


SystemSubtypeLiteral = Literal["compact_boundary", "init"]


class MessageDict(TypedDict):
Expand All @@ -12,18 +32,18 @@ class MessageDict(TypedDict):
content: NotRequired[str]
images: NotRequired[list[Any] | None]
is_sidechain: NotRequired[bool]
tool_result: NotRequired[Any]
tool_result_parsed: NotRequired[dict[str, Any] | None]
tool_result: NotRequired[ToolResultUnion | None]
tool_result_parsed: NotRequired[dict[str, object] | None]
slug: NotRequired[str | None]
model: NotRequired[str]
stop_reason: NotRequired[str]
thinking: NotRequired[str | None]
tool_uses: NotRequired[list[dict[str, Any]] | None]
tool_uses: NotRequired[list[ToolUseDict] | None]
is_api_error: NotRequired[bool]
usage: NotRequired[dict[str, Any]]
usage: NotRequired[MessageUsageDict]
subtype: NotRequired[str]
level: NotRequired[str]
data: NotRequired[Any]
data: NotRequired[RecordDataUnion]
progress_type: NotRequired[str]
tool_use_id: NotRequired[str | None]
parent_tool_use_id: NotRequired[str | None]
Expand Down
207 changes: 207 additions & 0 deletions models/tool_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
"""TypedDict shapes for Claude Code toolUseResult blobs at the JSONL parse boundary.

Ground truth: tests/test_jsonl_parser.py, tests/test_real_session_fixtures.py,
and utils/tool_dispatch.py predicate order (first match wins).
"""

from typing import Literal, TypedDict, TypeGuard


class BashToolResultDict(TypedDict, total=False):
stdout: str
stderr: str
exitCode: int
interrupted: bool
is_error: bool
returnCodeInterpretation: str


class FileEditToolResultDict(TypedDict, total=False):
structuredPatch: str
filePath: str
newString: str
replaceAll: bool


class PlanToolResultDict(TypedDict, total=False):
plan: list[object]
filePath: str
content: str


class FileWriteToolResultDict(TypedDict, total=False):
filePath: str
content: str


class GlobToolResultDict(TypedDict, total=False):
filenames: list[str]
numFiles: int
truncated: bool
durationMs: int


class GrepToolResultDict(TypedDict, total=False):
mode: str
numFiles: int
numLines: int
content: str
durationMs: int


class ReadFileObjDict(TypedDict, total=False):
filePath: str
numLines: int
content: str


class ReadToolResultDict(TypedDict, total=False):
file: ReadFileObjDict
content: list[object]


class WebSearchToolResultDict(TypedDict, total=False):
query: str
results: list[object] | None
durationSeconds: float


class WebFetchToolResultDict(TypedDict, total=False):
url: str
code: int
durationMs: int


class TaskMessageToolResultDict(TypedDict, total=False):
task_id: str
task_type: str
message: str
agentId: str


class TaskRetrievalToolResultDict(TypedDict, total=False):
retrieval_status: str
task: dict[str, object]


class TaskCompletedToolResultDict(TypedDict, total=False):
agentId: str
totalDurationMs: int
status: str
totalTokens: int
totalToolUseCount: int


class TaskAsyncToolResultDict(TypedDict, total=False):
agentId: str
isAsync: bool
status: str
description: str


class TodoItemDict(TypedDict, total=False):
id: str
content: str


class TodoWriteToolResultDict(TypedDict, total=False):
newTodos: list[TodoItemDict]
oldTodos: list[TodoItemDict]


class UserInputToolResultDict(TypedDict, total=False):
questions: list[dict[str, object]]
answers: dict[str, object]


class ToolResultContentBlockDict(TypedDict, total=False):
type: str
source: dict[str, object]


class ToolResultWithContentDict(TypedDict, total=False):
"""Read-on-image and similar payloads that embed content blocks."""

content: list[ToolResultContentBlockDict]


# Dict passed into dispatch predicates (structural superset of all tool blobs).
ToolResultDict = dict[str, object]

ToolResultUnion = (
str
| BashToolResultDict
| FileEditToolResultDict
| PlanToolResultDict
| FileWriteToolResultDict
| GlobToolResultDict
| GrepToolResultDict
| ReadToolResultDict
| WebSearchToolResultDict
| WebFetchToolResultDict
| TaskMessageToolResultDict
| TaskRetrievalToolResultDict
| TaskCompletedToolResultDict
| TaskAsyncToolResultDict
| TodoWriteToolResultDict
| UserInputToolResultDict
| ToolResultWithContentDict
| dict[str, object]
)


def is_tool_result_dict(tr: ToolResultUnion | None) -> TypeGuard[ToolResultDict]:
return isinstance(tr, dict)


def is_bash_tool_result(tr: ToolResultDict) -> TypeGuard[BashToolResultDict]:
return "stdout" in tr or "stderr" in tr


def is_file_edit_tool_result(tr: ToolResultDict) -> TypeGuard[FileEditToolResultDict]:
return "structuredPatch" in tr or ("filePath" in tr and "newString" in tr)


def is_plan_tool_result(tr: ToolResultDict) -> TypeGuard[PlanToolResultDict]:
return "plan" in tr and "filePath" in tr


def is_file_write_tool_result(tr: ToolResultDict) -> TypeGuard[FileWriteToolResultDict]:
return "filePath" in tr and "content" in tr


def is_glob_tool_result(tr: ToolResultDict) -> TypeGuard[GlobToolResultDict]:
filenames = tr.get("filenames")
return "filenames" in tr and isinstance(filenames, list)


def is_grep_tool_result(tr: ToolResultDict) -> TypeGuard[GrepToolResultDict]:
return "mode" in tr and "numFiles" in tr


def is_read_tool_result(tr: ToolResultDict) -> TypeGuard[ReadToolResultDict]:
file_obj = tr.get("file")
return "file" in tr and isinstance(file_obj, dict)


def is_web_search_tool_result(tr: ToolResultDict) -> TypeGuard[WebSearchToolResultDict]:
return "query" in tr and "results" in tr


def is_web_fetch_tool_result(tr: ToolResultDict) -> TypeGuard[WebFetchToolResultDict]:
return "url" in tr and "code" in tr
Comment thread
clean6378-max-it marked this conversation as resolved.


# Tool names on assistant tool_use blocks — pairs with slug on user tool_result rows.
ToolNameLiteral = Literal[
"Bash",
"Read",
"Write",
"Edit",
"Glob",
"Grep",
"Task",
"TodoWrite",
"WebFetch",
"WebSearch",
]
Loading
Loading