update: Reasoningコンテキスト管理のレビュー・対応
This commit is contained in:
@@ -868,6 +868,88 @@ mod tests {
|
||||
assert!(collect_assistant_thinking_parts(&req).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thinking_part_lands_in_assistant_role_message() {
|
||||
// wire 構造の position 検証: thinking part は assistant role の
|
||||
// message 配列に並ぶ(user role には絶対に入らない)。
|
||||
let scheme = AnthropicScheme::new();
|
||||
let request = Request::new()
|
||||
.user("question?")
|
||||
.item(Item::reasoning("thinking inside").with_signature("SIG-A"))
|
||||
.item(Item::tool_call("c1", "tool_a", "{}"))
|
||||
.item(Item::tool_result("c1", "result"))
|
||||
.user("follow up");
|
||||
let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
|
||||
|
||||
// 全 thinking part が assistant role の message に存在すること
|
||||
let mut thinking_msg_indices = Vec::new();
|
||||
for (i, msg) in req.messages.iter().enumerate() {
|
||||
if let AnthropicContent::Parts(parts) = &msg.content {
|
||||
if parts
|
||||
.iter()
|
||||
.any(|p| matches!(p, AnthropicContentPart::Thinking { .. }))
|
||||
{
|
||||
assert_eq!(
|
||||
msg.role, "assistant",
|
||||
"thinking part must be in assistant role, got {} at msg {}",
|
||||
msg.role, i,
|
||||
);
|
||||
thinking_msg_indices.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
!thinking_msg_indices.is_empty(),
|
||||
"expected at least one thinking part in messages: {:?}",
|
||||
req.messages,
|
||||
);
|
||||
|
||||
// thinking part を含む assistant message は、それに続く tool_use を含む
|
||||
// assistant message より前 (= 先頭側) に位置すること
|
||||
// (Anthropic 仕様: 同一論理ターン内で thinking → tool_use の順)
|
||||
let mut tool_use_msg_indices = Vec::new();
|
||||
for (i, msg) in req.messages.iter().enumerate() {
|
||||
if let AnthropicContent::Parts(parts) = &msg.content {
|
||||
if parts
|
||||
.iter()
|
||||
.any(|p| matches!(p, AnthropicContentPart::ToolUse { .. }))
|
||||
{
|
||||
tool_use_msg_indices.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(!tool_use_msg_indices.is_empty(), "expected tool_use part");
|
||||
let first_thinking = thinking_msg_indices[0];
|
||||
let first_tool_use = tool_use_msg_indices[0];
|
||||
assert!(
|
||||
first_thinking <= first_tool_use,
|
||||
"thinking msg ({}) must precede tool_use msg ({})",
|
||||
first_thinking,
|
||||
first_tool_use,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redacted_thinking_part_lands_in_assistant_role_message() {
|
||||
// RedactedThinking も同様に assistant role に置かれること。
|
||||
let scheme = AnthropicScheme::new();
|
||||
let request = Request::new()
|
||||
.user("ask")
|
||||
.item(Item::reasoning("").with_encrypted_content("opaque"))
|
||||
.item(Item::tool_call("c1", "tool_a", "{}"))
|
||||
.item(Item::tool_result("c1", "ok"));
|
||||
let req = scheme.build_request("claude-sonnet-4-20250514", &request, &cap_explicit());
|
||||
for msg in &req.messages {
|
||||
if let AnthropicContent::Parts(parts) = &msg.content {
|
||||
for part in parts {
|
||||
if matches!(part, AnthropicContentPart::RedactedThinking { .. }) {
|
||||
assert_eq!(msg.role, "assistant");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_definitions_carry_no_cache_control() {
|
||||
// Tool JSON schema must serialise unchanged — no sneak-in of
|
||||
|
||||
@@ -286,6 +286,48 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_reasoning_preserves_signature() {
|
||||
// 新世代 Claude の thinking signature が history.json に永続化され、
|
||||
// resume 後の Item::Reasoning に復元されること。
|
||||
let original = Item::reasoning("inner thought").with_signature("SIG-OPUS-XYZ");
|
||||
let logged: LoggedItem = (&original).into();
|
||||
let json = serde_json::to_string(&logged).unwrap();
|
||||
// wire 形式に signature キーが乗ること(古い形式との互換のため
|
||||
// 値が None のときは省略される。Some の値は載る)
|
||||
assert!(
|
||||
json.contains("SIG-OPUS-XYZ"),
|
||||
"serialised JSON must carry signature: {json}",
|
||||
);
|
||||
let parsed: LoggedItem = serde_json::from_str(&json).unwrap();
|
||||
match Item::from(parsed) {
|
||||
Item::Reasoning {
|
||||
text, signature, ..
|
||||
} => {
|
||||
assert_eq!(text, "inner thought");
|
||||
assert_eq!(signature.as_deref(), Some("SIG-OPUS-XYZ"));
|
||||
}
|
||||
other => panic!("unexpected variant: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_reasoning_without_signature_field_deserializes() {
|
||||
// signature フィールドが無い旧形式の history.json を読み込んでも
|
||||
// None としてロードできる(後方互換性)。
|
||||
let legacy_json = r#"{"kind":"reasoning","text":"old","summary":[],"encrypted_content":null}"#;
|
||||
let parsed: LoggedItem = serde_json::from_str(legacy_json).unwrap();
|
||||
match Item::from(parsed) {
|
||||
Item::Reasoning {
|
||||
text, signature, ..
|
||||
} => {
|
||||
assert_eq!(text, "old");
|
||||
assert!(signature.is_none());
|
||||
}
|
||||
other => panic!("unexpected variant: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_tool_result_with_content() {
|
||||
let original = Item::tool_result_with_content("call_1", "ok", "full output");
|
||||
|
||||
Reference in New Issue
Block a user