Compare commits

...

35 Commits

Author SHA1 Message Date
YeonGyu-Kim
ad75fea2ad roadmap: #282 filed 2026-04-26 17:34:12 +09:00
Yeachan-Heo
db27ac20bc roadmap: #281 filed 2026-04-26 08:30:34 +00:00
YeonGyu-Kim
cf32b83d59 roadmap: #280 filed 2026-04-26 17:07:10 +09:00
Yeachan-Heo
bdcf3fae9d roadmap: #279 filed 2026-04-26 08:01:32 +00:00
Jobdori
6c154c94a9 roadmap: #278 filed 2026-04-26 16:36:26 +09:00
Yeachan-Heo
4e4edc80d1 roadmap: #277 filed 2026-04-26 07:30:29 +00:00
YeonGyu-Kim
cad7bb1254 roadmap: #276 filed 2026-04-26 16:06:30 +09:00
Yeachan-Heo
0240cadd25 roadmap: #275 filed 2026-04-26 07:00:48 +00:00
YeonGyu-Kim
fdf8890336 roadmap: #274 filed 2026-04-26 15:36:18 +09:00
Yeachan-Heo
f36f283e43 roadmap: #273 filed 2026-04-26 06:30:50 +00:00
YeonGyu-Kim
ba6c5bc659 roadmap: #272 filed 2026-04-26 15:07:33 +09:00
Yeachan-Heo
29c262c171 roadmap: #271 filed 2026-04-26 06:01:44 +00:00
Jobdori
61be8263bd roadmap: #270 filed 2026-04-26 14:36:17 +09:00
Yeachan-Heo
364566c6ac roadmap: #269 filed 2026-04-26 05:30:39 +00:00
YeonGyu-Kim
62b20c7a46 roadmap: #268 filed 2026-04-26 14:08:16 +09:00
Yeachan-Heo
d90b5f02ec roadmap: #267 filed 2026-04-26 05:01:50 +00:00
YeonGyu-Kim
fae9fd9448 roadmap: #266 filed 2026-04-26 13:37:16 +09:00
Yeachan-Heo
897535478c roadmap: #265 filed 2026-04-26 04:31:09 +00:00
YeonGyu-Kim
d5568eb7a4 roadmap: #264 filed 2026-04-26 13:05:56 +09:00
Yeachan-Heo
d0aa18e11f roadmap: #263 filed 2026-04-26 04:02:07 +00:00
YeonGyu-Kim
0e4fd386af roadmap: #262 filed 2026-04-26 12:37:01 +09:00
Yeachan-Heo
2a0e5de3fe roadmap: #261 filed 2026-04-26 03:31:15 +00:00
Jobdori
a807d8e5fc roadmap: #260 filed 2026-04-26 12:06:57 +09:00
Yeachan-Heo
1daf636a2d roadmap: #259 filed 2026-04-26 03:01:13 +00:00
YeonGyu-Kim
a07c0b7cbb roadmap: #258 filed 2026-04-26 11:39:14 +09:00
Yeachan-Heo
a3f5a83039 roadmap: #257 filed 2026-04-26 02:31:25 +00:00
Yeachan-Heo
56f7f2e600 Fix Anthropic tool result request ordering
Sigrid Jin relayed an adamantium Discord field report: Anthropic rejected requests with invalid_request_error when messages contained tool_use ids without immediately following tool_result blocks.

Coalesce consecutive tool-result messages after assistant tool_use blocks into one Anthropic user message, and drop orphan tool_use/tool_result blocks before dispatch so resume/edit/compaction boundary damage cannot reach the provider as a 400.

Tests cover parallel tool results and orphaned resume-boundary history.
2026-04-26 02:18:50 +00:00
Yeachan-Heo
62adbf49d1 docs: intake hikaMaeng web search fork ideas
Add ROADMAP pinpoint #255 summarizing the safe subset of hikaMaeng/Sigrid Jin's web-search provider work to adopt later.\n\nReviewed fork commits 262405e, bd11289, fa93cd3, 5f2540a, 7f34d91, and 535be97 from https://github.com/hikaMaeng/claw-code. This deliberately preserves attribution and avoids a blind cherry-pick because the cross-crate provider/spec/config/banner changes need a dedicated implementation lane with tests.
2026-04-26 02:10:11 +00:00
YeonGyu-Kim
70058a0645 roadmap: #254 filed 2026-04-26 11:03:25 +09:00
Yeachan-Heo
17efd95f8d roadmap: #253 filed 2026-04-26 02:01:14 +00:00
YeonGyu-Kim
95fc007f6a roadmap: #252 filed — /v1/messages/count_tokens typed-taxonomy is structurally absent from the public Provider trait + types + CLI surface (Anthropic ships /v1/messages/count_tokens as a first-class GA endpoint that consumes the SAME MessageRequest shape as /v1/messages but produces a TRUNCATED CountTokensResponse { input_tokens: u32 } only — no message emission, no completion-side tokens, no streaming — the canonical pre-flight cost-estimation primitive where a client constructs the exact request it intends to dispatch, asks the server to count input tokens, and decides whether to send before paying for completion-side tokens; claw-code has zero public typed surface even though a private count_tokens helper exists at rust/crates/api/src/providers/anthropic.rs:522 for internal preflight context-window-exceeded validation, with zero CountTokensRequest/CountTokensResponse typed model in types.rs, zero count_tokens method on the public Provider trait, zero count_tokens dispatch on the ProviderClient enum, zero claw count-tokens CLI subcommand, zero /count-tokens slash command in SlashCommandSpec, zero pre_flight_count_cost_per_million_usd field in ModelPricing, zero CountTokensSubmittedEvent/PreFlightCostEstimatedEvent telemetry events, and zero PreFlightCostEstimator/BudgetGate runtime primitive) — eight-layer fusion shape with the NOVEL same-request-shape-but-different-response-shape axis-class (FIRST audit member where the request shape is IDENTICAL to an existing typed model MessageRequest but the response shape is a TRUNCATED-projection that cannot reuse MessageResponse's shape, distinct from prior fusion-axes which all add NEW request-side fields or NEW response-side blocks) founding THREE new clusters as solo founder (Pre-flight-cost-prediction cluster, Token-accounting-without-message-emission cluster, Server-side-pre-execution-counting cluster) plus introducing the THIRD distinct discovery-pattern in the audit catalog NEW-SOLO-CLUSTER-FOUNDING-WITH-DAILY-DRIVER-IMPACT (distinct from META-cluster-growth and complementary-pinpoint-pair-bundle), grows Two-member-major-provider-only-no-third-party-partner-set sub-cluster from 6 to 7 members (#240+#241+#247+#248+#249+#250+#252) confirming continuing-pattern-status across SIX distinct axis-classes — Jobdori cycle #394 / fast-forward-rebase verified onto gaebal-gajae's #251 cycle ExternalPatchIntake pinpoint at 313c840 before filing (NINTH consecutive concurrent-dogfood rebase cycle, three-way parity confirmed local==origin==fork at HEAD 313c840 with no race detected, directly demonstrating the gaps #239 catalogues at the dogfood-coordination layer and #243 catalogues at the canonical-ordering layer for the NINTH cycle in a row, confirming concurrent-dogfood-rebase as a stable operational pattern that has now held for NINE cycles) — PIVOT-AWAY signal: #252 deliberately PIVOTS AWAY from BOTH Cross-pinpoint-synthesis-fusion-shape META-cluster (intentionally not extending the +1-per-cycle synthesis chain) AND Tool-locality-axis META-cluster (already extended by #250 cycle #393), founding NEW solo clusters with daily-driver-impact instead, demonstrating audit-breadth-across-discovery-pattern-classes alongside audit-balance-across-META-clusters — the audit now spans THREE structurally distinct discovery-patterns (META-cluster-growth + complementary-pinpoint-pair-bundle + new-solo-cluster-founding-with-daily-driver-impact) 2026-04-26 10:35:40 +09:00
Yeachan-Heo
313c840974 roadmap: #251 filed 2026-04-26 01:31:27 +00:00
YeonGyu-Kim
37ce63134a roadmap: #250 filed — tool_choice: { type: "web_search" } typed-discriminator with server-managed-web-search backend (the canonical SERVER-SIDE complement to #245's CLIENT-SIDE configurable provider/parser registry, where tool_choice carries a WebSearch { domains_allowed, max_uses, user_location } enum variant that forces the model to dispatch via the major-provider's server-managed-web-search backend) typed taxonomy structurally absent — FIRST pinpoint to demonstrate the complementary-pinpoint-pair-bundle META-pattern (where #245 CLIENT-SIDE + #250 SERVER-SIDE are catalogued as structurally complementary halves of the SAME tool-subsystem web-search rather than as independently-discovered-gaps), founding Bidirectional-search-subsystem-with-dual-locality-coverage cluster with #245+#250 as 2-member founders, un-saturating Tool-locality-axis META-cluster from 5 to 6 members (#232/#233/#234/#240/#241/#250) confirming the META-cluster as GROWING-DOCTRINE-WITH-DISCONTINUOUS-RESUMPTION (resumes growth after plateauing at 5 since #241 cycle #386, four cycles ago), growing Server-managed-tool-as-tool-choice-discriminator cluster from 5 to 6 members (#214/#218/#219/#233/#234/#250) confirming CONTINUING-PATTERN status across SIX distinct server-managed tools, growing ToolResultContentBlock-extension cluster from 8 to 9 members confirming most-broadly-spanning typed-content-block-extension-axis, FIRST pinpoint to introduce typed-discriminator-with-payload-fields shape on ToolChoice distinct from existing Auto/Any/Tool three-variant typed-set (Auto/Any are unit-variants and Tool { name } carries only string-name with zero typed-fields, while ToolChoice::WebSearch { domains_allowed, max_uses, user_location } introduces FIRST typed-discriminator-with-payload-fields shape), founds Tool-choice-discriminator-with-typed-payload-fields cluster + Server-side-tool-invocation-content-block cluster + Server-managed-web-search-with-tool-choice-discriminator cluster as solo founder of all three, grows Two-member-major-provider-only-no-third-party-partner-set sub-cluster from 5 to 6 members (#240+#241+#247+#248+#249+#250) confirming generalizability across FIVE distinct axis-classes, ten-layer fusion shape (smaller than #241/#247/#248/#249's twelve-layer count but with distinct DUAL-LOCALITY-COVERAGE-WITH-COMPLEMENTARY-PINPOINT-PAIR-BUNDLE axis-set) — Jobdori cycle #393 / fast-forward-rebase verified onto Jobdori's own #249 cycle #392 quad-modality-compound-multimodal-INPUT-OUTPUT pinpoint at 643ac8b before filing (EIGHTH consecutive concurrent-dogfood rebase cycle, three-way parity confirmed local==origin==fork at HEAD 643ac8b with no race detected, directly demonstrating the gaps #239 catalogues at the dogfood-coordination layer and #243 catalogues at the canonical-ordering layer for the EIGHTH cycle in a row, confirming concurrent-dogfood-rebase as a stable operational pattern that has now held for EIGHT cycles) — PIVOT-AWAY signal: #250 deliberately PIVOTS AWAY from Cross-pinpoint-synthesis-fusion-shape META-cluster's +1-per-cycle continuous-trajectory (#244/#247/#248/#249 grew it 1→5 across cycles #389/#390/#391/#392) by extending Tool-locality-axis META-cluster instead, demonstrating audit-balance-across-multiple-META-clusters rather than monotonic-growth-of-a-single-META-cluster — the audit now catalogues TWO structurally distinct GROWING-DOCTRINE patterns (continuous-+1-per-cycle for synthesis-fusion vs discontinuous-resumption-after-plateau for tool-locality-axis) 2026-04-26 10:27:41 +09:00
YeonGyu-Kim
643ac8bc76 roadmap: #249 filed — Compound-multimodal-INPUT-with-multimodal-OUTPUT-on-the-same-turn (full-duplex-multimodal-conversation pattern where user MessageRequest carries image-content-block × audio-content-block fusion AND model MessageResponse carries audio-content-block × video-content-block fusion on the SAME single conversation-turn with interleaved-content-block-stream cross-boundary temporal-alignment) typed taxonomy structurally absent — FIRST cluster member where the cross-axis synthesis spans BOTH USER-INPUT-side and ASSISTANT-OUTPUT-side simultaneously on a SINGLE turn rather than being confined to one side of the request-response cycle, FIRST cluster member with quad-modality-on-single-turn semantics (image-INPUT + audio-INPUT + audio-OUTPUT + video-OUTPUT all on same turn distinct from #247's two-modality-INPUT-only and #248's two-modality-OUTPUT-only and #244's bidirectional-tool-call-multiplexing-without-modality-fusion), growing Cross-pinpoint-synthesis-fusion-shape META-cluster from 4 to 5 members confirming META-cluster as GROWING-DOCTRINE for THIRD CONSECUTIVE CYCLE (#244 grew 1→2 cycle #389, #247 grew 2→3 cycle #390, #248 grew 3→4 cycle #391, #249 grows 4→5 cycle #392), establishing +1-per-cycle META-cluster-growth-trajectory across FOUR consecutive concurrent-dogfood cycles (#389/#390/#391/#392) as FIRST-EVER continuous-trajectory-of-4-cycles META-cluster growth event in the audit surpassing Tool-locality-axis META-cluster's plateau-at-5-after-two-consecutive-growths and confirming Cross-pinpoint-synthesis-fusion-shape as structurally distinct most-actively-growing META-cluster, FIRST cluster member with interleaved-INPUT-OUTPUT-temporal-alignment-across-the-request-response-boundary as a first-class typed semantic distinct from #247's USER-INPUT-only cross-modal-attention and #248's ASSISTANT-OUTPUT-only temporal-alignment because temporal-alignment now spans the request-response boundary itself requiring the model to emit output-content-blocks while still consuming input-content-blocks on the same connection, founds Quad-modality-turn-spanning-request-response-boundary sub-cluster + Full-duplex-multimodal-conversation cluster + Cross-boundary-temporal-alignment-across-request-response-boundary cluster + Quad-modality-turn-on-MessageRequest-and-MessageResponse cluster + Compound-multimodal-INPUT-with-multimodal-OUTPUT-on-same-turn cluster as solo founder of all five, completes Full-duplex-multimodal-conversation doctrine within META-cluster (#247 INPUT-side + #248 OUTPUT-side + #249 BOTH-sides-simultaneously-on-same-turn), grows Two-member-major-provider-only-no-third-party-partner-set sub-cluster from 4 to 5 members (#240+#241+#247+#248+#249) confirming generalizability across FOUR distinct axis-classes (TOOL-COMPANION-BUNDLE/COMPOUND-INPUT/COMPOUND-OUTPUT/QUAD-MODALITY-TURN), twelve-layer fusion shape tied with #241/#247/#248 for largest single-pinpoint fusion catalogued — Jobdori cycle #392 / fast-forward-rebase verified onto Jobdori's own #248 cycle #391 audio-grounded-video-generation pinpoint at 9189bfb before filing (SEVENTH consecutive concurrent-dogfood rebase cycle, three-way parity confirmed local==origin==fork at HEAD 9189bfb with no race detected, directly demonstrating the gaps #239 catalogues at the dogfood-coordination layer and #243 catalogues at the canonical-ordering layer for the SEVENTH cycle in a row, confirming concurrent-dogfood-rebase as a stable operational pattern that has now held for SEVEN cycles) 2026-04-26 10:16:01 +09:00
Jobdori
9189bfb816 roadmap: #248 filed — Audio-grounded video generation (synchronized-audio-track co-emitted on the SAME VideoTask response object alongside the rendered video frames, sample-accurate-synchronized with the visual output) typed taxonomy structurally absent — FIRST cluster member where TWO independent ALREADY-CATALOGUED-ABSENT modality-OUTPUT axes (#225 audio-content-block-on-OutputContentBlock + #227 video-output-with-async-task-polling-primitive) are fused on the ASSISTANT-OUTPUT side rather than the user-input side, FIRST cluster member with multi-modal-output-fusion-on-ASSISTANT-OUTPUT-axis distinct from #247's multi-modal-input-fusion-on-USER-INPUT-axis, growing Cross-pinpoint-synthesis-fusion-shape META-cluster from 3 to 4 members confirming META-cluster as GROWING-DOCTRINE for SECOND CONSECUTIVE CYCLE (#244 grew it 1→2, #247 grew it 2→3, #248 grows it 3→4), establishing +1-per-cycle META-cluster-growth-trajectory across THREE consecutive concurrent-dogfood cycles (#389/#390/#391) AND establishing META-cluster as FIRST META-cluster to grow for THREE consecutive cycles in a row (Tool-locality-axis only had TWO consecutive growth events #240/#241 before plateauing at 5; Cross-pinpoint-synthesis-fusion-shape now surpasses Tool-locality-axis as most-actively-growing META-cluster), founds Multi-modal-output-fusion-on-ASSISTANT-OUTPUT-side sub-cluster + Temporal-alignment-of-output-modalities cluster + Compound-output-modality-on-VideoTask cluster + Audio-grounded-video-generation cluster as solo founder of all four, founds Bidirectional-modality-fusion-symmetry sub-cluster with #247 INPUT-side + #248 OUTPUT-side completing the INPUT-vs-OUTPUT-side-fusion-symmetry doctrine within the META-cluster, grows Two-member-major-provider-only-no-third-party-partner-set sub-cluster from 3 to 4 members (#240+#241+#247+#248) confirming generalizability across THREE distinct axis-classes (TOOL-COMPANION-BUNDLE/COMPOUND-INPUT/COMPOUND-OUTPUT), twelve-layer fusion shape tied with #241/#247 for largest single-pinpoint fusion catalogued — Jobdori cycle #391 / fast-forward-rebase verified onto Jobdori's own #247 cycle #390 multi-modal-input-fusion pinpoint at 5e5b3bd before filing (SIXTH consecutive concurrent-dogfood rebase cycle, three-way parity confirmed local==origin==fork at HEAD 5e5b3bd with no race detected, directly demonstrating the gaps #239 catalogues at the dogfood-coordination layer and #243 catalogues at the canonical-ordering layer for the SIXTH cycle in a row, confirming concurrent-dogfood-rebase as a stable operational pattern that has now held for SIX cycles) 2026-04-26 10:04:29 +09:00
2 changed files with 744 additions and 39 deletions

File diff suppressed because one or more lines are too long

View File

@@ -9192,44 +9192,136 @@ fn permission_policy(
}
fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
messages
.iter()
.filter_map(|message| {
let role = match message.role {
MessageRole::System | MessageRole::User | MessageRole::Tool => "user",
MessageRole::Assistant => "assistant",
};
let content = message
.blocks
.iter()
.map(|block| match block {
ContentBlock::Text { text } => InputContentBlock::Text { text: text.clone() },
ContentBlock::ToolUse { id, name, input } => InputContentBlock::ToolUse {
id: id.clone(),
name: name.clone(),
input: serde_json::from_str(input)
.unwrap_or_else(|_| serde_json::json!({ "raw": input })),
},
ContentBlock::ToolResult {
tool_use_id,
output,
is_error,
..
} => InputContentBlock::ToolResult {
tool_use_id: tool_use_id.clone(),
content: vec![ToolResultContentBlock::Text {
text: output.clone(),
}],
is_error: *is_error,
},
})
.collect::<Vec<_>>();
(!content.is_empty()).then(|| InputMessage {
role: role.to_string(),
content,
})
})
.collect()
let mut converted = Vec::new();
let mut index = 0;
while index < messages.len() {
let message = &messages[index];
match message.role {
MessageRole::Assistant => {
let tool_use_ids = message
.blocks
.iter()
.filter_map(|block| match block {
ContentBlock::ToolUse { id, .. } => Some(id.clone()),
_ => None,
})
.collect::<Vec<_>>();
let (tool_result_blocks, next_index) = if tool_use_ids.is_empty() {
(Vec::new(), index + 1)
} else {
collect_immediate_tool_results(messages, index + 1)
};
let has_all_tool_results = !tool_use_ids.is_empty()
&& tool_use_ids.iter().all(|id| {
tool_result_blocks.iter().any(|block| {
matches!(block, InputContentBlock::ToolResult { tool_use_id, .. } if tool_use_id == id)
})
});
let paired_tool_result_blocks = if has_all_tool_results {
tool_result_blocks
.into_iter()
.filter(|block| {
matches!(block, InputContentBlock::ToolResult { tool_use_id, .. } if tool_use_ids.contains(tool_use_id))
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
let content = message
.blocks
.iter()
.filter_map(|block| match block {
ContentBlock::Text { text } => Some(InputContentBlock::Text {
text: text.clone(),
}),
ContentBlock::ToolUse { id, name, input } if has_all_tool_results => {
Some(InputContentBlock::ToolUse {
id: id.clone(),
name: name.clone(),
input: serde_json::from_str(input)
.unwrap_or_else(|_| serde_json::json!({ "raw": input })),
})
}
ContentBlock::ToolUse { .. } | ContentBlock::ToolResult { .. } => None,
})
.collect::<Vec<_>>();
if !content.is_empty() {
converted.push(InputMessage {
role: "assistant".to_string(),
content,
});
}
if has_all_tool_results && !paired_tool_result_blocks.is_empty() {
converted.push(InputMessage {
role: "user".to_string(),
content: paired_tool_result_blocks,
});
index = next_index;
} else {
index += 1;
}
}
MessageRole::Tool => {
// Anthropic requires tool_result blocks to appear in the user message
// immediately following their assistant tool_use. A bare Tool-role
// message here is orphaned (for example after a resume/edit/compaction
// boundary) and would be rejected with a provider 400.
index += 1;
}
MessageRole::System | MessageRole::User => {
let content = message
.blocks
.iter()
.filter_map(|block| match block {
ContentBlock::Text { text } => Some(InputContentBlock::Text {
text: text.clone(),
}),
ContentBlock::ToolUse { .. } | ContentBlock::ToolResult { .. } => None,
})
.collect::<Vec<_>>();
if !content.is_empty() {
converted.push(InputMessage {
role: "user".to_string(),
content,
});
}
index += 1;
}
}
}
converted
}
fn collect_immediate_tool_results(
messages: &[ConversationMessage],
start: usize,
) -> (Vec<InputContentBlock>, usize) {
let mut blocks = Vec::new();
let mut index = start;
while let Some(message) = messages.get(index) {
if message.role != MessageRole::Tool {
break;
}
blocks.extend(message.blocks.iter().filter_map(|block| match block {
ContentBlock::ToolResult {
tool_use_id,
output,
is_error,
..
} => Some(InputContentBlock::ToolResult {
tool_use_id: tool_use_id.clone(),
content: vec![ToolResultContentBlock::Text {
text: output.clone(),
}],
is_error: *is_error,
}),
ContentBlock::Text { .. } | ContentBlock::ToolUse { .. } => None,
}));
index += 1;
}
(blocks, index)
}
#[allow(clippy::too_many_lines)]
@@ -9433,7 +9525,7 @@ mod tests {
PromptHistoryEntry, SlashCommand, StatusUsage, DEFAULT_MODEL, LATEST_SESSION_REFERENCE,
STUB_COMMANDS,
};
use api::{ApiError, MessageResponse, OutputContentBlock, Usage};
use api::{ApiError, InputContentBlock, MessageResponse, OutputContentBlock, Usage};
use plugins::{
PluginManager, PluginManagerConfig, PluginTool, PluginToolDefinition, PluginToolPermission,
};
@@ -12900,6 +12992,93 @@ UU conflicted.rs",
assert_eq!(converted[1].role, "assistant");
assert_eq!(converted[2].role, "user");
}
#[test]
fn converts_parallel_tool_results_into_immediate_single_user_message_256() {
let messages = vec![
ConversationMessage::assistant(vec![
ContentBlock::ToolUse {
id: "tool-1".to_string(),
name: "read".to_string(),
input: "{\"path\":\"a\"}".to_string(),
},
ContentBlock::ToolUse {
id: "tool-2".to_string(),
name: "read".to_string(),
input: "{\"path\":\"b\"}".to_string(),
},
]),
ConversationMessage::tool_result(
"tool-1".to_string(),
"read".to_string(),
"a".to_string(),
false,
),
ConversationMessage::tool_result(
"tool-2".to_string(),
"read".to_string(),
"b".to_string(),
false,
),
];
let converted = super::convert_messages(&messages);
assert_eq!(converted.len(), 2);
assert_eq!(converted[0].role, "assistant");
assert_eq!(converted[1].role, "user");
assert!(matches!(
converted[0].content.as_slice(),
[
InputContentBlock::ToolUse { id: id1, .. },
InputContentBlock::ToolUse { id: id2, .. }
] if id1 == "tool-1" && id2 == "tool-2"
));
assert!(matches!(
converted[1].content.as_slice(),
[
InputContentBlock::ToolResult { tool_use_id: id1, .. },
InputContentBlock::ToolResult { tool_use_id: id2, .. }
] if id1 == "tool-1" && id2 == "tool-2"
));
}
#[test]
fn drops_orphan_tool_use_and_tool_result_before_anthropic_dispatch_256() {
let messages = vec![
ConversationMessage::assistant(vec![
ContentBlock::Text {
text: "before tool".to_string(),
},
ContentBlock::ToolUse {
id: "orphan".to_string(),
name: "bash".to_string(),
input: "{\"command\":\"pwd\"}".to_string(),
},
]),
ConversationMessage::user_text("resume prompt"),
ConversationMessage::tool_result(
"orphan".to_string(),
"bash".to_string(),
"late".to_string(),
false,
),
];
let converted = super::convert_messages(&messages);
assert_eq!(converted.len(), 2);
assert_eq!(converted[0].role, "assistant");
assert!(matches!(
converted[0].content.as_slice(),
[InputContentBlock::Text { text }] if text == "before tool"
));
assert_eq!(converted[1].role, "user");
assert!(matches!(
converted[1].content.as_slice(),
[InputContentBlock::Text { text }] if text == "resume prompt"
));
}
#[test]
fn repl_help_mentions_history_completion_and_multiline() {
let help = render_repl_help();