feat: add synthesize_turn_messages
This commit is contained in:
@@ -36,6 +36,7 @@ from claude_code_api.events import (
|
||||
ThinkingBlock,
|
||||
ToolResultBlock,
|
||||
ToolUseBlock,
|
||||
UserMessage,
|
||||
)
|
||||
from claude_code_api.injection import (
|
||||
build_concat_prompt,
|
||||
@@ -51,10 +52,6 @@ HistoryInjectionMode = Literal["native_jsonl", "concat_message"]
|
||||
|
||||
ParseErrorCallback = Callable[[MessageParseError, dict[str, Any]], None]
|
||||
|
||||
_TERMINAL_STOP_REASONS: frozenset[str] = frozenset(
|
||||
{"end_turn", "max_tokens", "stop_sequence", "refusal"}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BackendOptions:
|
||||
@@ -196,8 +193,8 @@ class ClaudeCodeBackend:
|
||||
await session.aclose()
|
||||
raise
|
||||
|
||||
synthetic_asst = _synthesize_assistant_dict(events)
|
||||
new_history = [*list(messages), synthetic_asst]
|
||||
synthesized_cycle = synthesize_turn_messages(events)
|
||||
new_history = [*list(messages), *synthesized_cycle]
|
||||
self._sessions[hash_history(new_history)] = session
|
||||
|
||||
async def aclose(self) -> None:
|
||||
@@ -351,22 +348,43 @@ def _user_text_payload(content: Any) -> str:
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def _synthesize_assistant_dict(events: Iterable[Event]) -> dict[str, Any]:
|
||||
"""Render the terminal assistant message in Anthropic Messages format."""
|
||||
terminal: AssistantMessage | None = None
|
||||
for ev in reversed(list(events)):
|
||||
if (
|
||||
isinstance(ev, AssistantMessage)
|
||||
and ev.stop_reason in _TERMINAL_STOP_REASONS
|
||||
):
|
||||
terminal = ev
|
||||
break
|
||||
if terminal is None:
|
||||
return {"role": "assistant", "content": []}
|
||||
return {
|
||||
"role": "assistant",
|
||||
"content": [_block_to_dict(b) for b in terminal.content],
|
||||
}
|
||||
def synthesize_turn_messages(events: Iterable[Event]) -> list[dict[str, Any]]:
|
||||
"""Render a turn's full assistant↔tool cycle as Anthropic-shape messages.
|
||||
|
||||
A single ``complete()`` call can produce multiple ``AssistantMessage``
|
||||
records (each tool-use cycle is its own record, terminated by a
|
||||
``UserMessage`` carrying the matching ``tool_result`` blocks). We
|
||||
fold that whole sequence into a list of canonical messages — exactly
|
||||
what the Anthropic Messages API would see if claude were running
|
||||
over the wire instead of in a PTY. The result is what the session
|
||||
fingerprint is computed over and what gets seeded into JSONL on a
|
||||
cache-miss re-spawn, so the live PTY and a freshly-resumed one stay
|
||||
semantically equivalent.
|
||||
|
||||
Excludes intermediate ``UserMessage`` records that carry only the
|
||||
echoed prompt text (string content) — those are claude's own input
|
||||
record, not part of the conversational reply. Only tool_result
|
||||
``UserMessage`` records (list-of-blocks content) survive.
|
||||
"""
|
||||
out: list[dict[str, Any]] = []
|
||||
for ev in events:
|
||||
if isinstance(ev, AssistantMessage):
|
||||
out.append(
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [_block_to_dict(b) for b in ev.content],
|
||||
}
|
||||
)
|
||||
elif isinstance(ev, UserMessage):
|
||||
content = ev.content
|
||||
if isinstance(content, list) and content:
|
||||
out.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": [_block_to_dict(b) for b in content],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _block_to_dict(block: ContentBlock) -> dict[str, Any]:
|
||||
@@ -401,4 +419,5 @@ __all__ = [
|
||||
"ClaudeCodeBackend",
|
||||
"HistoryInjectionMode",
|
||||
"ParseErrorCallback",
|
||||
"synthesize_turn_messages",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user