fix: include tool results in conversation context

When OpenCode sends follow-up messages with tool results, include
the full conversation (user message, assistant tool calls, and tool
results) in the text sent to the model. Previously only the user
message was extracted, causing the model to never see tool results
and call the same tool repeatedly in an infinite loop.

Also add tool_calls and tool_call_id fields to CompletionMessage.
This commit is contained in:
Nikketryhard
2026-02-15 00:42:43 -06:00
parent 5d4125fa0d
commit 4f08b994c7
2 changed files with 95 additions and 10 deletions

View File

@@ -18,20 +18,26 @@ use super::AppState;
// ─── Input extraction ────────────────────────────────────────────────────────
/// Extract user text from Chat Completions messages array.
///
/// When tool results are present, builds the full conversation including
/// tool call results so the model can continue after tool use.
fn extract_chat_input(messages: &[CompletionMessage]) -> String {
let has_tool_results = messages.iter().any(|m| m.role == "tool");
if has_tool_results {
// Build full conversation context including tool results
return build_conversation_with_tools(messages);
}
// Simple path: no tools, just extract system + last user message
let mut system_parts = Vec::new();
let mut user_parts = Vec::new();
for msg in messages {
let text = match &msg.content {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Array(arr) => arr
.iter()
.filter_map(|item| item["text"].as_str())
.collect::<Vec<_>>()
.join("\n"),
_ => continue,
};
let text = extract_message_text(&msg.content);
if text.is_empty() {
continue;
}
match msg.role.as_str() {
"system" | "developer" => system_parts.push(text),
"user" => user_parts.push(text),
@@ -44,13 +50,87 @@ fn extract_chat_input(messages: &[CompletionMessage]) -> String {
result.push_str(&system_parts.join("\n"));
result.push_str("\n\n");
}
// Use the last user message
if let Some(last) = user_parts.last() {
result.push_str(last);
}
result.trim().to_string()
}
/// Extract text content from a message's content field (string or array).
fn extract_message_text(content: &serde_json::Value) -> String {
match content {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Array(arr) => arr
.iter()
.filter_map(|item| item["text"].as_str())
.collect::<Vec<_>>()
.join("\n"),
_ => String::new(),
}
}
/// Build conversation text that includes tool call results.
///
/// Format:
/// [system prompt]
/// [user message]
/// [assistant called tool X with args Y]
/// [tool result: Z]
/// [user followup if any]
fn build_conversation_with_tools(messages: &[CompletionMessage]) -> String {
let mut parts = Vec::new();
for msg in messages {
match msg.role.as_str() {
"system" | "developer" => {
let text = extract_message_text(&msg.content);
if !text.is_empty() {
parts.push(text);
}
}
"user" => {
let text = extract_message_text(&msg.content);
if !text.is_empty() {
parts.push(text);
}
}
"assistant" => {
// Include assistant text if any
let text = extract_message_text(&msg.content);
if !text.is_empty() {
parts.push(text);
}
// Include tool calls as context
if let Some(ref tool_calls) = msg.tool_calls {
for tc in tool_calls {
if let Some(func) = tc.get("function") {
let name = func["name"].as_str().unwrap_or("unknown");
let args = func["arguments"].as_str().unwrap_or("{}");
parts.push(format!(
"[Tool call: {}({})]",
name, args
));
}
}
}
}
"tool" => {
let text = extract_message_text(&msg.content);
let tool_id = msg.tool_call_id.as_deref().unwrap_or("unknown");
if !text.is_empty() {
parts.push(format!(
"[Tool result ({})]:\n{}",
tool_id, text
));
}
}
_ => {}
}
}
parts.join("\n\n")
}
// ─── Handler ─────────────────────────────────────────────────────────────────
/// POST /v1/chat/completions — OpenAI Chat Completions API compatibility shim.

View File

@@ -58,7 +58,12 @@ pub(crate) struct CompletionRequest {
#[derive(Deserialize)]
pub(crate) struct CompletionMessage {
pub role: String,
#[serde(default)]
pub content: serde_json::Value,
/// Tool calls made by the assistant (for assistant messages)
pub tool_calls: Option<Vec<serde_json::Value>>,
/// Tool call ID this message is responding to (for tool messages)
pub tool_call_id: Option<String>,
}
fn default_timeout() -> u64 {