2026-04-03 23:21:16 -04:00
|
|
|
// agent.rs — Core agent loop
|
2026-03-27 15:22:48 -04:00
|
|
|
//
|
2026-04-03 23:21:16 -04:00
|
|
|
// The simplest possible implementation of the agent pattern:
|
|
|
|
|
// send messages + tool definitions to the model, if it responds
|
|
|
|
|
// with tool calls then dispatch them and loop, if it responds
|
|
|
|
|
// with text then display it and wait for the next prompt.
|
|
|
|
|
//
|
|
|
|
|
// Uses streaming by default so text tokens appear as they're
|
|
|
|
|
// generated. Tool calls are accumulated from stream deltas and
|
|
|
|
|
// dispatched after the stream completes.
|
|
|
|
|
//
|
|
|
|
|
// The DMN (dmn.rs) is the outer loop that decides what prompts
|
|
|
|
|
// to send here. This module just handles single turns: prompt
|
|
|
|
|
// in, response out, tool calls dispatched.
|
2026-03-27 15:22:48 -04:00
|
|
|
|
2026-04-04 00:29:11 -04:00
|
|
|
pub mod api;
|
2026-03-27 15:22:48 -04:00
|
|
|
pub mod context;
|
2026-04-08 12:46:44 -04:00
|
|
|
pub mod context_new;
|
2026-04-04 17:25:10 -04:00
|
|
|
pub mod oneshot;
|
2026-04-08 11:20:03 -04:00
|
|
|
pub mod tokenizer;
|
2026-04-03 21:59:14 -04:00
|
|
|
pub mod tools;
|
2026-03-27 15:22:48 -04:00
|
|
|
|
2026-04-04 04:23:29 -04:00
|
|
|
use std::sync::Arc;
|
2026-04-03 23:21:16 -04:00
|
|
|
use anyhow::Result;
|
2026-03-27 15:22:48 -04:00
|
|
|
|
2026-04-08 14:55:10 -04:00
|
|
|
use api::ApiClient;
|
|
|
|
|
use context_new::{AstNode, NodeBody, ContextState, Section, Ast, PendingToolCall, ResponseParser, Role};
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
use tools::summarize_args;
|
2026-04-04 00:29:11 -04:00
|
|
|
|
2026-04-05 01:48:11 -04:00
|
|
|
use crate::mind::log::ConversationLog;
|
2026-03-27 15:22:48 -04:00
|
|
|
|
2026-04-05 22:18:07 -04:00
|
|
|
// --- Activity tracking (RAII guards) ---
|
|
|
|
|
|
|
|
|
|
pub struct ActivityEntry {
|
|
|
|
|
pub id: u64,
|
|
|
|
|
pub label: String,
|
|
|
|
|
pub started: std::time::Instant,
|
|
|
|
|
/// Auto-expires this long after creation (or completion).
|
|
|
|
|
pub expires_at: std::time::Instant,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// RAII guard — marks the activity "(complete)" on drop, starts expiry timer.
|
|
|
|
|
pub struct ActivityGuard {
|
|
|
|
|
agent: Arc<tokio::sync::Mutex<Agent>>,
|
|
|
|
|
id: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ACTIVITY_LINGER: std::time::Duration = std::time::Duration::from_secs(5);
|
|
|
|
|
|
|
|
|
|
impl Drop for ActivityGuard {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
if let Ok(mut ag) = self.agent.try_lock() {
|
|
|
|
|
if let Some(entry) = ag.activities.iter_mut().find(|a| a.id == self.id) {
|
|
|
|
|
entry.label.push_str(" (complete)");
|
|
|
|
|
entry.expires_at = std::time::Instant::now() + ACTIVITY_LINGER;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Agent {
|
|
|
|
|
/// Register an activity, returns its ID. Caller creates the guard.
|
|
|
|
|
pub fn push_activity(&mut self, label: impl Into<String>) -> u64 {
|
|
|
|
|
self.expire_activities();
|
|
|
|
|
let id = self.next_activity_id;
|
|
|
|
|
self.next_activity_id += 1;
|
|
|
|
|
self.activities.push(ActivityEntry {
|
|
|
|
|
id, label: label.into(),
|
|
|
|
|
started: std::time::Instant::now(),
|
|
|
|
|
expires_at: std::time::Instant::now() + std::time::Duration::from_secs(3600),
|
|
|
|
|
});
|
2026-04-05 23:04:10 -04:00
|
|
|
self.changed.notify_one();
|
2026-04-05 22:18:07 -04:00
|
|
|
id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Push a notification — auto-expires after 5 seconds.
|
|
|
|
|
pub fn notify(&mut self, label: impl Into<String>) {
|
|
|
|
|
self.expire_activities();
|
|
|
|
|
let id = self.next_activity_id;
|
|
|
|
|
self.next_activity_id += 1;
|
|
|
|
|
self.activities.push(ActivityEntry {
|
|
|
|
|
id, label: label.into(),
|
|
|
|
|
started: std::time::Instant::now(),
|
|
|
|
|
expires_at: std::time::Instant::now() + ACTIVITY_LINGER,
|
|
|
|
|
});
|
2026-04-05 23:04:10 -04:00
|
|
|
self.changed.notify_one();
|
2026-04-05 22:18:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Remove expired activities.
|
|
|
|
|
pub fn expire_activities(&mut self) {
|
|
|
|
|
let now = std::time::Instant::now();
|
|
|
|
|
self.activities.retain(|a| a.expires_at > now);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create an activity guard from outside the lock.
|
|
|
|
|
pub fn activity_guard(agent: &Arc<tokio::sync::Mutex<Agent>>, id: u64) -> ActivityGuard {
|
|
|
|
|
ActivityGuard { agent: agent.clone(), id }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Convenience: lock, push activity, unlock, return guard.
|
|
|
|
|
pub async fn start_activity(agent: &Arc<tokio::sync::Mutex<Agent>>, label: impl Into<String>) -> ActivityGuard {
|
|
|
|
|
let id = agent.lock().await.push_activity(label);
|
|
|
|
|
ActivityGuard { agent: agent.clone(), id }
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 23:21:16 -04:00
|
|
|
/// Result of a single agent turn.
|
|
|
|
|
pub struct TurnResult {
|
|
|
|
|
/// The text response (already sent through UI channel).
|
|
|
|
|
#[allow(dead_code)]
|
2026-03-27 15:22:48 -04:00
|
|
|
pub text: String,
|
2026-04-03 23:21:16 -04:00
|
|
|
/// Whether the model called yield_to_user during this turn.
|
|
|
|
|
pub yield_requested: bool,
|
|
|
|
|
/// Whether any tools (other than yield_to_user) were called.
|
|
|
|
|
pub had_tool_calls: bool,
|
|
|
|
|
/// Number of tool calls that returned errors this turn.
|
|
|
|
|
pub tool_errors: u32,
|
|
|
|
|
/// Model name to switch to after this turn completes.
|
2026-03-27 15:22:48 -04:00
|
|
|
pub model_switch: Option<String>,
|
2026-04-03 23:21:16 -04:00
|
|
|
/// Agent requested DMN pause (full stop on autonomous behavior).
|
2026-03-27 15:22:48 -04:00
|
|
|
pub dmn_pause: bool,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 23:21:16 -04:00
|
|
|
/// Accumulated state across tool dispatches within a single turn.
|
|
|
|
|
struct DispatchState {
|
|
|
|
|
yield_requested: bool,
|
|
|
|
|
had_tool_calls: bool,
|
|
|
|
|
tool_errors: u32,
|
|
|
|
|
model_switch: Option<String>,
|
|
|
|
|
dmn_pause: bool,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 04:23:29 -04:00
|
|
|
impl DispatchState {
|
|
|
|
|
fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
yield_requested: false, had_tool_calls: false,
|
|
|
|
|
tool_errors: 0, model_switch: None, dmn_pause: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 23:21:16 -04:00
|
|
|
pub struct Agent {
|
|
|
|
|
client: ApiClient,
|
2026-04-04 16:20:43 -04:00
|
|
|
tools: Vec<tools::Tool>,
|
2026-04-03 23:21:16 -04:00
|
|
|
/// Last known prompt token count from the API (tracks context size).
|
|
|
|
|
last_prompt_tokens: u32,
|
|
|
|
|
/// Current reasoning effort level ("none", "low", "high").
|
|
|
|
|
pub reasoning_effort: String,
|
2026-04-04 13:48:24 -04:00
|
|
|
/// Sampling parameters — adjustable at runtime from the thalamus screen.
|
|
|
|
|
pub temperature: f32,
|
|
|
|
|
pub top_p: f32,
|
|
|
|
|
pub top_k: u32,
|
2026-04-05 22:18:07 -04:00
|
|
|
/// Active activities — RAII guards auto-remove on drop.
|
|
|
|
|
pub activities: Vec<ActivityEntry>,
|
|
|
|
|
next_activity_id: u64,
|
2026-04-04 16:05:33 -04:00
|
|
|
/// Control tool flags — set by tool handlers, consumed by turn loop.
|
|
|
|
|
pub pending_yield: bool,
|
|
|
|
|
pub pending_model_switch: Option<String>,
|
|
|
|
|
pub pending_dmn_pause: bool,
|
2026-04-07 17:46:40 -04:00
|
|
|
/// Provenance tag for memory operations — identifies who made the change.
|
|
|
|
|
pub provenance: String,
|
2026-04-03 23:21:16 -04:00
|
|
|
/// Persistent conversation log — append-only record of all messages.
|
2026-04-06 21:48:12 -04:00
|
|
|
pub conversation_log: Option<ConversationLog>,
|
2026-04-03 23:21:16 -04:00
|
|
|
pub context: ContextState,
|
2026-04-05 02:29:44 -04:00
|
|
|
/// App config — used to reload identity on compaction and model switching.
|
|
|
|
|
pub app_config: crate::config::AppConfig,
|
2026-04-03 23:21:16 -04:00
|
|
|
pub prompt_file: String,
|
|
|
|
|
/// Stable session ID for memory-search dedup across turns.
|
2026-04-04 02:46:32 -04:00
|
|
|
pub session_id: String,
|
2026-04-05 19:17:13 -04:00
|
|
|
/// Incremented on compaction — UI uses this to detect resets.
|
|
|
|
|
pub generation: u64,
|
2026-04-06 21:48:12 -04:00
|
|
|
/// Whether incremental memory scoring is currently running.
|
|
|
|
|
pub memory_scoring_in_flight: bool,
|
2026-04-03 23:21:16 -04:00
|
|
|
/// Shared active tools — Agent writes, TUI reads.
|
2026-04-05 22:40:38 -04:00
|
|
|
pub active_tools: tools::SharedActiveTools,
|
2026-04-05 23:04:10 -04:00
|
|
|
/// Fires when agent state changes — UI wakes on this instead of polling.
|
|
|
|
|
pub changed: Arc<tokio::sync::Notify>,
|
2026-04-03 23:21:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Agent {
|
|
|
|
|
pub fn new(
|
|
|
|
|
client: ApiClient,
|
|
|
|
|
system_prompt: String,
|
|
|
|
|
personality: Vec<(String, String)>,
|
|
|
|
|
app_config: crate::config::AppConfig,
|
|
|
|
|
prompt_file: String,
|
|
|
|
|
conversation_log: Option<ConversationLog>,
|
2026-04-05 22:34:48 -04:00
|
|
|
active_tools: tools::SharedActiveTools,
|
2026-04-03 23:21:16 -04:00
|
|
|
) -> Self {
|
2026-04-08 14:55:10 -04:00
|
|
|
let mut context = ContextState::new();
|
|
|
|
|
context.push(Section::System, AstNode::system_msg(&system_prompt));
|
2026-04-08 11:36:33 -04:00
|
|
|
|
|
|
|
|
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"),
|
|
|
|
|
);
|
2026-04-08 14:55:10 -04:00
|
|
|
context.push(Section::System, AstNode::system_msg(&tools_text));
|
2026-04-08 11:36:33 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 14:55:10 -04:00
|
|
|
for (name, content) in &personality {
|
|
|
|
|
context.push(Section::Identity, AstNode::memory(name, content));
|
2026-04-07 20:15:31 -04:00
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
let session_id = format!("consciousness-{}", chrono::Utc::now().format("%Y%m%d-%H%M%S"));
|
|
|
|
|
let mut agent = Self {
|
|
|
|
|
client,
|
2026-04-04 16:20:43 -04:00
|
|
|
tools: tools::tools(),
|
2026-04-03 23:21:16 -04:00
|
|
|
last_prompt_tokens: 0,
|
|
|
|
|
reasoning_effort: "none".to_string(),
|
2026-04-04 13:48:24 -04:00
|
|
|
temperature: 0.6,
|
|
|
|
|
top_p: 0.95,
|
|
|
|
|
top_k: 20,
|
2026-04-05 22:18:07 -04:00
|
|
|
activities: Vec::new(),
|
|
|
|
|
next_activity_id: 0,
|
2026-04-04 16:05:33 -04:00
|
|
|
pending_yield: false,
|
|
|
|
|
pending_model_switch: None,
|
|
|
|
|
pending_dmn_pause: false,
|
2026-04-07 17:46:40 -04:00
|
|
|
provenance: "manual".to_string(),
|
2026-04-03 23:21:16 -04:00
|
|
|
conversation_log,
|
|
|
|
|
context,
|
|
|
|
|
app_config,
|
|
|
|
|
prompt_file,
|
|
|
|
|
session_id,
|
2026-04-05 19:17:13 -04:00
|
|
|
generation: 0,
|
2026-04-06 21:48:12 -04:00
|
|
|
memory_scoring_in_flight: false,
|
2026-04-03 23:21:16 -04:00
|
|
|
active_tools,
|
2026-04-05 23:04:10 -04:00
|
|
|
changed: Arc::new(tokio::sync::Notify::new()),
|
2026-04-03 23:21:16 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
agent.load_startup_journal();
|
|
|
|
|
agent
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 01:07:04 -04:00
|
|
|
/// Create a lightweight agent forked from this one's context.
|
|
|
|
|
///
|
|
|
|
|
/// The forked agent shares the same conversation prefix (system prompt,
|
|
|
|
|
/// personality, journal, entries) for KV cache sharing. The caller
|
|
|
|
|
/// appends the subconscious prompt as a user message and runs the turn.
|
|
|
|
|
pub fn fork(&self, tools: Vec<tools::Tool>) -> Self {
|
|
|
|
|
|
|
|
|
|
Self {
|
|
|
|
|
client: self.client.clone(),
|
|
|
|
|
tools,
|
|
|
|
|
last_prompt_tokens: 0,
|
|
|
|
|
reasoning_effort: "none".to_string(),
|
|
|
|
|
temperature: self.temperature,
|
|
|
|
|
top_p: self.top_p,
|
|
|
|
|
top_k: self.top_k,
|
|
|
|
|
activities: Vec::new(),
|
|
|
|
|
next_activity_id: 0,
|
|
|
|
|
pending_yield: false,
|
|
|
|
|
pending_model_switch: None,
|
|
|
|
|
pending_dmn_pause: false,
|
2026-04-07 17:46:40 -04:00
|
|
|
provenance: self.provenance.clone(),
|
2026-04-07 01:07:04 -04:00
|
|
|
conversation_log: None,
|
|
|
|
|
context: self.context.clone(),
|
|
|
|
|
app_config: self.app_config.clone(),
|
|
|
|
|
prompt_file: self.prompt_file.clone(),
|
|
|
|
|
session_id: self.session_id.clone(),
|
|
|
|
|
generation: 0,
|
|
|
|
|
memory_scoring_in_flight: false,
|
|
|
|
|
active_tools: tools::shared_active_tools(),
|
|
|
|
|
changed: Arc::new(tokio::sync::Notify::new()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 14:55:10 -04:00
|
|
|
/// Assemble the full prompt as token IDs.
|
|
|
|
|
/// Context sections + assistant prompt suffix.
|
2026-04-08 11:36:33 -04:00
|
|
|
pub fn assemble_prompt_tokens(&self) -> Vec<u32> {
|
2026-04-08 14:55:10 -04:00
|
|
|
let mut tokens = self.context.token_ids();
|
2026-04-08 11:36:33 -04:00
|
|
|
tokens.push(tokenizer::IM_START);
|
|
|
|
|
tokens.extend(tokenizer::encode("assistant\n"));
|
|
|
|
|
tokens
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 14:55:10 -04:00
|
|
|
/// Push a node into the conversation and log it.
|
|
|
|
|
pub fn push_node(&mut self, node: AstNode) {
|
2026-04-03 23:21:16 -04:00
|
|
|
if let Some(ref log) = self.conversation_log {
|
2026-04-08 14:55:10 -04:00
|
|
|
if let Err(e) = log.append_node(&node) {
|
2026-04-03 23:21:16 -04:00
|
|
|
eprintln!("warning: failed to log entry: {:#}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-08 14:55:10 -04:00
|
|
|
self.context.push(Section::Conversation, node);
|
2026-04-06 22:20:22 -04:00
|
|
|
self.changed.notify_one();
|
|
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
|
2026-04-08 14:55:10 -04:00
|
|
|
/// Run the agent turn loop: assemble prompt, stream response,
|
|
|
|
|
/// parse into AST, dispatch tool calls, repeat until text response.
|
2026-04-03 23:21:16 -04:00
|
|
|
pub async fn turn(
|
2026-04-05 21:13:48 -04:00
|
|
|
agent: Arc<tokio::sync::Mutex<Agent>>,
|
2026-04-03 23:21:16 -04:00
|
|
|
) -> Result<TurnResult> {
|
2026-04-04 04:23:29 -04:00
|
|
|
let active_tools = {
|
2026-04-08 14:55:10 -04:00
|
|
|
let me = agent.lock().await;
|
|
|
|
|
me.active_tools.clone()
|
|
|
|
|
};
|
2026-04-03 23:21:16 -04:00
|
|
|
|
2026-04-08 14:55:10 -04:00
|
|
|
// Collect finished background tools
|
|
|
|
|
{
|
|
|
|
|
let mut finished = Vec::new();
|
|
|
|
|
{
|
|
|
|
|
let mut tools = active_tools.lock().unwrap();
|
2026-04-03 23:42:27 -04:00
|
|
|
let mut i = 0;
|
|
|
|
|
while i < tools.len() {
|
|
|
|
|
if tools[i].handle.is_finished() {
|
2026-04-05 21:13:48 -04:00
|
|
|
finished.push(tools.remove(i));
|
2026-04-03 23:42:27 -04:00
|
|
|
} else {
|
|
|
|
|
i += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
}
|
2026-04-08 14:55:10 -04:00
|
|
|
if !finished.is_empty() {
|
|
|
|
|
let mut results = Vec::new();
|
|
|
|
|
for entry in finished {
|
|
|
|
|
if let Ok((call, output)) = entry.handle.await {
|
|
|
|
|
results.push((call, output));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-05 21:13:48 -04:00
|
|
|
let mut me = agent.lock().await;
|
|
|
|
|
let mut bg_ds = DispatchState::new();
|
2026-04-08 14:55:10 -04:00
|
|
|
for (call, output) in results {
|
2026-04-05 22:34:48 -04:00
|
|
|
me.apply_tool_result(&call, output, &mut bg_ds);
|
2026-04-05 21:13:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-08 14:55:10 -04:00
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
|
|
|
|
|
let mut overflow_retries: u32 = 0;
|
|
|
|
|
let mut empty_retries: u32 = 0;
|
2026-04-04 04:23:29 -04:00
|
|
|
let mut ds = DispatchState::new();
|
2026-04-03 23:21:16 -04:00
|
|
|
|
|
|
|
|
loop {
|
2026-04-05 22:18:07 -04:00
|
|
|
let _thinking = start_activity(&agent, "thinking...").await;
|
2026-04-08 14:55:10 -04:00
|
|
|
|
|
|
|
|
// Assemble prompt and start stream (brief lock)
|
2026-04-04 04:23:29 -04:00
|
|
|
let (mut rx, _stream_guard) = {
|
2026-04-05 22:18:07 -04:00
|
|
|
let me = agent.lock().await;
|
2026-04-08 14:55:10 -04:00
|
|
|
let prompt_tokens = me.assemble_prompt_tokens();
|
|
|
|
|
me.client.stream_completion(
|
|
|
|
|
&prompt_tokens,
|
|
|
|
|
api::SamplingParams {
|
|
|
|
|
temperature: me.temperature,
|
|
|
|
|
top_p: me.top_p,
|
|
|
|
|
top_k: me.top_k,
|
|
|
|
|
},
|
|
|
|
|
None,
|
|
|
|
|
)
|
2026-04-04 04:23:29 -04:00
|
|
|
};
|
2026-04-07 22:49:35 -04:00
|
|
|
|
2026-04-08 14:55:10 -04:00
|
|
|
// Create assistant branch and parser (brief lock)
|
|
|
|
|
let branch_idx = {
|
2026-04-05 21:13:48 -04:00
|
|
|
let mut me = agent.lock().await;
|
2026-04-08 14:55:10 -04:00
|
|
|
let idx = me.context.len(Section::Conversation);
|
|
|
|
|
me.context.push(Section::Conversation,
|
|
|
|
|
AstNode::branch(Role::Assistant, vec![]));
|
|
|
|
|
idx
|
|
|
|
|
};
|
|
|
|
|
let mut parser = ResponseParser::new(branch_idx);
|
|
|
|
|
let mut pending_calls: Vec<PendingToolCall> = Vec::new();
|
|
|
|
|
let mut had_content = false;
|
|
|
|
|
let mut stream_error: Option<String> = None;
|
|
|
|
|
|
|
|
|
|
// Stream loop — no lock held across I/O
|
|
|
|
|
while let Some(event) = rx.recv().await {
|
|
|
|
|
match event {
|
|
|
|
|
api::StreamToken::Token { text, id: _ } => {
|
|
|
|
|
had_content = true;
|
|
|
|
|
let mut me = agent.lock().await;
|
|
|
|
|
let calls = parser.feed(&text, &mut me.context);
|
|
|
|
|
for call in calls {
|
|
|
|
|
// Dispatch tool call immediately
|
|
|
|
|
let call_clone = call.clone();
|
|
|
|
|
let agent_handle = agent.clone();
|
|
|
|
|
let handle = tokio::spawn(async move {
|
|
|
|
|
let args: serde_json::Value =
|
|
|
|
|
serde_json::from_str(&call_clone.arguments).unwrap_or_default();
|
|
|
|
|
let output = tools::dispatch_with_agent(
|
|
|
|
|
&call_clone.name, &args, Some(agent_handle),
|
|
|
|
|
).await;
|
|
|
|
|
(call_clone, output)
|
|
|
|
|
});
|
|
|
|
|
active_tools.lock().unwrap().push(tools::ActiveToolCall {
|
|
|
|
|
id: call.id.clone(),
|
|
|
|
|
name: call.name.clone(),
|
|
|
|
|
detail: call.arguments.clone(),
|
|
|
|
|
started: std::time::Instant::now(),
|
|
|
|
|
background: false,
|
|
|
|
|
handle,
|
|
|
|
|
});
|
|
|
|
|
pending_calls.push(call);
|
|
|
|
|
}
|
2026-04-04 04:23:29 -04:00
|
|
|
}
|
2026-04-08 14:55:10 -04:00
|
|
|
api::StreamToken::Error(e) => {
|
|
|
|
|
stream_error = Some(e);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
api::StreamToken::Done { usage } => {
|
|
|
|
|
if let Some(u) = usage {
|
|
|
|
|
agent.lock().await.last_prompt_tokens = u.prompt_tokens;
|
|
|
|
|
}
|
|
|
|
|
break;
|
2026-04-04 04:23:29 -04:00
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
}
|
2026-04-08 14:55:10 -04:00
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
|
2026-04-08 14:55:10 -04:00
|
|
|
// Flush parser remainder
|
|
|
|
|
{
|
|
|
|
|
let mut me = agent.lock().await;
|
|
|
|
|
parser.finish(&mut me.context);
|
|
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
|
2026-04-08 14:55:10 -04:00
|
|
|
// Handle errors
|
|
|
|
|
if let Some(e) = stream_error {
|
|
|
|
|
let err = anyhow::anyhow!("{}", e);
|
|
|
|
|
let mut me = agent.lock().await;
|
|
|
|
|
if context_new::is_context_overflow(&err) && overflow_retries < 2 {
|
|
|
|
|
overflow_retries += 1;
|
|
|
|
|
me.notify(format!("context overflow — retrying ({}/2)", overflow_retries));
|
|
|
|
|
me.compact();
|
|
|
|
|
continue;
|
2026-04-04 04:23:29 -04:00
|
|
|
}
|
2026-04-08 14:55:10 -04:00
|
|
|
if context_new::is_stream_error(&err) && empty_retries < 2 {
|
|
|
|
|
empty_retries += 1;
|
|
|
|
|
me.notify(format!("stream error — retrying ({}/2)", empty_retries));
|
|
|
|
|
drop(me);
|
|
|
|
|
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
|
|
|
|
continue;
|
2026-04-04 04:23:29 -04:00
|
|
|
}
|
2026-04-08 14:55:10 -04:00
|
|
|
return Err(err);
|
|
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
|
2026-04-08 14:55:10 -04:00
|
|
|
// Empty response — nudge and retry
|
|
|
|
|
if !had_content && pending_calls.is_empty() {
|
|
|
|
|
if empty_retries < 2 {
|
|
|
|
|
empty_retries += 1;
|
|
|
|
|
let mut me = agent.lock().await;
|
|
|
|
|
me.push_node(AstNode::user_msg(
|
|
|
|
|
"[system] Your previous response was empty. \
|
|
|
|
|
Please respond with text or use a tool."
|
|
|
|
|
));
|
|
|
|
|
continue;
|
2026-04-03 23:21:16 -04:00
|
|
|
}
|
2026-04-08 14:55:10 -04:00
|
|
|
} else {
|
|
|
|
|
empty_retries = 0;
|
|
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
|
2026-04-08 14:55:10 -04:00
|
|
|
// Wait for tool calls to complete
|
|
|
|
|
if !pending_calls.is_empty() {
|
|
|
|
|
ds.had_tool_calls = true;
|
|
|
|
|
|
|
|
|
|
// Collect non-background tool handles
|
|
|
|
|
let mut handles = Vec::new();
|
|
|
|
|
{
|
|
|
|
|
let mut tools_guard = active_tools.lock().unwrap();
|
|
|
|
|
let mut i = 0;
|
|
|
|
|
while i < tools_guard.len() {
|
|
|
|
|
if !tools_guard[i].background {
|
|
|
|
|
handles.push(tools_guard.remove(i));
|
|
|
|
|
} else {
|
|
|
|
|
i += 1;
|
|
|
|
|
}
|
2026-04-03 23:42:27 -04:00
|
|
|
}
|
2026-04-05 21:13:48 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 14:55:10 -04:00
|
|
|
for entry in handles {
|
|
|
|
|
if let Ok((call, output)) = entry.handle.await {
|
|
|
|
|
let mut me = agent.lock().await;
|
|
|
|
|
me.apply_tool_result(&call, output, &mut ds);
|
2026-04-04 04:23:29 -04:00
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
}
|
2026-04-05 21:13:48 -04:00
|
|
|
continue;
|
|
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
|
2026-04-08 14:55:10 -04:00
|
|
|
// Text-only response — extract text and return
|
|
|
|
|
let text = {
|
|
|
|
|
let me = agent.lock().await;
|
|
|
|
|
let children = me.context.conversation()[branch_idx].children();
|
|
|
|
|
children.iter()
|
|
|
|
|
.filter_map(|c| c.leaf())
|
|
|
|
|
.filter(|l| matches!(l.body(), NodeBody::Content(_)))
|
|
|
|
|
.map(|l| l.body().text())
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join("")
|
|
|
|
|
};
|
2026-04-05 21:13:48 -04:00
|
|
|
|
|
|
|
|
let mut me = agent.lock().await;
|
|
|
|
|
if me.pending_yield { ds.yield_requested = true; me.pending_yield = false; }
|
|
|
|
|
if me.pending_model_switch.is_some() { ds.model_switch = me.pending_model_switch.take(); }
|
|
|
|
|
if me.pending_dmn_pause { ds.dmn_pause = true; me.pending_dmn_pause = false; }
|
|
|
|
|
|
|
|
|
|
return Ok(TurnResult {
|
|
|
|
|
text,
|
|
|
|
|
yield_requested: ds.yield_requested,
|
|
|
|
|
had_tool_calls: ds.had_tool_calls,
|
|
|
|
|
tool_errors: ds.tool_errors,
|
|
|
|
|
model_switch: ds.model_switch,
|
|
|
|
|
dmn_pause: ds.dmn_pause,
|
|
|
|
|
});
|
2026-03-27 15:22:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 04:23:29 -04:00
|
|
|
/// Dispatch a tool call without holding the agent lock across I/O.
|
|
|
|
|
async fn dispatch_tool_call_unlocked(
|
2026-04-05 21:13:48 -04:00
|
|
|
agent: &Arc<tokio::sync::Mutex<Agent>>,
|
2026-04-05 22:40:38 -04:00
|
|
|
active_tools: &tools::SharedActiveTools,
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
call: &PendingToolCall,
|
2026-04-03 23:21:16 -04:00
|
|
|
ds: &mut DispatchState,
|
|
|
|
|
) {
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
let args: serde_json::Value = match serde_json::from_str(&call.arguments) {
|
2026-04-04 23:24:31 -04:00
|
|
|
Ok(v) => v,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
let err = format!("Error: malformed tool call arguments: {e}");
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
let _act = start_activity(agent, format!("rejected: {} (bad args)", call.name)).await;
|
2026-04-05 21:13:48 -04:00
|
|
|
let mut me = agent.lock().await;
|
2026-04-05 22:34:48 -04:00
|
|
|
me.apply_tool_result(call, err, ds);
|
2026-04-04 23:24:31 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-04-03 23:21:16 -04:00
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
let args_summary = summarize_args(&call.name, &args);
|
|
|
|
|
let _calling = start_activity(agent, format!("calling: {}", call.name)).await;
|
2026-04-04 04:23:29 -04:00
|
|
|
|
|
|
|
|
let call_clone = call.clone();
|
2026-04-04 16:14:27 -04:00
|
|
|
let agent_handle = agent.clone();
|
2026-04-03 23:42:27 -04:00
|
|
|
let handle = tokio::spawn(async move {
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
let output = tools::dispatch_with_agent(&call_clone.name, &args, Some(agent_handle)).await;
|
2026-04-04 04:23:29 -04:00
|
|
|
(call_clone, output)
|
2026-04-03 23:42:27 -04:00
|
|
|
});
|
2026-04-04 04:23:29 -04:00
|
|
|
active_tools.lock().unwrap().push(
|
2026-04-03 23:42:27 -04:00
|
|
|
tools::ActiveToolCall {
|
2026-04-04 04:23:29 -04:00
|
|
|
id: call.id.clone(),
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
name: call.name.clone(),
|
2026-04-03 23:42:27 -04:00
|
|
|
detail: args_summary,
|
|
|
|
|
started: std::time::Instant::now(),
|
|
|
|
|
background: false,
|
|
|
|
|
handle,
|
|
|
|
|
}
|
|
|
|
|
);
|
2026-04-03 23:21:16 -04:00
|
|
|
|
2026-04-03 23:42:27 -04:00
|
|
|
let entry = {
|
2026-04-04 04:23:29 -04:00
|
|
|
let mut tools = active_tools.lock().unwrap();
|
2026-04-03 23:42:27 -04:00
|
|
|
tools.pop().unwrap()
|
|
|
|
|
};
|
|
|
|
|
if let Ok((call, output)) = entry.handle.await {
|
2026-04-05 21:13:48 -04:00
|
|
|
let mut me = agent.lock().await;
|
2026-04-05 22:34:48 -04:00
|
|
|
me.apply_tool_result(&call, output, ds);
|
2026-04-03 23:42:27 -04:00
|
|
|
}
|
2026-03-27 15:22:48 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 23:21:16 -04:00
|
|
|
fn apply_tool_result(
|
|
|
|
|
&mut self,
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
call: &PendingToolCall,
|
2026-04-04 16:08:59 -04:00
|
|
|
output: String,
|
2026-04-03 23:21:16 -04:00
|
|
|
ds: &mut DispatchState,
|
|
|
|
|
) {
|
2026-04-04 16:05:33 -04:00
|
|
|
ds.had_tool_calls = true;
|
2026-04-04 16:08:59 -04:00
|
|
|
if output.starts_with("Error:") {
|
2026-04-03 23:21:16 -04:00
|
|
|
ds.tool_errors += 1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 23:42:27 -04:00
|
|
|
self.active_tools.lock().unwrap().retain(|t| t.id != call.id);
|
2026-04-03 23:21:16 -04:00
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
// Tag memory_render results as Memory nodes for context deduplication
|
|
|
|
|
if call.name == "memory_render" && !output.starts_with("Error:") {
|
|
|
|
|
let args: serde_json::Value =
|
|
|
|
|
serde_json::from_str(&call.arguments).unwrap_or_default();
|
2026-04-03 23:21:16 -04:00
|
|
|
if let Some(key) = args.get("key").and_then(|v| v.as_str()) {
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
self.push_node(AstNode::memory(key, &output));
|
2026-04-03 23:21:16 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
self.push_node(AstNode::tool_result(&output));
|
2026-04-03 23:21:16 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-07 19:02:58 -04:00
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
pub fn conversation_from(&self, from: usize) -> &[AstNode] {
|
|
|
|
|
let conv = self.context.conversation();
|
|
|
|
|
if from < conv.len() { &conv[from..] } else { &[] }
|
2026-03-27 15:22:48 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 23:21:16 -04:00
|
|
|
fn load_startup_journal(&mut self) {
|
|
|
|
|
let store = match crate::store::Store::load() {
|
|
|
|
|
Ok(s) => s,
|
|
|
|
|
Err(_) => return,
|
|
|
|
|
};
|
2026-03-27 15:22:48 -04:00
|
|
|
|
2026-04-03 23:21:16 -04:00
|
|
|
let oldest_msg_ts = self.conversation_log.as_ref()
|
|
|
|
|
.and_then(|log| log.oldest_timestamp());
|
2026-03-27 15:22:48 -04:00
|
|
|
|
2026-04-03 23:21:16 -04:00
|
|
|
let mut journal_nodes: Vec<_> = store.nodes.values()
|
|
|
|
|
.filter(|n| n.node_type == crate::store::NodeType::EpisodicSession)
|
|
|
|
|
.collect();
|
|
|
|
|
journal_nodes.sort_by_key(|n| n.created_at);
|
|
|
|
|
|
|
|
|
|
let cutoff_idx = if let Some(cutoff) = oldest_msg_ts {
|
|
|
|
|
let cutoff_ts = cutoff.timestamp();
|
|
|
|
|
let mut idx = journal_nodes.len();
|
|
|
|
|
for (i, node) in journal_nodes.iter().enumerate() {
|
|
|
|
|
if node.created_at >= cutoff_ts {
|
|
|
|
|
idx = i + 1;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
idx
|
|
|
|
|
} else {
|
|
|
|
|
journal_nodes.len()
|
|
|
|
|
};
|
|
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
let journal_budget = context_new::context_window() * 15 / 100;
|
|
|
|
|
let mut entries = Vec::new();
|
2026-04-03 23:21:16 -04:00
|
|
|
let mut total_tokens = 0;
|
|
|
|
|
|
|
|
|
|
for node in journal_nodes[..cutoff_idx].iter().rev() {
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
let ts = chrono::DateTime::from_timestamp(node.created_at, 0);
|
|
|
|
|
let ast = AstNode::memory(&node.key, &node.content)
|
|
|
|
|
.with_timestamp(ts.unwrap_or_else(chrono::Utc::now));
|
|
|
|
|
let tok = ast.tokens();
|
|
|
|
|
if total_tokens + tok > journal_budget && !entries.is_empty() {
|
2026-04-03 23:21:16 -04:00
|
|
|
break;
|
|
|
|
|
}
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
total_tokens += tok;
|
|
|
|
|
entries.push(ast);
|
2026-04-03 23:21:16 -04:00
|
|
|
}
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
entries.reverse();
|
2026-04-03 23:21:16 -04:00
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
if entries.is_empty() { return; }
|
2026-04-03 23:21:16 -04:00
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
self.context.clear(Section::Journal);
|
|
|
|
|
for entry in entries {
|
|
|
|
|
self.context.push(Section::Journal, entry);
|
2026-04-03 23:21:16 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn last_prompt_tokens(&self) -> u32 {
|
|
|
|
|
self.last_prompt_tokens
|
|
|
|
|
}
|
|
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
/// Rebuild the context window: reload identity, trim, reload journal.
|
2026-04-03 23:21:16 -04:00
|
|
|
pub fn compact(&mut self) {
|
|
|
|
|
match crate::config::reload_for_model(&self.app_config, &self.prompt_file) {
|
|
|
|
|
Ok((system_prompt, personality)) => {
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
self.context.clear(Section::System);
|
|
|
|
|
self.context.push(Section::System, AstNode::system_msg(&system_prompt));
|
|
|
|
|
self.context.clear(Section::Identity);
|
|
|
|
|
for (name, content) in &personality {
|
|
|
|
|
self.context.push(Section::Identity, AstNode::memory(name, content));
|
2026-04-07 20:15:31 -04:00
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("warning: failed to reload identity: {:#}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 21:48:12 -04:00
|
|
|
self.load_startup_journal();
|
|
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
// TODO: trim_entries — dedup memories, evict to budget
|
2026-04-05 19:17:13 -04:00
|
|
|
self.generation += 1;
|
2026-04-03 23:21:16 -04:00
|
|
|
self.last_prompt_tokens = 0;
|
|
|
|
|
}
|
|
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
/// Restore from the conversation log.
|
2026-04-03 23:21:16 -04:00
|
|
|
/// Returns true if the log had content to restore.
|
|
|
|
|
pub fn restore_from_log(&mut self) -> bool {
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
let nodes = match &self.conversation_log {
|
|
|
|
|
Some(log) => match log.read_nodes(64 * 1024 * 1024) {
|
|
|
|
|
Ok(nodes) if !nodes.is_empty() => nodes,
|
2026-04-03 23:21:16 -04:00
|
|
|
_ => return false,
|
|
|
|
|
},
|
|
|
|
|
None => return false,
|
|
|
|
|
};
|
|
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
self.context.clear(Section::Conversation);
|
|
|
|
|
for node in nodes {
|
|
|
|
|
self.context.push(Section::Conversation, node);
|
|
|
|
|
}
|
2026-04-03 23:21:16 -04:00
|
|
|
self.compact();
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
self.last_prompt_tokens = self.context.tokens() as u32;
|
2026-04-03 23:21:16 -04:00
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn swap_client(&mut self, new_client: ApiClient) {
|
|
|
|
|
self.client = new_client;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn model(&self) -> &str {
|
|
|
|
|
&self.client.model
|
|
|
|
|
}
|
|
|
|
|
|
WIP: Agent core migrated to AST types
agent/mod.rs fully uses AstNode/ContextState/PendingToolCall.
Killed: push_message, push_entry, append_streaming, finalize_streaming,
streaming_index, assemble_api_messages, age_out_images, working_stack,
context_sections, entries. ConversationLog rewritten for AstNode.
Remaining: api dead code (chat path), mind/, user/, oneshot, learn.
Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-08 14:59:38 -04:00
|
|
|
pub fn conversation(&self) -> &[AstNode] {
|
|
|
|
|
self.context.conversation()
|
2026-04-03 23:21:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn client_clone(&self) -> ApiClient {
|
|
|
|
|
self.client.clone()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 13:25:18 -04:00
|
|
|
|
2026-03-28 19:49:13 -04:00
|
|
|
|
2026-04-01 15:12:14 -04:00
|
|
|
}
|