68290a9121
* refactor: Reduce continuation prompt token usage by 95 lines Removed redundant instructions from continuation prompt that were originally added to mitigate a session continuity issue. That issue has since been resolved, making these detailed instructions unnecessary on every continuation. Changes: - Reduced continuation prompt from ~106 lines to ~11 lines (~95 line reduction) - Changed "User's Goal:" to "Next Prompt in Session:" (more accurate framing) - Removed redundant WHAT TO RECORD, WHEN TO SKIP, and OUTPUT FORMAT sections - Kept concise reminder: "Continue generating observations and progress summaries..." - Initial prompt still contains all detailed instructions Impact: - Significant token savings on every continuation prompt - Faster context injection with no loss of functionality - Instructions remain comprehensive in initial prompt Files modified: - src/sdk/prompts.ts (buildContinuationPrompt function) - plugin/scripts/worker-service.cjs (compiled output) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: Enhance observation and summary prompts for clarity and token efficiency * Enhance prompt clarity and instructions in prompts.ts - Added a reminder to think about instructions before starting work. - Simplified the continuation prompt instruction by removing "for this ongoing session." * feat: Enhance settings.json with permissions and deny access to sensitive files refactor: Remove PLAN-full-observation-display.md and PR_SUMMARY.md as they are no longer needed chore: Delete SECURITY_SUMMARY.md since it is redundant after recent changes fix: Update worker-service.cjs to streamline observation generation instructions cleanup: Remove src-analysis.md and src-tree.md for a cleaner codebase refactor: Modify prompts.ts to clarify instructions for memory processing * refactor: Remove legacy worker service implementation * feat: Enhance summary hook to extract last assistant message and improve logging - Added function to extract the last assistant message from the transcript. - Updated summary hook to include last assistant message in the summary request. - Modified SDKSession interface to store last assistant message. - Adjusted buildSummaryPrompt to utilize last assistant message for generating summaries. - Updated worker service and session manager to handle last assistant message in summarize requests. - Introduced silentDebug utility for improved logging and diagnostics throughout the summary process. * docs: Add comprehensive implementation plan for ROI metrics feature Added detailed implementation plan covering: - Token usage capture from Agent SDK - Database schema changes (migration #8) - Discovery cost tracking per observation - Context hook display with ROI metrics - Testing and rollout strategy Timeline: ~20 hours over 4 days Goal: Empirical data for YC application amendment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: Add transcript processing scripts for analysis and formatting - Implemented `dump-transcript-readable.ts` to generate a readable markdown dump of transcripts, excluding certain entry types. - Created `extract-rich-context-examples.ts` to extract and showcase rich context examples from transcripts, highlighting user requests and assistant reasoning. - Developed `format-transcript-context.ts` to format transcript context into a structured markdown format for improved observation generation. - Added `test-transcript-parser.ts` for validating data extraction from transcript JSONL files, including statistics and error reporting. - Introduced `transcript-to-markdown.ts` for a complete representation of transcript data in markdown format, showing all context data. - Enhanced type definitions in `transcript.ts` to support new features and ensure type safety. - Built `transcript-parser.ts` to handle parsing of transcript JSONL files, including error handling and data extraction methods. * Refactor hooks and SDKAgent for improved observation handling - Updated `new-hook.ts` to clean user prompts by stripping leading slashes for better semantic clarity. - Enhanced `save-hook.ts` to include additional tools in the SKIP_TOOLS set, preventing unnecessary observations from certain command invocations. - Modified `prompts.ts` to change the structure of observation prompts, emphasizing the observational role and providing a detailed XML output format for observations. - Adjusted `SDKAgent.ts` to enforce stricter tool usage restrictions, ensuring the memory agent operates solely as an observer without any tool access. * feat: Enhance session initialization to accept user prompts and prompt numbers - Updated `handleSessionInit` in `worker-service.ts` to extract `userPrompt` and `promptNumber` from the request body and pass them to `initializeSession`. - Modified `initializeSession` in `SessionManager.ts` to handle optional `currentUserPrompt` and `promptNumber` parameters. - Added logic to update the existing session's `userPrompt` and `lastPromptNumber` if a `currentUserPrompt` is provided. - Implemented debug logging for session initialization and updates to track user prompts and prompt numbers. --------- Co-authored-by: Claude <noreply@anthropic.com>
424 lines
14 KiB
Python
424 lines
14 KiB
Python
"""Pydantic models for Claude Code transcript JSON structures.
|
|
|
|
Enhanced to leverage official Anthropic types where beneficial.
|
|
"""
|
|
|
|
from typing import Any, List, Union, Optional, Dict, Literal, cast
|
|
from pydantic import BaseModel
|
|
|
|
from anthropic.types import Message as AnthropicMessage
|
|
from anthropic.types import StopReason
|
|
from anthropic.types import Usage as AnthropicUsage
|
|
from anthropic.types.content_block import ContentBlock
|
|
|
|
|
|
class TodoItem(BaseModel):
|
|
id: str
|
|
content: str
|
|
status: Literal["pending", "in_progress", "completed"]
|
|
priority: Literal["high", "medium", "low"]
|
|
|
|
|
|
class UsageInfo(BaseModel):
|
|
"""Token usage information that extends Anthropic's Usage type to handle optional fields."""
|
|
|
|
input_tokens: Optional[int] = None
|
|
cache_creation_input_tokens: Optional[int] = None
|
|
cache_read_input_tokens: Optional[int] = None
|
|
output_tokens: Optional[int] = None
|
|
service_tier: Optional[str] = None
|
|
server_tool_use: Optional[Dict[str, Any]] = None
|
|
|
|
def to_anthropic_usage(self) -> Optional[AnthropicUsage]:
|
|
"""Convert to Anthropic Usage type if both required fields are present."""
|
|
if self.input_tokens is not None and self.output_tokens is not None:
|
|
return AnthropicUsage(
|
|
input_tokens=self.input_tokens,
|
|
output_tokens=self.output_tokens,
|
|
cache_creation_input_tokens=self.cache_creation_input_tokens,
|
|
cache_read_input_tokens=self.cache_read_input_tokens,
|
|
service_tier=self.service_tier, # type: ignore
|
|
server_tool_use=self.server_tool_use, # type: ignore
|
|
)
|
|
return None
|
|
|
|
@classmethod
|
|
def from_anthropic_usage(cls, usage: AnthropicUsage) -> "UsageInfo":
|
|
"""Create UsageInfo from Anthropic Usage."""
|
|
return cls(
|
|
input_tokens=usage.input_tokens,
|
|
output_tokens=usage.output_tokens,
|
|
cache_creation_input_tokens=usage.cache_creation_input_tokens,
|
|
cache_read_input_tokens=usage.cache_read_input_tokens,
|
|
service_tier=usage.service_tier,
|
|
server_tool_use=usage.server_tool_use.model_dump()
|
|
if usage.server_tool_use
|
|
else None,
|
|
)
|
|
|
|
|
|
class TextContent(BaseModel):
|
|
type: Literal["text"]
|
|
text: str
|
|
|
|
|
|
class ToolUseContent(BaseModel):
|
|
type: Literal["tool_use"]
|
|
id: str
|
|
name: str
|
|
input: Dict[str, Any]
|
|
|
|
|
|
class ToolResultContent(BaseModel):
|
|
type: Literal["tool_result"]
|
|
tool_use_id: str
|
|
content: Union[str, List[Dict[str, Any]]]
|
|
is_error: Optional[bool] = None
|
|
|
|
|
|
class ThinkingContent(BaseModel):
|
|
type: Literal["thinking"]
|
|
thinking: str
|
|
signature: Optional[str] = None
|
|
|
|
|
|
class ImageSource(BaseModel):
|
|
type: Literal["base64"]
|
|
media_type: str
|
|
data: str
|
|
|
|
|
|
class ImageContent(BaseModel):
|
|
type: Literal["image"]
|
|
source: ImageSource
|
|
|
|
|
|
# Enhanced ContentItem to include official Anthropic ContentBlock types
|
|
ContentItem = Union[
|
|
TextContent,
|
|
ToolUseContent,
|
|
ToolResultContent,
|
|
ThinkingContent,
|
|
ImageContent,
|
|
ContentBlock, # Official Anthropic content block types
|
|
]
|
|
|
|
|
|
class UserMessage(BaseModel):
|
|
role: Literal["user"]
|
|
content: Union[str, List[ContentItem]]
|
|
|
|
|
|
class AssistantMessage(BaseModel):
|
|
"""Assistant message model compatible with Anthropic's Message type."""
|
|
|
|
id: str
|
|
type: Literal["message"]
|
|
role: Literal["assistant"]
|
|
model: str
|
|
content: List[ContentItem]
|
|
stop_reason: Optional[StopReason] = None
|
|
stop_sequence: Optional[str] = None
|
|
usage: Optional[UsageInfo] = None
|
|
|
|
@classmethod
|
|
def from_anthropic_message(
|
|
cls, anthropic_msg: AnthropicMessage
|
|
) -> "AssistantMessage":
|
|
"""Create AssistantMessage from official Anthropic Message."""
|
|
# Convert Anthropic Message to our format, preserving official types where possible
|
|
return cls(
|
|
id=anthropic_msg.id,
|
|
type=anthropic_msg.type,
|
|
role=anthropic_msg.role,
|
|
model=anthropic_msg.model,
|
|
content=list(
|
|
anthropic_msg.content
|
|
), # Convert to list for ContentItem compatibility
|
|
stop_reason=anthropic_msg.stop_reason,
|
|
stop_sequence=anthropic_msg.stop_sequence,
|
|
usage=normalize_usage_info(anthropic_msg.usage),
|
|
)
|
|
|
|
|
|
class FileInfo(BaseModel):
|
|
filePath: str
|
|
content: str
|
|
numLines: int
|
|
startLine: int
|
|
totalLines: int
|
|
|
|
|
|
class FileReadResult(BaseModel):
|
|
type: Literal["text"]
|
|
file: FileInfo
|
|
|
|
|
|
class CommandResult(BaseModel):
|
|
stdout: str
|
|
stderr: str
|
|
interrupted: bool
|
|
isImage: bool
|
|
|
|
|
|
class TodoResult(BaseModel):
|
|
oldTodos: List[TodoItem]
|
|
newTodos: List[TodoItem]
|
|
|
|
|
|
class EditResult(BaseModel):
|
|
oldString: Optional[str] = None
|
|
newString: Optional[str] = None
|
|
replaceAll: Optional[bool] = None
|
|
originalFile: Optional[str] = None
|
|
structuredPatch: Optional[Any] = None
|
|
userModified: Optional[bool] = None
|
|
|
|
|
|
ToolUseResult = Union[
|
|
str,
|
|
List[TodoItem],
|
|
FileReadResult,
|
|
CommandResult,
|
|
TodoResult,
|
|
EditResult,
|
|
List[ContentItem],
|
|
]
|
|
|
|
|
|
class BaseTranscriptEntry(BaseModel):
|
|
parentUuid: Optional[str]
|
|
isSidechain: bool
|
|
userType: str
|
|
cwd: str
|
|
sessionId: str
|
|
version: str
|
|
uuid: str
|
|
timestamp: str
|
|
isMeta: Optional[bool] = None
|
|
|
|
|
|
class UserTranscriptEntry(BaseTranscriptEntry):
|
|
type: Literal["user"]
|
|
message: UserMessage
|
|
toolUseResult: Optional[ToolUseResult] = None
|
|
|
|
|
|
class AssistantTranscriptEntry(BaseTranscriptEntry):
|
|
type: Literal["assistant"]
|
|
message: AssistantMessage
|
|
requestId: Optional[str] = None
|
|
|
|
|
|
class SummaryTranscriptEntry(BaseModel):
|
|
type: Literal["summary"]
|
|
summary: str
|
|
leafUuid: str
|
|
cwd: Optional[str] = None
|
|
|
|
|
|
class SystemTranscriptEntry(BaseTranscriptEntry):
|
|
"""System messages like warnings, notifications, etc."""
|
|
|
|
type: Literal["system"]
|
|
content: str
|
|
level: Optional[str] = None # e.g., "warning", "info", "error"
|
|
|
|
|
|
class QueueOperationTranscriptEntry(BaseModel):
|
|
"""Queue operations (enqueue/dequeue) for message queueing tracking.
|
|
|
|
These are internal operations that track when messages are queued and dequeued.
|
|
They are parsed but not rendered, as the content duplicates actual user messages.
|
|
"""
|
|
|
|
type: Literal["queue-operation"]
|
|
operation: Literal["enqueue", "dequeue"]
|
|
timestamp: str
|
|
sessionId: str
|
|
content: Optional[List[ContentItem]] = None # Only present for enqueue operations
|
|
|
|
|
|
TranscriptEntry = Union[
|
|
UserTranscriptEntry,
|
|
AssistantTranscriptEntry,
|
|
SummaryTranscriptEntry,
|
|
SystemTranscriptEntry,
|
|
QueueOperationTranscriptEntry,
|
|
]
|
|
|
|
|
|
def normalize_usage_info(usage_data: Any) -> Optional[UsageInfo]:
|
|
"""Normalize usage data to be compatible with both custom and Anthropic formats."""
|
|
if usage_data is None:
|
|
return None
|
|
|
|
# If it's already a UsageInfo instance, return as-is
|
|
if isinstance(usage_data, UsageInfo):
|
|
return usage_data
|
|
|
|
# If it's an Anthropic Usage instance, convert using our method
|
|
if isinstance(usage_data, AnthropicUsage):
|
|
return UsageInfo.from_anthropic_usage(usage_data)
|
|
|
|
# If it has the shape of an Anthropic Usage, try to construct it first
|
|
if hasattr(usage_data, "input_tokens") and hasattr(usage_data, "output_tokens"):
|
|
try:
|
|
# Try to create an Anthropic Usage first
|
|
anthropic_usage = AnthropicUsage.model_validate(usage_data)
|
|
return UsageInfo.from_anthropic_usage(anthropic_usage)
|
|
except Exception:
|
|
# Fall back to direct conversion
|
|
return UsageInfo(
|
|
input_tokens=getattr(usage_data, "input_tokens", None),
|
|
cache_creation_input_tokens=getattr(
|
|
usage_data, "cache_creation_input_tokens", None
|
|
),
|
|
cache_read_input_tokens=getattr(
|
|
usage_data, "cache_read_input_tokens", None
|
|
),
|
|
output_tokens=getattr(usage_data, "output_tokens", None),
|
|
service_tier=getattr(usage_data, "service_tier", None),
|
|
server_tool_use=getattr(usage_data, "server_tool_use", None),
|
|
)
|
|
|
|
# If it's a dict, validate and convert to our format
|
|
if isinstance(usage_data, dict):
|
|
return UsageInfo.model_validate(usage_data)
|
|
|
|
return None
|
|
|
|
|
|
def parse_content_item(item_data: Dict[str, Any]) -> ContentItem:
|
|
"""Parse a content item using enhanced approach with Anthropic types."""
|
|
try:
|
|
content_type = item_data.get("type", "")
|
|
|
|
# Try official Anthropic types first for better future compatibility
|
|
if content_type == "text":
|
|
try:
|
|
from anthropic.types.text_block import TextBlock
|
|
|
|
return TextBlock.model_validate(item_data)
|
|
except Exception:
|
|
return TextContent.model_validate(item_data)
|
|
elif content_type == "tool_use":
|
|
try:
|
|
from anthropic.types.tool_use_block import ToolUseBlock
|
|
|
|
return ToolUseBlock.model_validate(item_data)
|
|
except Exception:
|
|
return ToolUseContent.model_validate(item_data)
|
|
elif content_type == "thinking":
|
|
try:
|
|
from anthropic.types.thinking_block import ThinkingBlock
|
|
|
|
return ThinkingBlock.model_validate(item_data)
|
|
except Exception:
|
|
return ThinkingContent.model_validate(item_data)
|
|
elif content_type == "tool_result":
|
|
return ToolResultContent.model_validate(item_data)
|
|
elif content_type == "image":
|
|
return ImageContent.model_validate(item_data)
|
|
else:
|
|
# Fallback to text content for unknown types
|
|
return TextContent(type="text", text=str(item_data))
|
|
except Exception:
|
|
return TextContent(type="text", text=str(item_data))
|
|
|
|
|
|
def parse_message_content(content_data: Any) -> Union[str, List[ContentItem]]:
|
|
"""Parse message content, handling both string and list formats."""
|
|
if isinstance(content_data, str):
|
|
return content_data
|
|
elif isinstance(content_data, list):
|
|
content_list = cast(List[Dict[str, Any]], content_data)
|
|
return [parse_content_item(item) for item in content_list]
|
|
else:
|
|
return str(content_data)
|
|
|
|
|
|
def parse_transcript_entry(data: Dict[str, Any]) -> TranscriptEntry:
|
|
"""
|
|
Parse a JSON dictionary into the appropriate TranscriptEntry type.
|
|
|
|
Enhanced to optionally use official Anthropic types for assistant messages.
|
|
|
|
Args:
|
|
data: Dictionary parsed from JSON
|
|
|
|
Returns:
|
|
The appropriate TranscriptEntry subclass
|
|
|
|
Raises:
|
|
ValueError: If the data doesn't match any known transcript entry type
|
|
"""
|
|
entry_type = data.get("type")
|
|
|
|
if entry_type == "user":
|
|
# Parse message content if present
|
|
data_copy = data.copy()
|
|
if "message" in data_copy and "content" in data_copy["message"]:
|
|
data_copy["message"] = data_copy["message"].copy()
|
|
data_copy["message"]["content"] = parse_message_content(
|
|
data_copy["message"]["content"]
|
|
)
|
|
# Parse toolUseResult if present and it's a list of content items
|
|
if "toolUseResult" in data_copy and isinstance(
|
|
data_copy["toolUseResult"], list
|
|
):
|
|
# Check if it's a list of content items (MCP tool results)
|
|
tool_use_result = cast(List[Any], data_copy["toolUseResult"])
|
|
if (
|
|
tool_use_result
|
|
and isinstance(tool_use_result[0], dict)
|
|
and "type" in tool_use_result[0]
|
|
):
|
|
data_copy["toolUseResult"] = [
|
|
parse_content_item(cast(Dict[str, Any], item))
|
|
for item in tool_use_result
|
|
if isinstance(item, dict)
|
|
]
|
|
return UserTranscriptEntry.model_validate(data_copy)
|
|
|
|
elif entry_type == "assistant":
|
|
# Enhanced assistant message parsing with optional Anthropic types
|
|
data_copy = data.copy()
|
|
|
|
# Validate compatibility with official Anthropic Message type
|
|
if "message" in data_copy:
|
|
try:
|
|
message_data = data_copy["message"]
|
|
AnthropicMessage.model_validate(message_data)
|
|
# Successfully validated - our data is compatible with official Anthropic types
|
|
except Exception:
|
|
# Validation failed - continue with standard parsing
|
|
pass
|
|
|
|
# Standard parsing path (works for all cases)
|
|
if "message" in data_copy and "content" in data_copy["message"]:
|
|
message_copy = data_copy["message"].copy()
|
|
message_copy["content"] = parse_message_content(message_copy["content"])
|
|
|
|
# Normalize usage data to support both Anthropic and custom formats
|
|
if "usage" in message_copy:
|
|
message_copy["usage"] = normalize_usage_info(message_copy["usage"])
|
|
|
|
data_copy["message"] = message_copy
|
|
return AssistantTranscriptEntry.model_validate(data_copy)
|
|
|
|
elif entry_type == "summary":
|
|
return SummaryTranscriptEntry.model_validate(data)
|
|
|
|
elif entry_type == "system":
|
|
return SystemTranscriptEntry.model_validate(data)
|
|
|
|
elif entry_type == "queue-operation":
|
|
# Parse content if present (only in enqueue operations)
|
|
data_copy = data.copy()
|
|
if "content" in data_copy and isinstance(data_copy["content"], list):
|
|
data_copy["content"] = parse_message_content(data_copy["content"])
|
|
return QueueOperationTranscriptEntry.model_validate(data_copy)
|
|
|
|
else:
|
|
raise ValueError(f"Unknown transcript entry type: {entry_type}") |