feat(13-10): node-side chat history, scoped by caller, compacted not truncated
history.rs persists the ChatMessage transcript under data_dir (D-08), keyed by a HistoryKey derived from CallerScope so an operator's AIUI session and a mesh peer's transcript are structurally distinct files, not two rows a filter could forget. Writes are atomic (temp sibling + rename, matching music/index.rs::save_atomic's precedent) and 0600, following grants.rs's convention. Tool results longer than MAX_TOOL_RESULT_CHARS are truncated with a visible marker before entering history -- a new, assistant-scoped constant, never assist.rs's LoRa-airtime-tuned reply cap. Once the transcript exceeds KEEP_VERBATIM_TURNS, older turns fold into a running summary extended incrementally as turns age out, never regenerated from the full transcript. Wallet/files-category tool-call arguments are never persisted (AI-SPEC §7b's field policy applied to storage, not only tracing) -- categories are resolved by the caller from the same tools registry execute_tool uses, so history.rs never re-derives a second, driftable category list. Nothing reachable from confirm.rs's pending- confirmation state has a parameter path into this module at all (S-09 stays true structurally). assistant.history / assistant.clear-history route through 13-01's existing assistant.* dispatcher arm (dispatcher.rs untouched), each scoped to the calling session's own HistoryKey. run_loop (loop_.rs) now returns (answer, full_history) instead of just the answer string -- structurally necessary so chat() (mod.rs) can persist the tool-call/tool-result messages the loop built internally, not only the user question and final answer (Rule 3, mirroring 13-05's precedent of touching a file outside its own plan's files_modified list when the plan's own intent requires it). chat() persists this turn after run_loop returns; it does not yet feed prior persisted turns back into live model context -- a documented, deliberately scoped follow-up (see mod.rs's chat() doc comment and the plan SUMMARY). 8 new tests under assistant::history::tests::, including operator_and_mesh_transcripts_are_separate and wallet_tool_arguments_never_reach_the_transcript (asserted against both the deserialized struct and the raw on-disk bytes). Full assistant:: suite: 50/50 (42 baseline-after-Task-1 + 8 new); confirm::tests:: restart_drops_pending_not_executes still passes -- S-09 not weakened. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
821d8700ce
commit
3da9928cc9
@@ -13,6 +13,7 @@
|
||||
pub mod backends;
|
||||
pub mod confirm;
|
||||
pub mod grants;
|
||||
pub mod history;
|
||||
pub mod loop_;
|
||||
pub mod tools;
|
||||
|
||||
@@ -263,9 +264,20 @@ pub fn build_system_prompt(visible_tools: &[tools::ToolDef]) -> String {
|
||||
/// Entry point: run one chat turn for `caller` through the shared loop.
|
||||
/// Builds the visible-tool set from the caller's granted categories only
|
||||
/// (D-16 — the model should never even see a tool it can't use), selects a
|
||||
/// backend per D-04's chain (Ollama first, Claude fallback), and runs it to
|
||||
/// a final answer. 13-10 Task 2 adds D-08 history persistence on top of
|
||||
/// this.
|
||||
/// backend per D-04's chain (Ollama first, Claude fallback), runs it to a
|
||||
/// final answer, and persists the completed turn to this caller's own
|
||||
/// `history.rs` transcript (D-08/D-02) before returning.
|
||||
///
|
||||
/// Scoping note: this seeds the model's context with only the NEW user
|
||||
/// message, not the caller's prior persisted turns — the persistence
|
||||
/// side of D-08 (append/load/compact/redact/truncate, `assistant.history`/
|
||||
/// `assistant.clear-history`) is fully wired, but feeding loaded history
|
||||
/// back into a live multi-turn conversation is left for a follow-up (see
|
||||
/// 13-10-SUMMARY.md's Next Phase Readiness): reconstructing prior
|
||||
/// `ToolCall`/`ToolResult` pairs from the persisted, id-less record in a
|
||||
/// way that stays correct against Claude's strict `tool_use`/`tool_result`
|
||||
/// id-pairing requirement needs its own test budget, and none of Task 2's
|
||||
/// `<behavior>` bullets require it this plan.
|
||||
pub async fn chat(
|
||||
handler: Arc<RpcHandler>,
|
||||
caller: CallerScope,
|
||||
@@ -280,23 +292,48 @@ pub async fn chat(
|
||||
|
||||
let system_prompt = build_system_prompt(&visible_tools);
|
||||
|
||||
let history = vec![tools::ChatMessage {
|
||||
let key = history::HistoryKey::from_caller(&caller);
|
||||
|
||||
let turn_history = vec![tools::ChatMessage {
|
||||
role: tools::Role::User,
|
||||
text: Some(user_text),
|
||||
tool_calls: vec![],
|
||||
tool_results: vec![],
|
||||
}];
|
||||
|
||||
let ctx = ToolExecCtx::new(registry, caller, handler);
|
||||
let ctx = ToolExecCtx::new(registry, caller, handler.clone());
|
||||
|
||||
loop_::run_loop(
|
||||
let (answer, turn_messages) = loop_::run_loop(
|
||||
backend.as_ref(),
|
||||
&system_prompt,
|
||||
&visible_tools,
|
||||
history,
|
||||
turn_history,
|
||||
&ctx,
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
// D-08: persist this completed turn (the user's message, any
|
||||
// intermediate tool-call/tool-result messages, and the final answer)
|
||||
// — never the pending-confirmation state, which this function has no
|
||||
// access to in the first place (see history.rs's module doc; S-09
|
||||
// stays true structurally).
|
||||
let tool_categories: std::collections::HashMap<String, PermissionCategory> = tools::registry()
|
||||
.all()
|
||||
.into_iter()
|
||||
.map(|t| (t.name.to_string(), t.category))
|
||||
.collect();
|
||||
let mut hist = history::History::load(handler.data_dir(), &key).await;
|
||||
if let Err(e) = hist
|
||||
.append(handler.data_dir(), &key, &turn_messages, &tool_categories)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
error = %e,
|
||||
"assistant.chat: failed to persist this turn's history (the answer is still returned to the caller)"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(answer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -503,7 +540,7 @@ mod tests {
|
||||
gate2
|
||||
.resolve(&snap2.req_id, &snap2.nonce, false)
|
||||
.expect("decline resolves");
|
||||
let answer = loop_task.await.expect("join").expect("run_loop");
|
||||
let (answer, _history) = loop_task.await.expect("join").expect("run_loop");
|
||||
assert_eq!(answer, "Understood — I did not restart it.");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user