Skip to main content

artifactize_core/llm/claude_agent/
wire.rs

1//! The lines Claude Code writes in `stream-json` mode, parsed once into types, and the lines
2//! artifactize writes back. Fields artifactize does not use are ignored; an unknown line or
3//! block type is `Other`, since Claude Code adds new ones between versions.
4
5use serde::{Deserialize, Serialize};
6use serde_json::{Value, json};
7
8use crate::types::{ClaudeMessageId, ClaudeRequestId, ClaudeToolUseId};
9
10/// One line from Claude Code.
11#[derive(Debug, Deserialize)]
12#[serde(tag = "type", rename_all = "snake_case")]
13pub enum Line {
14    /// Claude Code asks the host something: here, a message for the host's MCP server.
15    ControlRequest {
16        request_id: ClaudeRequestId,
17        request: ControlRequest,
18    },
19    /// The answer to a request the host sent: here, `initialize`.
20    ControlResponse {
21        response: ControlResponse,
22    },
23    System(System),
24    Assistant(Assistant),
25    User(User),
26    StreamEvent {
27        event: StreamEvent,
28    },
29    RateLimitEvent {
30        rate_limit_info: RateLimit,
31    },
32    Result(Finished),
33    #[serde(other)]
34    Other,
35}
36
37#[derive(Debug, Deserialize)]
38#[serde(tag = "subtype", rename_all = "snake_case")]
39pub enum ControlRequest {
40    McpMessage {
41        server_name: String,
42        message: Rpc,
43    },
44    #[serde(other)]
45    Other,
46}
47
48/// A JSON-RPC message for the host's MCP server: a request the server answers, or a
49/// notification (no `id`).
50#[derive(Debug, Deserialize)]
51#[serde(untagged)]
52pub enum Rpc {
53    Known(McpRequest),
54    Unknown {
55        #[serde(default)]
56        id: Option<RpcId>,
57        method: String,
58    },
59}
60
61#[derive(Debug, Deserialize)]
62#[serde(tag = "method")]
63pub enum McpRequest {
64    #[serde(rename = "initialize")]
65    Initialize { id: RpcId, params: InitializeParams },
66    #[serde(rename = "tools/list")]
67    ToolsList { id: RpcId },
68    #[serde(rename = "tools/call")]
69    ToolsCall { id: RpcId, params: CallParams },
70    #[serde(rename = "ping")]
71    Ping { id: RpcId },
72}
73
74/// A JSON-RPC id: a number or a string, echoed back unchanged.
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(untagged)]
77pub enum RpcId {
78    Number(i64),
79    Text(String),
80}
81
82#[derive(Debug, Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct InitializeParams {
85    pub protocol_version: String,
86}
87
88#[derive(Debug, Deserialize)]
89pub struct CallParams {
90    pub name: String,
91    #[serde(default)]
92    pub arguments: Option<Value>,
93}
94
95#[derive(Debug, Deserialize)]
96#[serde(tag = "subtype", rename_all = "snake_case")]
97pub enum ControlResponse {
98    Success {
99        request_id: ClaudeRequestId,
100        #[serde(default)]
101        response: Option<Initialized>,
102    },
103    Error {
104        request_id: ClaudeRequestId,
105        #[serde(default)]
106        error: String,
107    },
108}
109
110/// What `initialize` answers: the session's account, Claude Code's version and the models
111/// it offers. No message has been sent, so no tokens were used.
112#[derive(Debug, Default, Deserialize)]
113pub struct Initialized {
114    #[serde(default)]
115    pub models: Vec<Model>,
116    #[serde(default)]
117    pub account: Option<Account>,
118    #[serde(default)]
119    pub claude_code_version: Option<String>,
120}
121
122#[derive(Debug, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct Model {
125    /// What `--model` takes: an alias such as `opus`, or a model id.
126    pub value: String,
127    /// The model id the value runs.
128    #[serde(default)]
129    pub resolved_model: Option<String>,
130    #[serde(default)]
131    pub display_name: Option<String>,
132}
133
134#[derive(Debug, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct Account {
137    #[serde(default)]
138    pub subscription_type: Option<String>,
139    #[serde(default)]
140    pub api_provider: Option<String>,
141}
142
143#[derive(Debug, Deserialize)]
144#[serde(tag = "subtype", rename_all = "snake_case")]
145pub enum System {
146    /// The session started, with the model it runs.
147    Init {
148        #[serde(default)]
149        model: Option<String>,
150    },
151    #[serde(other)]
152    Other,
153}
154
155#[derive(Debug, Deserialize)]
156pub struct Assistant {
157    pub message: AssistantMessage,
158    #[serde(default)]
159    pub error: Option<AssistantError>,
160    #[serde(default)]
161    pub parent_tool_use_id: Option<String>,
162}
163
164/// Why Claude Code could not answer: it writes the reason as a synthetic assistant message.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
166#[serde(rename_all = "snake_case")]
167pub enum AssistantError {
168    AuthenticationFailed,
169    BillingError,
170    RateLimit,
171    InvalidRequest,
172    ServerError,
173    #[serde(other)]
174    Unknown,
175}
176
177#[derive(Debug, Deserialize)]
178pub struct AssistantMessage {
179    /// Absent, or empty, on the synthetic message of an error.
180    #[serde(default, deserialize_with = "nonempty")]
181    pub id: Option<ClaudeMessageId>,
182    #[serde(default)]
183    pub model: String,
184    #[serde(default)]
185    pub content: Vec<Block>,
186    #[serde(default)]
187    pub usage: Option<Usage>,
188}
189
190#[derive(Debug, Clone, PartialEq, Deserialize)]
191#[serde(tag = "type", rename_all = "snake_case")]
192pub enum Block {
193    Text {
194        text: String,
195    },
196    Thinking {
197        #[serde(default)]
198        thinking: String,
199        #[serde(default)]
200        signature: String,
201    },
202    ToolUse {
203        id: ClaudeToolUseId,
204        name: String,
205        #[serde(default)]
206        input: Value,
207    },
208    #[serde(other)]
209    Other,
210}
211
212#[derive(Debug, Deserialize)]
213pub struct User {
214    pub message: UserMessage,
215    #[serde(default)]
216    pub parent_tool_use_id: Option<String>,
217}
218
219#[derive(Debug, Deserialize)]
220pub struct UserMessage {
221    pub content: UserContent,
222}
223
224#[derive(Debug, Deserialize)]
225#[serde(untagged)]
226pub enum UserContent {
227    Text(String),
228    Blocks(Vec<UserBlock>),
229}
230
231#[derive(Debug, Deserialize)]
232#[serde(tag = "type", rename_all = "snake_case")]
233pub enum UserBlock {
234    ToolResult {
235        tool_use_id: ClaudeToolUseId,
236        #[serde(default)]
237        content: ResultContent,
238        #[serde(default)]
239        is_error: bool,
240    },
241    #[serde(other)]
242    Other,
243}
244
245#[derive(Debug, Deserialize)]
246#[serde(untagged)]
247pub enum ResultContent {
248    Text(String),
249    Blocks(Vec<ResultBlock>),
250}
251
252impl Default for ResultContent {
253    fn default() -> Self {
254        Self::Text(String::new())
255    }
256}
257
258#[derive(Debug, Deserialize)]
259#[serde(tag = "type", rename_all = "snake_case")]
260pub enum ResultBlock {
261    Text {
262        text: String,
263    },
264    Image {
265        source: ImageSource,
266    },
267    #[serde(other)]
268    Other,
269}
270
271#[derive(Debug, Deserialize)]
272pub struct ImageSource {
273    pub media_type: String,
274    pub data: String,
275}
276
277/// The Messages API's streaming events, which Claude Code passes on.
278#[derive(Debug, Deserialize)]
279#[serde(tag = "type", rename_all = "snake_case")]
280pub enum StreamEvent {
281    MessageStart {
282        message: StartedMessage,
283    },
284    ContentBlockStart {
285        index: usize,
286        content_block: Block,
287    },
288    ContentBlockDelta {
289        index: usize,
290        delta: Delta,
291    },
292    ContentBlockStop {
293        index: usize,
294    },
295    MessageDelta {
296        #[serde(default)]
297        usage: Option<Usage>,
298    },
299    MessageStop,
300    #[serde(other)]
301    Other,
302}
303
304#[derive(Debug, Deserialize)]
305pub struct StartedMessage {
306    pub id: ClaudeMessageId,
307    #[serde(default)]
308    pub model: String,
309    #[serde(default)]
310    pub usage: Option<Usage>,
311}
312
313#[derive(Debug, Deserialize)]
314#[serde(tag = "type", rename_all = "snake_case")]
315pub enum Delta {
316    TextDelta {
317        text: String,
318    },
319    ThinkingDelta {
320        thinking: String,
321    },
322    #[serde(other)]
323    Other,
324}
325
326/// One response's token counters, as the Messages API reports them.
327#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
328pub struct Usage {
329    #[serde(default)]
330    pub input_tokens: Option<u64>,
331    #[serde(default)]
332    pub output_tokens: Option<u64>,
333    #[serde(default)]
334    pub cache_read_input_tokens: Option<u64>,
335    #[serde(default)]
336    pub cache_creation_input_tokens: Option<u64>,
337    #[serde(default)]
338    pub cache_creation: Option<CacheCreation>,
339    #[serde(default)]
340    pub output_tokens_details: Option<OutputDetails>,
341}
342
343#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
344pub struct CacheCreation {
345    #[serde(default)]
346    pub ephemeral_1h_input_tokens: Option<u64>,
347}
348
349#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
350pub struct OutputDetails {
351    #[serde(default)]
352    pub thinking_tokens: Option<u64>,
353}
354
355#[derive(Debug, Clone, Deserialize)]
356#[serde(rename_all = "camelCase")]
357pub struct RateLimit {
358    pub status: LimitStatus,
359    /// When the window resets; Claude Code writes Unix seconds.
360    #[serde(default, deserialize_with = "unix_time")]
361    pub resets_at: Option<time::OffsetDateTime>,
362    /// The window: `five_hour`, `seven_day` and others.
363    #[serde(default)]
364    pub rate_limit_type: Option<String>,
365}
366
367#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
368#[serde(rename_all = "snake_case")]
369pub enum LimitStatus {
370    Allowed,
371    AllowedWarning,
372    Rejected,
373    #[serde(other)]
374    Other,
375}
376
377/// The end of one user turn: the answer, or why there is none.
378#[derive(Debug)]
379pub enum Finished {
380    /// The final answer's structured output, when a schema asked for one.
381    Structured(Value),
382    /// The final answer's text.
383    Text(String),
384    Failed {
385        ending: Ending,
386        errors: Vec<String>,
387    },
388}
389
390/// Why a turn ended without an answer.
391#[derive(Debug, Clone, PartialEq, Eq)]
392pub enum Ending {
393    /// Claude Code gave up getting an answer that matches the schema.
394    NoStructuredOutput,
395    /// Any other ending, by Claude Code's name for it.
396    Other(String),
397}
398
399/// The success subtype; every other one is an ending without an answer.
400const SUCCESS: &str = "success";
401/// The subtype of a turn whose answer never matched the schema.
402const MAX_STRUCTURED_OUTPUT_RETRIES: &str = "error_max_structured_output_retries";
403
404impl<'de> Deserialize<'de> for Finished {
405    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
406        #[derive(Deserialize)]
407        struct Raw {
408            #[serde(default)]
409            subtype: String,
410            #[serde(default)]
411            is_error: bool,
412            #[serde(default)]
413            result: Option<String>,
414            #[serde(default)]
415            structured_output: Option<Value>,
416            #[serde(default)]
417            errors: Vec<String>,
418        }
419        let raw = Raw::deserialize(deserializer)?;
420        Ok(match raw {
421            Raw {
422                ref subtype,
423                is_error: false,
424                ..
425            } if subtype == SUCCESS => match raw.structured_output {
426                Some(output) => Self::Structured(output),
427                None => Self::Text(raw.result.unwrap_or_default()),
428            },
429            Raw {
430                subtype, errors, ..
431            } => Self::Failed {
432                ending: if subtype == MAX_STRUCTURED_OUTPUT_RETRIES {
433                    Ending::NoStructuredOutput
434                } else {
435                    Ending::Other(subtype)
436                },
437                errors,
438            },
439        })
440    }
441}
442
443/// An id that may be absent or empty: `None` then.
444fn nonempty<'de, D: serde::Deserializer<'de>>(
445    deserializer: D,
446) -> Result<Option<ClaudeMessageId>, D::Error> {
447    Option::<String>::deserialize(deserializer)?
448        .filter(|id| !id.is_empty())
449        .map(|id| id.parse().map_err(serde::de::Error::custom))
450        .transpose()
451}
452
453/// Unix seconds as a time; a time out of range is unknown.
454fn unix_time<'de, D: serde::Deserializer<'de>>(
455    deserializer: D,
456) -> Result<Option<time::OffsetDateTime>, D::Error> {
457    Ok(Option::<i64>::deserialize(deserializer)?
458        .and_then(|seconds| time::OffsetDateTime::from_unix_timestamp(seconds).ok()))
459}
460
461/// The id of the one control request artifactize sends.
462pub const INITIALIZE: &str = "initialize";
463
464/// `initialize`, naming the MCP server artifactize hosts in this process.
465pub fn initialize(server: Option<&str>) -> Value {
466    json!({
467        "type": "control_request",
468        "request_id": INITIALIZE,
469        "request": {"subtype": "initialize", "sdkMcpServers": server.into_iter().collect::<Vec<_>>()},
470    })
471}
472
473/// A user message sent verbatim: Claude Code adds none of its per-turn context to it.
474pub fn user(text: &str) -> Value {
475    json!({
476        "type": "user",
477        "session_id": "",
478        "message": {"role": "user", "content": [{"type": "text", "text": text}]},
479        "parent_tool_use_id": null,
480        "client_composed": true,
481    })
482}
483
484/// The answer to an MCP control request: the server's JSON-RPC `reply`.
485pub fn mcp_reply(request_id: &ClaudeRequestId, reply: Value) -> Value {
486    json!({
487        "type": "control_response",
488        "response": {"subtype": "success", "request_id": request_id, "response": {"mcp_response": reply}},
489    })
490}
491
492/// The refusal of a control request artifactize does not serve.
493pub fn refusal(request_id: &ClaudeRequestId, error: &str) -> Value {
494    json!({
495        "type": "control_response",
496        "response": {"subtype": "error", "request_id": request_id, "error": error},
497    })
498}