loopd: neutralise user messages, tool results and the memory block
Text from outside the model (the user's message, every tool result, and the memory block) is neutralised the moment it is first logged, so every later request replays the same bytes and a tag in that text cannot reach the model as one of its own control tokens. The model's own output, replay, and the system prompt are left byte-for-byte. Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -7,6 +7,7 @@ use std::path::{Path, PathBuf};
|
||||
use crate::config::Config;
|
||||
use crate::llama::{ChatMessage, ToolSchema};
|
||||
use crate::tools::Registry;
|
||||
use bxcore::sanitize::neutralise;
|
||||
use bxcore::{Hash32, LogRecord, sha256};
|
||||
|
||||
use serde::Deserialize;
|
||||
@@ -46,7 +47,9 @@ impl Baseline {
|
||||
}
|
||||
|
||||
/// The system prompt trimmed, then a blank line and the memory block `memoryd` rendered (M5a
|
||||
/// spec, section 4), when there is one; the core tool schemas.
|
||||
/// spec, section 4), when there is one; the core tool schemas. The block is neutralised here
|
||||
/// (M5b spec, section 8): `soul.md` is not checked on write, and the snapshot of the baseline
|
||||
/// is what every request of the session sends.
|
||||
pub fn assemble_with(
|
||||
cfg: &Config,
|
||||
registry: &Registry,
|
||||
@@ -55,10 +58,10 @@ impl Baseline {
|
||||
let system = std::fs::read_to_string(&cfg.baseline.system)
|
||||
.map_err(|e| BaselineError::Read(cfg.baseline.system.clone(), e))?;
|
||||
let mut system = system.trim_end().to_string();
|
||||
let memory = memory.trim();
|
||||
let memory = neutralise(memory.trim());
|
||||
if !memory.is_empty() {
|
||||
system.push_str("\n\n");
|
||||
system.push_str(memory);
|
||||
system.push_str(&memory);
|
||||
}
|
||||
Ok(Baseline {
|
||||
system,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! One turn: ask, run any tool calls under their limits, and return.
|
||||
//! The loop is bounded every way a runaway can go: a cap on tool iterations, a repeated-call
|
||||
//! detector, the two M2a limits (thinking tokens, a full context), and a tool-result cap.
|
||||
//! Text from outside (the user's message, every tool result) is neutralised as it is first
|
||||
//! logged (M5b spec, section 8), so every later request replays the same bytes; the model's own
|
||||
//! output is logged as the server returned it.
|
||||
|
||||
use crate::baseline::messages;
|
||||
use crate::config::Config;
|
||||
@@ -8,6 +11,7 @@ use crate::llama::info::{CacheOutcome, cache_outcome};
|
||||
use crate::llama::{ChatEvent, ChatRequest, Client, InferError};
|
||||
use crate::session::{Session, SessionError};
|
||||
use crate::tools::{Registry, ToolPort, cap_result, denial_message, dispatch};
|
||||
use bxcore::sanitize::neutralise;
|
||||
use bxcore::{
|
||||
CallId, DataClass, LogRecord, Timestamp, ToolRequest, ToolResponse, TurnEvent, Usage,
|
||||
};
|
||||
@@ -118,7 +122,7 @@ pub fn run_turn(
|
||||
session
|
||||
.append(LogRecord::User {
|
||||
time: Timestamp::now(),
|
||||
content: content.to_string(),
|
||||
content: neutralise(content).into_owned(),
|
||||
})
|
||||
.map_err(TurnError::Session)?;
|
||||
|
||||
@@ -235,7 +239,11 @@ pub fn run_turn(
|
||||
run_call(rt, session, call, call_id, on_event)
|
||||
};
|
||||
|
||||
// Capped first, then neutralised: the logged text may be a few bytes over the cap,
|
||||
// and `truncated` says only whether the cap cut it. Denials and failures take this
|
||||
// path too, since they reach the model as tool messages as well.
|
||||
let (text, truncated) = cap_result(&text, rt.cfg.r#loop.tool_result_cap);
|
||||
let text = neutralise(&text).into_owned();
|
||||
session
|
||||
.append(LogRecord::ToolResult {
|
||||
time: Timestamp::now(),
|
||||
|
||||
Reference in New Issue
Block a user