Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/llm/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Trait-based LLM client implementations for multiple providers.
- Core message and tool types defined locally instead of re-exporting from `ollama-rs`
- tool calls hold name and arguments via `JsonResult`, preserving unparseable argument strings in the `error` variant
- tool info stores name, description, and parameters without wrapper enums
- when converting assistant tool calls into provider requests, the function `arguments` include the original tool-call `id` under the `_id` field
- chat messages are an enum of `UserMessage`, `AssistantMessage`, `SystemMessage`, and `ToolMessage`, each with only relevant fields
- `AssistantMessage` holds a `Vec<AssistantPart>` for text, tool calls, and thinking segments
- tool calls include an `id` string, assigned locally when missing
Expand Down
2 changes: 1 addition & 1 deletion crates/llm/src/gemini_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ impl LlmClient for GeminiRustClient {
}
AssistantPart::ToolCall(tc) => {
let args = match tc.arguments {
JsonResult::Content { content } => content,
JsonResult::Content { .. } => tc.arguments_content_with_id(),
JsonResult::Error { .. } => Value::Null,
};
parts_vec.push(Part::FunctionCall {
Expand Down
6 changes: 4 additions & 2 deletions crates/llm/src/harmony.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ fn build_prompt(
}
AssistantPart::ToolCall(tc) => {
let args = match &tc.arguments {
JsonResult::Content { content } => content.to_string(),
JsonResult::Content { .. } => {
tc.arguments_content_with_id().to_string()
}
JsonResult::Error { error } => error.clone(),
};
convo_msgs.push(
Expand Down Expand Up @@ -474,7 +476,7 @@ mod tests {
),
]);
assert!(prefill_tokens.is_none());
let args = json!({"a": 2, "b": 2}).to_string();
let args = json!({"a": 2, "b": 2, "_id": "1"}).to_string();
let result = json!({"sum": 4}).to_string();
let expected_tail = format!(
concat!(
Expand Down
11 changes: 11 additions & 0 deletions crates/llm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@ pub struct ToolCall {
pub arguments: JsonResult,
}

impl ToolCall {
pub fn arguments_content_with_id(&self) -> Value {
let mut result = (*self.arguments.as_result().unwrap()).clone();
result
.as_object_mut()
.unwrap()
.insert("_id".into(), Value::String(self.id.clone()));
result
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AssistantPart {
Expand Down
4 changes: 3 additions & 1 deletion crates/llm/src/ollama.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ impl LlmClient for OllamaClient {
}
AssistantPart::ToolCall(tc) => {
let args = match tc.arguments {
JsonResult::Content { content } => content,
JsonResult::Content { .. } => {
tc.arguments_content_with_id()
}
JsonResult::Error { .. } => Value::Null,
};
msg.tool_calls.push(OllamaToolCall {
Expand Down
4 changes: 3 additions & 1 deletion crates/llm/src/openai_chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ impl LlmClient for OpenAiChatClient {
.into_iter()
.map(|tc| {
let args = match &tc.arguments {
JsonResult::Content { content } => content.to_string(),
JsonResult::Content { .. } => {
tc.arguments_content_with_id().to_string()
}
JsonResult::Error { error } => error.clone(),
};
ChatCompletionMessageToolCall {
Expand Down
6 changes: 6 additions & 0 deletions crates/llment/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ Basic terminal chat interface scaffold using a bespoke component framework built
- `ChatMessageRequest` includes MCP `tool_infos` before enabling thinking
- Built-in tools registered via `setup_builtin_tools`
- `setup_builtin_tools` returns a running `McpService` inserted into the shared `McpContext`
- Tools:
- `chat.get_message_count`: returns the number of chat messages
- `chat.discard_function_response`:
- parameters: `{ id: string }`
- finds a `Tool` message by matching `id` and clears its result text
- returns `"ok"` on success or a not-found message if no matching entry exists
- `McpContext` retains running service handles
- `get_message_count` returns the number of chat messages

Expand Down
12 changes: 12 additions & 0 deletions crates/llment/prompts/snippets/instructions/context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{% if tool_enabled("chat.discard_function_response") %}
## Context

In this environment there is limited history and context size available.

It's important that you remove function responses (FR) that are no longer necessary by calling the chat.
discard_function_response.

Summarize the necessary parts of the FR with chain-of-thought, then proactively discard the FR as soon as possible -- before proceeding with other function calls. If the contents of the FR needs to be part of a message to the user, wait for a subsequent round before discarding.

Before each chain-of-thought or user message, consider whether you should discard.
{% endif %}
1 change: 1 addition & 0 deletions crates/llment/prompts/system/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{% include "snippets/instructions/focus.md" %}
{% include "snippets/instructions/task.md" %}
{% include "snippets/instructions/autonomy.md" %}
{% include "snippets/instructions/context.md" %}
{% for file in glob("snippets/env/*.md") %}
{% include file %}
{% endfor %}
Expand Down
52 changes: 45 additions & 7 deletions crates/llment/src/builtins.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
use std::sync::{Arc, Mutex};

use arc_swap::ArcSwap;
use llm::{ChatMessage, ToolInfo, mcp::McpService};
use llm::{ChatMessage, JsonResult, ToolInfo, mcp::McpService};
use rmcp::{
ServerHandler,
handler::server::router::tool::ToolRouter,
handler::server::{router::tool::ToolRouter, tool::Parameters},
model::{ServerCapabilities, ServerInfo},
service::{RoleClient, RunningService, ServiceExt},
tool, tool_handler, tool_router,
};
use schemars::{JsonSchema, schema_for};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::io::duplex;

#[derive(Serialize, Deserialize, JsonSchema)]
pub struct GetMessageCountParams {}

#[derive(Serialize, Deserialize, JsonSchema)]
pub struct DiscardFunctionResponseParams {
/// The id of the ToolCall/Tool response to discard
pub id: String,
}

#[derive(Clone)]
struct BuiltinTools {
chat_history: Arc<Mutex<Vec<ChatMessage>>>,
Expand All @@ -38,6 +45,30 @@ impl BuiltinTools {
fn get_message_count(&self) -> String {
self.chat_history.lock().unwrap().len().to_string()
}

#[tool(
name = "discard_function_response",
description = "Removes the content from a tool response in history by id"
)]
fn discard_function_response(
&self,
Parameters(params): Parameters<DiscardFunctionResponseParams>,
) -> String {
let mut history = self.chat_history.lock().unwrap();
if let Some((idx, _)) = history.iter().enumerate().rev().find(|(_, m)| match m {
ChatMessage::Tool(t) => t.id == params.id,
_ => false,
}) {
if let ChatMessage::Tool(t) = &mut history[idx] {
t.content = JsonResult::Content {
content: Value::String("<response discarded>".into()),
};
}
"ok".into()
} else {
format!("Tool response with id '{}' not found", params.id)
}
}
}

#[tool_handler(router = self.tool_router)]
Expand All @@ -59,11 +90,18 @@ pub async fn setup_builtin_tools(
builtins.clone().serve(server_transport),
McpService {
prefix: "chat".into(),
tools: ArcSwap::new(Arc::new(vec![ToolInfo {
name: "get_message_count".into(),
description: "Returns the number of chat messages".into(),
parameters: schema_for!(GetMessageCountParams),
}])),
tools: ArcSwap::new(Arc::new(vec![
ToolInfo {
name: "get_message_count".into(),
description: "Returns the number of chat messages".into(),
parameters: schema_for!(GetMessageCountParams),
},
ToolInfo {
name: "discard_function_response".into(),
description: "Removes the content from a tool response in history by id".into(),
parameters: schema_for!(DiscardFunctionResponseParams),
},
])),
}
.serve(client_transport)
);
Expand Down