fix: REPL display, /compact panic, identity leak, DeepSeek reasoning, thinking blocks

Five interrelated fixes from parallel Hephaestus sessions:

1. fix(repl): display assistant text after spinner (#2981, #2982, #2937)
   - Added final_assistant_text() call after run_turn spinner completes
   - REPL now shows response text like run_prompt_json does

2. fix(compact): handle Thinking content blocks (#2985)
   - Added ContentBlock::Thinking variant throughout compact summarizer
   - Prevents panic when /compact encounters thinking blocks

3. fix(prompt): provider-aware model identity (#2822)
   - New ModelFamilyIdentity enum (Claude vs Generic)
   - Non-Anthropic models no longer say 'I am Claude'
   - model_family_identity_for() detects provider and sets identity

4. fix(openai): preserve DeepSeek reasoning_content (#2821)
   - Stream parser now captures reasoning_content from OpenAI-compat
   - Emits ThinkingDelta/SignatureDelta events for reasoning models
   - Thinking blocks included in conversation history for re-send

5. feat(runtime): Thinking block support across codebase
   - AssistantEvent::Thinking variant in conversation.rs
   - ContentBlock::Thinking in session serialization
   - Thinking-aware compact summarization
   - Tests for thinking block ordering and content

Closes #2981, #2982, #2937, #2985, #2822, #2821
This commit is contained in:
YeonGyu-Kim
2026-05-06 15:32:34 +09:00
parent 553d25ee50
commit 75c08bc982
15 changed files with 1099 additions and 75 deletions

View File

@@ -30,6 +30,10 @@ pub enum ContentBlock {
Text {
text: String,
},
Thinking {
thinking: String,
signature: Option<String>,
},
ToolUse {
id: String,
name: String,
@@ -737,6 +741,22 @@ impl ContentBlock {
object.insert("type".to_string(), JsonValue::String("text".to_string()));
object.insert("text".to_string(), JsonValue::String(text.clone()));
}
Self::Thinking {
thinking,
signature,
} => {
object.insert(
"type".to_string(),
JsonValue::String("thinking".to_string()),
);
object.insert("thinking".to_string(), JsonValue::String(thinking.clone()));
if let Some(signature) = signature {
object.insert(
"signature".to_string(),
JsonValue::String(signature.clone()),
);
}
}
Self::ToolUse { id, name, input } => {
object.insert(
"type".to_string(),
@@ -783,6 +803,13 @@ impl ContentBlock {
"text" => Ok(Self::Text {
text: required_string(object, "text")?,
}),
"thinking" => Ok(Self::Thinking {
thinking: required_string(object, "thinking")?,
signature: object
.get("signature")
.and_then(JsonValue::as_str)
.map(String::from),
}),
"tool_use" => Ok(Self::ToolUse {
id: required_string(object, "id")?,
name: required_string(object, "name")?,
@@ -1208,6 +1235,36 @@ mod tests {
assert_eq!(restored.session_id, session.session_id);
}
#[test]
fn persists_assistant_thinking_block_round_trip_through_jsonl() {
// given
let mut session = Session::new();
session
.push_message(ConversationMessage::assistant(vec![
ContentBlock::Thinking {
thinking: "trace the path through session persistence".to_string(),
signature: Some("sig-123".to_string()),
},
]))
.expect("thinking block should append");
let path = temp_session_path("thinking-jsonl");
// when
session.save_to_path(&path).expect("session should save");
let restored = Session::load_from_path(&path).expect("session should load");
fs::remove_file(&path).expect("temp file should be removable");
// then
assert_eq!(restored, session);
assert_eq!(
restored.messages[0].blocks[0],
ContentBlock::Thinking {
thinking: "trace the path through session persistence".to_string(),
signature: Some("sig-123".to_string()),
}
);
}
#[test]
fn loads_legacy_session_json_object() {
let path = temp_session_path("legacy");