Move tool definitions into ContextState as system entries

Tool definitions are now pushed as a ContextEntry in the system
section at Agent construction time, formatted in the Qwen chat
template style. They're tokenized, scored, and treated like any
other context entry.

assemble_prompt_tokens() no longer takes a tools parameter —
tools are already in the context. This prepares for the switch
to /v1/completions where tools aren't a separate API field.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-08 11:36:33 -04:00
parent 67e3228c32
commit e9765799c4
3 changed files with 220 additions and 3 deletions

View file

@ -192,6 +192,24 @@ impl Agent {
let mut system = ContextSection::new("System prompt");
system.push(ContextEntry::new(
ConversationEntry::System(Message::system(&system_prompt)), None));
// Tool definitions — part of the context, tokenized and scored
let tool_defs: Vec<String> = tools::tools().iter()
.map(|t| t.to_json()).collect();
if !tool_defs.is_empty() {
let tools_text = format!(
"# Tools\n\nYou have access to the following functions:\n\n<tools>\n{}\n</tools>\n\n\
If you choose to call a function ONLY reply in the following format with NO suffix:\n\n\
<tool_call>\n<function=example_function_name>\n\
<parameter=example_parameter_1>\nvalue_1\n</parameter>\n\
</function>\n</tool_call>\n\n\
IMPORTANT: Function calls MUST follow the specified format.",
tool_defs.join("\n"),
);
system.push(ContextEntry::new(
ConversationEntry::System(Message::system(&tools_text)), None));
}
let mut identity = ContextSection::new("Identity");
for (_name, content) in &personality {
identity.push(ContextEntry::new(
@ -293,6 +311,42 @@ impl Agent {
msgs
}
/// Assemble the full prompt as token IDs for the completions API.
/// System section (includes tools), identity, journal, conversation,
/// then the assistant prompt suffix.
pub fn assemble_prompt_tokens(&self) -> Vec<u32> {
let mut tokens = Vec::new();
// System section — includes system prompt + tool definitions
for e in self.context.system.entries() {
tokens.extend(&e.token_ids);
}
// Identity — rendered as one user message
let ctx = self.context.render_context_message();
if !ctx.is_empty() {
tokens.extend(tokenizer::tokenize_entry("user", &ctx));
}
// Journal — rendered as one user message
let jnl = self.context.render_journal();
if !jnl.is_empty() {
tokens.extend(tokenizer::tokenize_entry("user", &jnl));
}
// Conversation entries — use cached token_ids
for e in self.context.conversation.entries() {
if e.entry.is_log() || e.entry.is_thinking() { continue; }
tokens.extend(&e.token_ids);
}
// Prompt the assistant to respond
tokens.push(tokenizer::IM_START);
tokens.extend(tokenizer::encode("assistant\n"));
tokens
}
/// Run agent orchestration cycle, returning structured output.
/// Push a conversation message — stamped and logged.
pub fn push_message(&mut self, mut msg: Message) {