use super::prompt_read::PromptReadContext;
use super::recall::RecallSource;
use super::scheduled::{plan_block, StepLedger};
use super::scratchpad::{scratchpad_state_block, ScratchpadStore};
const RESUME_DEFAULT_TURNS: usize = 6;
const RESUME_CONTEXT_DESCRIPTION: &str =
"Recover what you were working on — returns this conversation's recent \
turns, the active prompt address/root/digest, the submitted receipt's \
origin and previous/parent/root links, your current <plan>, and saved \
<state>. Use prompt_read on a returned address for exact text. For \
prompt-rooted work, use artifact_read {\"address\":\"root\"} to recover \
the objective's artifact chain. Use this \
after a resume or when you've lost the thread. No args.";
pub fn resume_context_tool_definition() -> serde_json::Value {
serde_json::json!({
"type": "function",
"function": {
"name": "resume_context",
"description": RESUME_CONTEXT_DESCRIPTION,
"parameters": { "type": "object", "properties": {}, "required": [] }
}
})
}
pub(crate) fn execute_resume_context(
recall_source: Option<&dyn RecallSource>,
step_ledger: Option<&dyn StepLedger>,
scratchpad_store: Option<&dyn ScratchpadStore>,
prompt_context: Option<PromptReadContext<'_>>,
_color: bool,
_tool_output_lines: usize,
) -> String {
let mut out = String::from("Recovering what this conversation was working on.\n");
out.push_str(
"For prompt-rooted work, recover this objective's artifact chain with \
artifact_read {\"address\":\"root\"}.\n",
);
if let Some(context) = prompt_context {
if let Some(receipt) = context.active_receipt() {
out.push_str(&format!(
"\n— active operator prompt —\naddress: {}\nroot: {}\nmodel_digest: {}\n\
exact text: prompt_read {{\"address\":\"current\"}}\n",
receipt.id(),
receipt.root_prompt_id(),
receipt.model_digest()
));
if let Some(submitted) = context.submitted_receipt() {
out.push_str(&format!(
"\n— submitted request receipt —\naddress: {}\norigin: {}\n\
previous_address: {}\nparent_address: {}\nroot_address: {}\n\
exact submitted text: prompt_read {{\"address\":\"submitted\"}}\n",
submitted.id(),
prompt_origin_name(submitted.origin()),
display_optional_prompt_id(submitted.previous_prompt_id()),
display_optional_prompt_id(submitted.parent_prompt_id()),
submitted.root_prompt_id(),
));
if submitted.previous_prompt_id().is_some() {
out.push_str(
"recover chronological predecessor: prompt_read \
{\"address\":\"previous\"}\n",
);
}
if submitted.parent_prompt_id().is_some() {
out.push_str("recover semantic parent: prompt_read {\"address\":\"parent\"}\n");
}
}
} else {
out.push_str(
"\n— active operator prompt —\nexact text: prompt_read \
{\"address\":\"current\"} (ephemeral; no durable address)\n\
\n— submitted request receipt —\naddress: null\norigin: null\n\
previous_address: null\nparent_address: null\nroot_address: null\n",
);
}
}
out.push_str("\n— recent turns (oldest first) —\n");
match recall_source {
Some(source) => match source.this_conversation_recent(RESUME_DEFAULT_TURNS) {
Ok(hits) if !hits.is_empty() => {
for h in &hits {
out.push_str(&format!("[{}] {}\n", h.seq, h.snippet));
}
}
Ok(_) => out.push_str("(no earlier turns recorded yet)\n"),
Err(e) => out.push_str(&format!("(could not load earlier turns: {e})\n")),
},
None => out.push_str(
"(no conversation history available this session — headless / eval \
sessions may have none)\n",
),
}
if let Some(block) = step_ledger.and_then(plan_block) {
out.push('\n');
out.push_str(&block);
out.push('\n');
}
if let Some(block) = scratchpad_store.and_then(scratchpad_state_block) {
out.push('\n');
out.push_str(&block);
out.push('\n');
}
out
}
fn display_optional_prompt_id(id: Option<crate::PromptId>) -> String {
id.map_or_else(|| "null".to_string(), |id| id.to_string())
}
fn prompt_origin_name(origin: crate::PromptOrigin) -> &'static str {
match origin {
crate::PromptOrigin::Operator => "operator",
crate::PromptOrigin::HarnessRetry => "harness_retry",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::SearchHit;
use std::sync::Mutex;
#[derive(Default)]
struct RecentMock {
recent: Vec<SearchHit>,
fail_with: Option<String>,
calls: Mutex<Vec<usize>>,
}
impl RecallSource for RecentMock {
fn search(&self, _query: &str, _limit: usize) -> anyhow::Result<Vec<SearchHit>> {
Ok(Vec::new())
}
fn this_conversation_recent(&self, limit: usize) -> anyhow::Result<Vec<SearchHit>> {
self.calls.lock().unwrap().push(limit);
match &self.fail_with {
Some(e) => Err(anyhow::anyhow!("{e}")),
None => Ok(self.recent.iter().take(limit).cloned().collect()),
}
}
}
fn hit(seq: i64, snippet: &str) -> SearchHit {
SearchHit {
conversation_id: "conv-x".to_string(),
title: "the task".to_string(),
seq,
snippet: snippet.to_string(),
rank: 0.0,
}
}
#[test]
fn definition_is_argless_and_named() {
let def = resume_context_tool_definition();
assert_eq!(def["function"]["name"], "resume_context");
assert_eq!(
def["function"]["parameters"]["properties"],
serde_json::json!({})
);
let desc = def["function"]["description"].as_str().unwrap();
assert!(desc.contains("Recover what you were working on"), "{desc}");
assert!(
desc.contains("<plan>") && desc.contains("<state>"),
"{desc}"
);
assert!(
desc.contains("artifact_read {\"address\":\"root\"}"),
"{desc}"
);
}
#[test]
fn renders_turns_plan_and_state_together() {
use crate::agentic::scheduled::SessionStepLedger;
use crate::agentic::scratchpad::SessionScratchpadStore;
let source = RecentMock {
recent: vec![hit(1, "you: start it\n me: ok"), hit(2, "you: more")],
..Default::default()
};
let ledger = SessionStepLedger::default();
ledger.set_plan(&["scope it".to_string(), "build it".to_string()]);
let scratch = SessionScratchpadStore::default();
scratch.set("current_task", "wire the PyO3 bindings".to_string());
let out = execute_resume_context(
Some(&source),
Some(&ledger as &dyn StepLedger),
Some(&scratch as &dyn ScratchpadStore),
None,
false,
20,
);
assert!(out.contains("— recent turns (oldest first) —"), "{out}");
assert!(out.contains("[1] you: start it"), "{out}");
assert!(out.contains("[2] you: more"), "{out}");
assert!(
out.contains("<plan>") && out.contains("→ 1. scope it"),
"{out}"
);
assert!(
out.contains("<state>") && out.contains("current_task: wire the PyO3 bindings"),
"{out}"
);
assert_eq!(*source.calls.lock().unwrap(), vec![RESUME_DEFAULT_TURNS]);
assert_eq!(ledger.count(), 2, "resume_context does not mutate the plan");
assert_eq!(scratch.keys_count(), 1, "or the scratchpad");
}
#[test]
fn no_source_returns_clear_no_history_message() {
let out = execute_resume_context(None, None, None, None, false, 20);
assert!(
out.contains("no conversation history available this session"),
"{out}"
);
}
#[test]
fn resume_names_active_and_submitted_prompt_handles_without_copying_text() {
let operator = crate::TurnPromptContext::ephemeral_operator(
"conv",
"raw operator secret",
"exact operator secret",
);
let retry = crate::TurnPromptContext::ephemeral_harness_retry(
"conv",
"retry raw",
"please continue",
&operator,
)
.unwrap();
let context = PromptReadContext::new(Some(&retry), "wrong fallback", None);
let out = execute_resume_context(None, None, None, Some(context), false, 20);
assert!(out.contains(&operator.active().id().to_string()), "{out}");
assert!(out.contains(operator.active().model_digest()), "{out}");
assert!(out.contains(&retry.submitted().id().to_string()), "{out}");
assert!(out.contains("origin: harness_retry"), "{out}");
assert!(
out.contains(&format!("previous_address: {}", operator.submitted().id())),
"{out}"
);
assert!(
out.contains(&format!("parent_address: {}", operator.submitted().id())),
"{out}"
);
assert!(out.contains("recover chronological predecessor"), "{out}");
assert!(out.contains("prompt_read"), "{out}");
assert!(
out.contains("artifact_read {\"address\":\"root\"}"),
"{out}"
);
assert!(!out.contains("exact operator secret"), "{out}");
assert!(!out.contains("please continue"), "{out}");
}
#[test]
fn empty_sources_render_without_panicking() {
let source = RecentMock::default();
let out = execute_resume_context(Some(&source), None, None, None, false, 20);
assert!(out.contains("(no earlier turns recorded yet)"), "{out}");
assert!(!out.contains("<plan>"), "{out}");
assert!(!out.contains("<state>"), "{out}");
}
#[test]
fn turn_load_failure_is_folded_in_not_aborted() {
use crate::agentic::scheduled::SessionStepLedger;
let source = RecentMock {
fail_with: Some("db is on fire".to_string()),
..Default::default()
};
let ledger = SessionStepLedger::default();
ledger.set_plan(&["recover".to_string()]);
let out = execute_resume_context(
Some(&source),
Some(&ledger as &dyn StepLedger),
None,
None,
false,
20,
);
assert!(
out.contains("could not load earlier turns: db is on fire"),
"{out}"
);
assert!(out.contains("<plan>"), "{out}");
}
}