clawd-code-sdk 1.0.0__tar.gz → 1.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/PKG-INFO +1 -1
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/pyproject.toml +1 -1
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/__init__.py +4 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/__init__.py +17 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/base.py +1 -0
- clawd_code_sdk-1.0.2/src/clawd_code_sdk/models/content_blocks.py +146 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/messages.py +121 -11
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/storage/helpers.py +0 -39
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/storage/models.py +47 -94
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/storage/replay.py +85 -196
- clawd_code_sdk-1.0.2/tests/e2e/test_storage_parsing.py +26 -0
- clawd_code_sdk-1.0.0/src/clawd_code_sdk/models/content_blocks.py +0 -97
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/.gitignore +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/LICENSE +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/README.md +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/_bundled/.gitignore +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/_errors.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/_internal/__init__.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/_internal/message_parser.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/_internal/query.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/_internal/transport/__init__.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/_internal/transport/subprocess_cli.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/_version.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/anthropic_types.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/client.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/list_sessions.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/mcp_utils.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/agents.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/control.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/hooks.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/input_types.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/mcp.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/options.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/output_types.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/permissions.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/prompt_requests.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/prompts.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/server_info.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/settings.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/system_messages.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/models/thinking.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/py.typed +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/query.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/session.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/storage/ARCHITECTURE.md +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/storage/__init__.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/src/clawd_code_sdk/usage.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/__init__.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/conftest.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/e2e/__init__.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/e2e/test_agents_and_settings.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/e2e/test_dynamic_control.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/e2e/test_hook_events.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/e2e/test_hooks.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/e2e/test_include_partial_messages.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/e2e/test_mcp_tools.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/e2e/test_sdk_mcp_tools.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/e2e/test_slash_commands.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/e2e/test_stderr_callback.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/e2e/test_structured_output.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/e2e/test_subagent_invocation.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/e2e/test_tool_permissions.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/mcp_server.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/mock_claude_server.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/test_changelog.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/test_client.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/test_errors.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/test_image.png +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/test_integration.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/test_message_parser.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/test_sdk_mcp_integration.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/test_session.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/test_streaming_client.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/test_subprocess_buffering.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/test_tool_callbacks.py +0 -0
- {clawd_code_sdk-1.0.0 → clawd_code_sdk-1.0.2}/tests/test_transport.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clawd-code-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Python SDK for Claude Code
|
|
5
5
|
Project-URL: Documentation, https://github.com/phil65/claude-agent-sdk-python
|
|
6
6
|
Project-URL: Homepage, https://github.com/phil65/claude-agent-sdk-python
|
|
@@ -37,6 +37,8 @@ from .models import (
|
|
|
37
37
|
CommandHookHandler,
|
|
38
38
|
OnUserQuestion,
|
|
39
39
|
ContentBlock,
|
|
40
|
+
ImageBlock,
|
|
41
|
+
ImageSource,
|
|
40
42
|
HookCallback,
|
|
41
43
|
HookContext,
|
|
42
44
|
HookHandler,
|
|
@@ -168,7 +170,9 @@ __all__ = [
|
|
|
168
170
|
"HookMatcher",
|
|
169
171
|
"HookMatcherConfig",
|
|
170
172
|
"Hooks",
|
|
173
|
+
"ImageBlock",
|
|
171
174
|
"ImageMediaType",
|
|
175
|
+
"ImageSource",
|
|
172
176
|
"InitSystemMessage",
|
|
173
177
|
"InvalidRequestError",
|
|
174
178
|
"ListSessionsOptions",
|
|
@@ -206,6 +206,8 @@ from .output_types import (
|
|
|
206
206
|
)
|
|
207
207
|
from .content_blocks import (
|
|
208
208
|
ContentBlock,
|
|
209
|
+
ImageBlock,
|
|
210
|
+
ImageSource,
|
|
209
211
|
TextBlock,
|
|
210
212
|
ThinkingBlock,
|
|
211
213
|
ToolResultBlock,
|
|
@@ -318,6 +320,19 @@ def _message_discriminator(data: Any) -> str:
|
|
|
318
320
|
return str(type(data).__name__)
|
|
319
321
|
|
|
320
322
|
|
|
323
|
+
MessageUnion = (
|
|
324
|
+
UserMessage
|
|
325
|
+
| AssistantMessage
|
|
326
|
+
| ResultMessage
|
|
327
|
+
| StreamEvent
|
|
328
|
+
| RateLimitMessage
|
|
329
|
+
| ToolProgressMessage
|
|
330
|
+
| ToolUseSummaryMessage
|
|
331
|
+
| AuthStatusMessage
|
|
332
|
+
| PromptSuggestionMessage
|
|
333
|
+
| SystemMessageUnion
|
|
334
|
+
)
|
|
335
|
+
|
|
321
336
|
Message = Annotated[
|
|
322
337
|
Annotated[UserMessage, Tag("user")]
|
|
323
338
|
| Annotated[AssistantMessage, Tag("assistant")]
|
|
@@ -427,7 +442,9 @@ __all__ = [
|
|
|
427
442
|
"HookSpecificOutput",
|
|
428
443
|
"HookStartedSystemMessage",
|
|
429
444
|
"Hooks",
|
|
445
|
+
"ImageBlock",
|
|
430
446
|
"ImageMediaType",
|
|
447
|
+
"ImageSource",
|
|
431
448
|
"InitSystemMessage",
|
|
432
449
|
"InstructionsLoadedHookInput",
|
|
433
450
|
"JSONRPCError",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Content block types shared by both wire-format messages and JSONL storage.
|
|
2
|
+
|
|
3
|
+
Claude Code uses the same content block schema on the wire (SDK ↔ CLI JSON
|
|
4
|
+
messages) and in persisted JSONL session transcripts. A single set of models
|
|
5
|
+
therefore serves both purposes, avoiding a redundant conversion layer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Discriminator, TypeAdapter
|
|
14
|
+
|
|
15
|
+
from clawd_code_sdk.models import ToolInput
|
|
16
|
+
from clawd_code_sdk.models.base import ClaudeCodeBaseModel, StopReason, ToolName
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from clawd_code_sdk.anthropic_types import ToolResultContentBlock
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# =============================================================================
|
|
24
|
+
# Content block types
|
|
25
|
+
# =============================================================================
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _ContentBlockBase(BaseModel):
|
|
29
|
+
"""Shared base for all content block types."""
|
|
30
|
+
|
|
31
|
+
# extra="allow": storage JSONL includes all union fields on every block
|
|
32
|
+
# with null for fields belonging to other block types.
|
|
33
|
+
model_config = ConfigDict(extra="allow", defer_build=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TextBlock(_ContentBlockBase):
|
|
37
|
+
"""Text content block."""
|
|
38
|
+
|
|
39
|
+
type: Literal["text"] = "text"
|
|
40
|
+
text: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ThinkingBlock(_ContentBlockBase):
|
|
44
|
+
"""Thinking/reasoning content block."""
|
|
45
|
+
|
|
46
|
+
type: Literal["thinking"] = "thinking"
|
|
47
|
+
thinking: str
|
|
48
|
+
signature: str = ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ToolUseBlock(_ContentBlockBase):
|
|
52
|
+
"""Tool use content block."""
|
|
53
|
+
|
|
54
|
+
type: Literal["tool_use"] = "tool_use"
|
|
55
|
+
id: str = ""
|
|
56
|
+
name: ToolName | str = ""
|
|
57
|
+
input: ToolInput | dict[str, Any] = {}
|
|
58
|
+
caller: dict[str, str] | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ToolResultBlock(_ContentBlockBase):
|
|
62
|
+
"""Tool result content block."""
|
|
63
|
+
|
|
64
|
+
type: Literal["tool_result"] = "tool_result"
|
|
65
|
+
tool_use_id: str = ""
|
|
66
|
+
content: str | list[dict[str, Any]] | None = None # BetaContentBlock
|
|
67
|
+
is_error: bool | None = None
|
|
68
|
+
|
|
69
|
+
def get_parsed_content(self) -> list[ToolResultContentBlock] | str | None:
|
|
70
|
+
from clawd_code_sdk.anthropic_types import validate_tool_result_content
|
|
71
|
+
|
|
72
|
+
if self.content is None or isinstance(self.content, str):
|
|
73
|
+
return self.content
|
|
74
|
+
# Validate list content against Anthropic SDK types
|
|
75
|
+
return validate_tool_result_content(self.content)
|
|
76
|
+
|
|
77
|
+
def extract_text(self) -> str:
|
|
78
|
+
"""Extract text content from this tool result."""
|
|
79
|
+
if self.content is None:
|
|
80
|
+
return ""
|
|
81
|
+
if isinstance(self.content, str):
|
|
82
|
+
return self.content
|
|
83
|
+
text_parts = [
|
|
84
|
+
tc.get("text", "")
|
|
85
|
+
for tc in self.content
|
|
86
|
+
if isinstance(tc, dict) and tc.get("type") == "text"
|
|
87
|
+
]
|
|
88
|
+
return "\n".join(text_parts)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ImageSource(_ContentBlockBase):
|
|
92
|
+
"""Base64-encoded image source data."""
|
|
93
|
+
|
|
94
|
+
type: Literal["base64"]
|
|
95
|
+
media_type: str
|
|
96
|
+
data: str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ImageBlock(_ContentBlockBase):
|
|
100
|
+
"""Image content block (storage-only, not emitted on the wire)."""
|
|
101
|
+
|
|
102
|
+
type: Literal["image"] = "image"
|
|
103
|
+
source: ImageSource
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# =============================================================================
|
|
107
|
+
# Unions and adapters
|
|
108
|
+
# =============================================================================
|
|
109
|
+
|
|
110
|
+
ContentBlock = Annotated[
|
|
111
|
+
TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock | ImageBlock,
|
|
112
|
+
Discriminator("type"),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
content_block_adapter = TypeAdapter[ContentBlock](ContentBlock)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# =============================================================================
|
|
119
|
+
# Message-level models
|
|
120
|
+
# =============================================================================
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class MessageParam(ClaudeCodeBaseModel):
|
|
124
|
+
"""Replacement for Anthropic MessageParam which serializes to our own content blocks."""
|
|
125
|
+
|
|
126
|
+
content: Sequence[ContentBlock] | str
|
|
127
|
+
role: Literal["user", "assistant"]
|
|
128
|
+
model_config = ConfigDict(extra="allow")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class AssistantMessageContent(ClaudeCodeBaseModel):
|
|
132
|
+
"""Assistant message payload mirroring ``anthropic.types.beta.BetaMessage``.
|
|
133
|
+
|
|
134
|
+
Uses our own ``ContentBlock`` types instead of the Anthropic SDK's
|
|
135
|
+
``BetaContentBlock`` variants. Extra fields from the wire format
|
|
136
|
+
(e.g. ``container``, ``context_management``) are preserved via ``extra="allow"``.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
id: str
|
|
140
|
+
type: Literal["message"] = "message"
|
|
141
|
+
role: Literal["assistant"] = "assistant"
|
|
142
|
+
content: Sequence[ContentBlock]
|
|
143
|
+
model: str
|
|
144
|
+
stop_reason: StopReason | None = None
|
|
145
|
+
stop_sequence: str | None = None
|
|
146
|
+
model_config = ConfigDict(extra="allow")
|
|
@@ -4,9 +4,22 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from collections.abc import Sequence
|
|
6
6
|
import re
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict
|
|
8
|
-
|
|
9
|
-
from anthropic.types import
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal, NotRequired, TypedDict, get_args
|
|
8
|
+
|
|
9
|
+
from anthropic.types.beta import (
|
|
10
|
+
BetaInputJSONDelta,
|
|
11
|
+
BetaMessageDeltaUsage,
|
|
12
|
+
BetaRawContentBlockDeltaEvent,
|
|
13
|
+
BetaRawContentBlockStartEvent,
|
|
14
|
+
BetaRawMessageDeltaEvent,
|
|
15
|
+
BetaRawMessageStreamEvent,
|
|
16
|
+
BetaTextBlock as ATextBlock,
|
|
17
|
+
BetaTextDelta,
|
|
18
|
+
BetaThinkingBlock as AThinkingBlock,
|
|
19
|
+
BetaThinkingDelta,
|
|
20
|
+
BetaToolUseBlock as AToolUseBlock,
|
|
21
|
+
)
|
|
22
|
+
from anthropic.types.beta.beta_raw_message_delta_event import Delta as BetaRawMessageDelta
|
|
10
23
|
from pydantic import BaseModel, ConfigDict
|
|
11
24
|
|
|
12
25
|
from clawd_code_sdk._errors import (
|
|
@@ -28,9 +41,7 @@ from clawd_code_sdk.models.output_types import ToolUseResult
|
|
|
28
41
|
|
|
29
42
|
|
|
30
43
|
if TYPE_CHECKING:
|
|
31
|
-
from clawd_code_sdk.models.content_blocks import
|
|
32
|
-
ContentBlock,
|
|
33
|
-
)
|
|
44
|
+
from clawd_code_sdk.models.content_blocks import ContentBlock
|
|
34
45
|
|
|
35
46
|
|
|
36
47
|
# Message types
|
|
@@ -66,6 +77,16 @@ OverAgeDisabledReason = Literal[
|
|
|
66
77
|
"no_limits_configured",
|
|
67
78
|
"unknown",
|
|
68
79
|
]
|
|
80
|
+
_AnthropicStopReason = Literal[
|
|
81
|
+
"end_turn", "max_tokens", "stop_sequence", "tool_use", "pause_turn", "refusal"
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _coerce_stop_reason(value: str | None) -> _AnthropicStopReason | None:
|
|
86
|
+
"""Coerce a stored stop_reason string to the Anthropic SDK literal type."""
|
|
87
|
+
if value is not None and value in get_args(_AnthropicStopReason):
|
|
88
|
+
return value # type: ignore[return-value]
|
|
89
|
+
return None
|
|
69
90
|
|
|
70
91
|
|
|
71
92
|
class SDKSessionInfo(BaseModel):
|
|
@@ -313,25 +334,114 @@ class StreamEvent(BaseMessage):
|
|
|
313
334
|
"""Stream event for partial message updates during streaming."""
|
|
314
335
|
|
|
315
336
|
type: Literal["stream_event"] = "stream_event"
|
|
316
|
-
event:
|
|
337
|
+
event: BetaRawMessageStreamEvent
|
|
317
338
|
parent_tool_use_id: str | None = None
|
|
318
339
|
|
|
319
340
|
@classmethod
|
|
320
341
|
def block_stop(cls, *, index: int, session_id: str, uuid: str) -> StreamEvent:
|
|
321
342
|
"""Create a synthetic content_block_stop StreamEvent."""
|
|
322
|
-
from anthropic.types import
|
|
343
|
+
from anthropic.types.beta import BetaRawContentBlockStopEvent
|
|
323
344
|
|
|
324
|
-
stop_event =
|
|
345
|
+
stop_event = BetaRawContentBlockStopEvent(type="content_block_stop", index=index)
|
|
325
346
|
return StreamEvent(event=stop_event, session_id=session_id, uuid=uuid)
|
|
326
347
|
|
|
327
348
|
@classmethod
|
|
328
349
|
def message_stop(cls, *, session_id: str, uuid: str) -> StreamEvent:
|
|
329
350
|
"""Create a synthetic message_stop StreamEvent."""
|
|
330
|
-
from anthropic.types import
|
|
351
|
+
from anthropic.types.beta import BetaRawMessageStopEvent
|
|
331
352
|
|
|
332
|
-
stop_event =
|
|
353
|
+
stop_event = BetaRawMessageStopEvent(type="message_stop")
|
|
333
354
|
return StreamEvent(event=stop_event, session_id=session_id, uuid=uuid)
|
|
334
355
|
|
|
356
|
+
@classmethod
|
|
357
|
+
def block_start_text(cls, *, index: int, session_id: str, uuid: str) -> StreamEvent:
|
|
358
|
+
"""Create a synthetic content_block_start StreamEvent for a text block."""
|
|
359
|
+
content_block = ATextBlock(type="text", text="")
|
|
360
|
+
start_event = BetaRawContentBlockStartEvent(
|
|
361
|
+
type="content_block_start", index=index, content_block=content_block
|
|
362
|
+
)
|
|
363
|
+
return StreamEvent(event=start_event, session_id=session_id, uuid=uuid)
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
def block_start_thinking(cls, *, index: int, session_id: str, uuid: str) -> StreamEvent:
|
|
367
|
+
"""Create a synthetic content_block_start StreamEvent for a thinking block."""
|
|
368
|
+
content_block = AThinkingBlock(type="thinking", thinking="", signature="")
|
|
369
|
+
start_event = BetaRawContentBlockStartEvent(
|
|
370
|
+
type="content_block_start", index=index, content_block=content_block
|
|
371
|
+
)
|
|
372
|
+
return StreamEvent(event=start_event, session_id=session_id, uuid=uuid)
|
|
373
|
+
|
|
374
|
+
@classmethod
|
|
375
|
+
def block_start_tool_use(
|
|
376
|
+
cls, *, tool_use_id: str, name: str, index: int, session_id: str, uuid: str
|
|
377
|
+
) -> StreamEvent:
|
|
378
|
+
"""Create a synthetic content_block_start StreamEvent for a tool_use block."""
|
|
379
|
+
content_block = AToolUseBlock(type="tool_use", id=tool_use_id, name=name, input={})
|
|
380
|
+
start_event = BetaRawContentBlockStartEvent(
|
|
381
|
+
type="content_block_start", index=index, content_block=content_block
|
|
382
|
+
)
|
|
383
|
+
return StreamEvent(event=start_event, session_id=session_id, uuid=uuid)
|
|
384
|
+
|
|
385
|
+
@classmethod
|
|
386
|
+
def block_text_delta(cls, *, text: str, index: int, session_id: str, uuid: str) -> StreamEvent:
|
|
387
|
+
"""Create a synthetic content_block_delta StreamEvent with full block content."""
|
|
388
|
+
delta_event = BetaRawContentBlockDeltaEvent(
|
|
389
|
+
type="content_block_delta",
|
|
390
|
+
index=index,
|
|
391
|
+
delta=BetaTextDelta(type="text_delta", text=text),
|
|
392
|
+
)
|
|
393
|
+
return StreamEvent(event=delta_event, session_id=session_id, uuid=uuid)
|
|
394
|
+
|
|
395
|
+
@classmethod
|
|
396
|
+
def block_thinking_delta(
|
|
397
|
+
cls,
|
|
398
|
+
*,
|
|
399
|
+
thinking: str,
|
|
400
|
+
index: int,
|
|
401
|
+
session_id: str,
|
|
402
|
+
uuid: str,
|
|
403
|
+
) -> StreamEvent:
|
|
404
|
+
"""Create a synthetic content_block_delta StreamEvent with full block content."""
|
|
405
|
+
delta_event = BetaRawContentBlockDeltaEvent(
|
|
406
|
+
type="content_block_delta",
|
|
407
|
+
index=index,
|
|
408
|
+
delta=BetaThinkingDelta(type="thinking_delta", thinking=thinking),
|
|
409
|
+
)
|
|
410
|
+
return StreamEvent(event=delta_event, session_id=session_id, uuid=uuid)
|
|
411
|
+
|
|
412
|
+
@classmethod
|
|
413
|
+
def block_tool_json_delta(
|
|
414
|
+
cls,
|
|
415
|
+
*,
|
|
416
|
+
partial_json: str,
|
|
417
|
+
index: int,
|
|
418
|
+
session_id: str,
|
|
419
|
+
uuid: str,
|
|
420
|
+
) -> StreamEvent:
|
|
421
|
+
"""Create a synthetic content_block_delta StreamEvent with full block content."""
|
|
422
|
+
delta_event = BetaRawContentBlockDeltaEvent(
|
|
423
|
+
type="content_block_delta",
|
|
424
|
+
index=index,
|
|
425
|
+
delta=BetaInputJSONDelta(type="input_json_delta", partial_json=partial_json),
|
|
426
|
+
)
|
|
427
|
+
return StreamEvent(event=delta_event, session_id=session_id, uuid=uuid)
|
|
428
|
+
|
|
429
|
+
@classmethod
|
|
430
|
+
def message_delta(
|
|
431
|
+
cls,
|
|
432
|
+
*,
|
|
433
|
+
stop_reason: str | None,
|
|
434
|
+
session_id: str,
|
|
435
|
+
uuid: str,
|
|
436
|
+
) -> StreamEvent:
|
|
437
|
+
"""Create a synthetic message_delta StreamEvent."""
|
|
438
|
+
usage = BetaMessageDeltaUsage(output_tokens=0)
|
|
439
|
+
delta = BetaRawMessageDelta(
|
|
440
|
+
stop_reason=_coerce_stop_reason(stop_reason), stop_sequence=None
|
|
441
|
+
)
|
|
442
|
+
delta_event = BetaRawMessageDeltaEvent(type="message_delta", delta=delta, usage=usage)
|
|
443
|
+
return StreamEvent(event=delta_event, session_id=session_id, uuid=uuid)
|
|
444
|
+
|
|
335
445
|
|
|
336
446
|
class ToolProgressMessage(BaseMessage):
|
|
337
447
|
"""Progress update for a running tool."""
|
|
@@ -284,42 +284,3 @@ def extract_title(session_path: Path, max_chars: int = 60) -> str | None:
|
|
|
284
284
|
# walk(root)
|
|
285
285
|
|
|
286
286
|
# return result
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if __name__ == "__main__":
|
|
290
|
-
import sys
|
|
291
|
-
|
|
292
|
-
projects_dir = get_claude_projects_dir()
|
|
293
|
-
if not projects_dir.exists():
|
|
294
|
-
print(f"No projects directory found at {projects_dir}")
|
|
295
|
-
sys.exit(1)
|
|
296
|
-
|
|
297
|
-
total_entries = 0
|
|
298
|
-
total_sessions = 0
|
|
299
|
-
errors = 0
|
|
300
|
-
|
|
301
|
-
for project_dir in sorted(projects_dir.iterdir()):
|
|
302
|
-
if not project_dir.is_dir():
|
|
303
|
-
continue
|
|
304
|
-
session_files = sorted(
|
|
305
|
-
project_dir.glob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True
|
|
306
|
-
)
|
|
307
|
-
if not session_files:
|
|
308
|
-
continue
|
|
309
|
-
project_path = decode_project_path(project_dir.name)
|
|
310
|
-
print(f"\nProject: {project_path} ({len(session_files)} sessions)")
|
|
311
|
-
for session_file in session_files:
|
|
312
|
-
total_sessions += 1
|
|
313
|
-
try:
|
|
314
|
-
entries = read_session(session_file)
|
|
315
|
-
total_entries += len(entries)
|
|
316
|
-
title = extract_title(session_file) or "(no title)"
|
|
317
|
-
print(f" {session_file.stem}: {len(entries)} entries - {title}")
|
|
318
|
-
except Exception as e:
|
|
319
|
-
errors += 1
|
|
320
|
-
print(f" {session_file.stem}: ERROR - {e}")
|
|
321
|
-
|
|
322
|
-
print("\n--- Summary ---")
|
|
323
|
-
print(f"Sessions: {total_sessions}")
|
|
324
|
-
print(f"Total entries: {total_entries}")
|
|
325
|
-
print(f"Errors: {errors}")
|
|
@@ -16,12 +16,24 @@ See ARCHITECTURE.md for detailed documentation of the storage format.
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
from collections.abc import Sequence
|
|
19
|
-
from typing import Annotated, Any, Literal
|
|
19
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
|
20
20
|
|
|
21
21
|
from pydantic import BaseModel, Discriminator, Field, Tag
|
|
22
22
|
|
|
23
23
|
from clawd_code_sdk.models import ToolUseResult
|
|
24
24
|
from clawd_code_sdk.models.base import ClaudeCodeBaseModel, StopReason
|
|
25
|
+
from clawd_code_sdk.models.content_blocks import (
|
|
26
|
+
ContentBlock,
|
|
27
|
+
ImageBlock,
|
|
28
|
+
ImageSource,
|
|
29
|
+
TextBlock,
|
|
30
|
+
ToolResultBlock,
|
|
31
|
+
)
|
|
32
|
+
from clawd_code_sdk.models.messages import Usage
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from collections.abc import Iterable
|
|
25
37
|
|
|
26
38
|
|
|
27
39
|
# See https://github.com/daaain/claude-code-log/blob/main/claude_code_log/models.py
|
|
@@ -30,97 +42,33 @@ UserType = Literal["external", "internal"]
|
|
|
30
42
|
MCPToolCallStatus = Literal["started", "completed", "failed"]
|
|
31
43
|
|
|
32
44
|
|
|
33
|
-
class
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
type: Literal["text"]
|
|
37
|
-
text: str
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class ClaudeToolUseBlock(BaseModel):
|
|
41
|
-
"""Tool use content block."""
|
|
42
|
-
|
|
43
|
-
type: Literal["tool_use"]
|
|
44
|
-
id: str
|
|
45
|
-
name: str
|
|
46
|
-
input: dict[str, Any]
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class ClaudeToolResultBlock(BaseModel):
|
|
50
|
-
"""Tool result content block."""
|
|
51
|
-
|
|
52
|
-
type: Literal["tool_result"]
|
|
53
|
-
tool_use_id: str
|
|
54
|
-
content: list[dict[str, Any]] | str | None = None
|
|
55
|
-
is_error: bool | None = None
|
|
56
|
-
|
|
57
|
-
def extract_text(self) -> str:
|
|
58
|
-
"""Extract text content from this tool result."""
|
|
59
|
-
match self.content:
|
|
60
|
-
case None:
|
|
61
|
-
return ""
|
|
62
|
-
case str():
|
|
63
|
-
return self.content
|
|
64
|
-
case list():
|
|
65
|
-
text_parts = [
|
|
66
|
-
tc.get("text", "")
|
|
67
|
-
for tc in self.content
|
|
68
|
-
if isinstance(tc, dict) and tc.get("type") == "text"
|
|
69
|
-
]
|
|
70
|
-
return "\n".join(text_parts)
|
|
71
|
-
case _ as unreachable:
|
|
72
|
-
assert_never(unreachable)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class ClaudeThinkingBlock(BaseModel):
|
|
76
|
-
"""Thinking/reasoning content block."""
|
|
77
|
-
|
|
78
|
-
type: Literal["thinking"]
|
|
79
|
-
thinking: str
|
|
80
|
-
signature: str | None = None
|
|
45
|
+
class ClaudeUsage(Usage):
|
|
46
|
+
"""Token usage from Claude API response, with additional storage fields."""
|
|
81
47
|
|
|
82
|
-
|
|
83
|
-
class ClaudeImageSource(BaseModel):
|
|
84
|
-
"""Base64-encoded image source data."""
|
|
85
|
-
|
|
86
|
-
type: Literal["base64"]
|
|
87
|
-
media_type: str
|
|
88
|
-
data: str
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class ClaudeImageBlock(BaseModel):
|
|
92
|
-
"""Image content block."""
|
|
93
|
-
|
|
94
|
-
type: Literal["image"]
|
|
95
|
-
source: ClaudeImageSource
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
ClaudeContentBlock = Annotated[
|
|
99
|
-
ClaudeTextBlock
|
|
100
|
-
| ClaudeToolUseBlock
|
|
101
|
-
| ClaudeToolResultBlock
|
|
102
|
-
| ClaudeThinkingBlock
|
|
103
|
-
| ClaudeImageBlock,
|
|
104
|
-
Field(discriminator="type"),
|
|
105
|
-
]
|
|
106
|
-
"""Discriminated union of all content block types in message content arrays."""
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
class ClaudeUsage(BaseModel):
|
|
110
|
-
"""Token usage from Claude API response."""
|
|
111
|
-
|
|
112
|
-
input_tokens: int = 0
|
|
113
|
-
output_tokens: int = 0
|
|
114
|
-
cache_creation_input_tokens: int = 0
|
|
115
|
-
cache_read_input_tokens: int = 0
|
|
116
48
|
service_tier: str | None = None
|
|
117
49
|
server_tool_use: dict[str, Any] | None = None
|
|
118
50
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_entries(cls, entries: Iterable[ClaudeJSONLEntry]) -> ClaudeUsage:
|
|
53
|
+
"""Extract deduplicated aggregate token usage from stored entries.
|
|
54
|
+
|
|
55
|
+
Storage duplicates usage data across all content-block entries that
|
|
56
|
+
share the same API ``message.id``. This deduplicates by ``message.id``
|
|
57
|
+
and sums across all unique API calls.
|
|
58
|
+
"""
|
|
59
|
+
seen_ids: set[str] = set()
|
|
60
|
+
total = cls()
|
|
61
|
+
for entry in entries:
|
|
62
|
+
if not isinstance(entry, ClaudeAssistantEntry):
|
|
63
|
+
continue
|
|
64
|
+
msg = entry.message
|
|
65
|
+
if not isinstance(msg, ClaudeApiMessage):
|
|
66
|
+
continue
|
|
67
|
+
if msg.id in seen_ids:
|
|
68
|
+
continue
|
|
69
|
+
seen_ids.add(msg.id)
|
|
70
|
+
total.accumulate(msg.usage)
|
|
71
|
+
return total
|
|
124
72
|
|
|
125
73
|
|
|
126
74
|
# =============================================================================
|
|
@@ -135,7 +83,7 @@ class ClaudeApiMessage(BaseModel):
|
|
|
135
83
|
id: str
|
|
136
84
|
type: Literal["message"] = "message"
|
|
137
85
|
role: Literal["assistant"]
|
|
138
|
-
content: str | Sequence[
|
|
86
|
+
content: str | Sequence[ContentBlock]
|
|
139
87
|
stop_reason: StopReason | None = None
|
|
140
88
|
stop_sequence: str | None = None
|
|
141
89
|
usage: ClaudeUsage = Field(default_factory=ClaudeUsage)
|
|
@@ -145,7 +93,7 @@ class ClaudeUserMessage(BaseModel):
|
|
|
145
93
|
"""User message content."""
|
|
146
94
|
|
|
147
95
|
role: Literal["user"]
|
|
148
|
-
content: str | Sequence[
|
|
96
|
+
content: str | Sequence[ContentBlock]
|
|
149
97
|
usage: ClaudeUsage | None = None
|
|
150
98
|
"""Usage info (for type compatibility with ClaudeApiMessage)."""
|
|
151
99
|
|
|
@@ -197,6 +145,13 @@ class ClaudeUserEntry(ClaudeMessageEntryBase):
|
|
|
197
145
|
list[ToolUseResult | dict[str, Any]] | ToolUseResult | dict[str, Any] | str | None
|
|
198
146
|
) = None
|
|
199
147
|
|
|
148
|
+
@property
|
|
149
|
+
def is_tool_result(self) -> bool:
|
|
150
|
+
"""Whether this is a synthetic tool_result entry (vs. an actual user prompt)."""
|
|
151
|
+
if isinstance(self.message.content, str):
|
|
152
|
+
return False
|
|
153
|
+
return all(b.type == "tool_result" for b in self.message.content)
|
|
154
|
+
|
|
200
155
|
|
|
201
156
|
class ClaudeAssistantEntry(ClaudeMessageEntryBase):
|
|
202
157
|
"""Assistant message entry."""
|
|
@@ -217,12 +172,10 @@ class ClaudeDocumentContent(ClaudeCodeBaseModel):
|
|
|
217
172
|
"""Document content block."""
|
|
218
173
|
|
|
219
174
|
type: Literal["document"]
|
|
220
|
-
source:
|
|
175
|
+
source: ImageSource
|
|
221
176
|
|
|
222
177
|
|
|
223
|
-
ClaudeQueueContent =
|
|
224
|
-
str | ClaudeTextBlock | ClaudeImageBlock | ClaudeDocumentContent | ClaudeToolResultBlock
|
|
225
|
-
)
|
|
178
|
+
type ClaudeQueueContent = str | TextBlock | ImageBlock | ClaudeDocumentContent | ToolResultBlock
|
|
226
179
|
|
|
227
180
|
|
|
228
181
|
class ClaudeEnqueueOperation(ClaudeCodeBaseModel):
|