Compare commits

...

1 Commits

Author SHA1 Message Date
kh.ai
296d267608 collab: add fresh_context to send_input 2026-01-28 19:51:36 -08:00
6 changed files with 112 additions and 0 deletions

View File

@@ -92,6 +92,11 @@ impl AgentControl {
result
}
pub(crate) async fn reset_conversation(&self, agent_id: ThreadId) -> CodexResult<String> {
let state = self.upgrade()?;
state.send_op(agent_id, Op::ResetConversation).await
}
/// Interrupt the current task for an existing agent thread.
pub(crate) async fn interrupt_agent(&self, agent_id: ThreadId) -> CodexResult<String> {
let state = self.upgrade()?;

View File

@@ -2381,6 +2381,9 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
Op::ThreadRollback { num_turns } => {
handlers::thread_rollback(&sess, sub.id.clone(), num_turns).await;
}
Op::ResetConversation => {
handlers::reset_conversation(&sess, sub.id.clone()).await;
}
Op::RunUserShellCommand { command } => {
handlers::run_user_shell_command(
&sess,
@@ -2893,6 +2896,29 @@ mod handlers {
.await;
}
pub async fn reset_conversation(sess: &Arc<Session>, sub_id: String) {
let has_active_turn = { sess.active_turn.lock().await.is_some() };
if has_active_turn {
sess.send_event_raw(Event {
id: sub_id,
msg: EventMsg::Warning(WarningEvent {
message: "Cannot reset conversation while a turn is in progress.".to_string(),
}),
})
.await;
return;
}
let turn_context = sess.new_default_turn_with_sub_id(sub_id).await;
let initial_context = sess.build_initial_context(&turn_context).await;
sess.replace_history(initial_context).await;
{
let mut state = sess.state.lock().await;
state.initial_context_seeded = true;
}
sess.recompute_token_usage(turn_context.as_ref()).await;
}
pub async fn shutdown(sess: &Arc<Session>, sub_id: String) -> bool {
sess.abort_all_tasks(TurnAbortReason::Interrupted).await;
sess.services

View File

@@ -201,6 +201,8 @@ mod send_input {
message: String,
#[serde(default)]
interrupt: bool,
#[serde(default)]
fresh_context: bool,
}
#[derive(Debug, Serialize)]
@@ -222,6 +224,30 @@ mod send_input {
"Empty message can't be sent to an agent".to_string(),
));
}
if args.interrupt && args.fresh_context {
return Err(FunctionCallError::RespondToModel(
"fresh_context cannot be combined with interrupt; interrupt first, wait, then retry".to_string(),
));
}
if args.fresh_context {
let status = session
.services
.agent_control
.get_status(receiver_thread_id)
.await;
if matches!(status, AgentStatus::Running) {
return Err(FunctionCallError::RespondToModel(
"agent must not be running to reset context; wait for completion then retry"
.to_string(),
));
}
session
.services
.agent_control
.reset_conversation(receiver_thread_id)
.await
.map_err(|err| collab_agent_error(receiver_thread_id, err))?;
}
if args.interrupt {
session
.services
@@ -898,6 +924,45 @@ mod tests {
.expect("shutdown should submit");
}
#[tokio::test]
async fn send_input_resets_before_prompt_when_fresh_context_set() {
let (mut session, turn) = make_session_and_context().await;
let manager = thread_manager();
session.services.agent_control = manager.agent_control();
let config = turn.client.config().as_ref().clone();
let thread = manager.start_thread(config).await.expect("start thread");
let agent_id = thread.thread_id;
let invocation = invocation(
Arc::new(session),
Arc::new(turn),
"send_input",
function_payload(json!({
"id": agent_id.to_string(),
"message": "hi",
"fresh_context": true
})),
);
CollabHandler
.handle(invocation)
.await
.expect("send_input should succeed");
let ops = manager.captured_ops();
let ops_for_agent: Vec<&Op> = ops
.iter()
.filter_map(|(id, op)| (*id == agent_id).then_some(op))
.collect();
assert_eq!(ops_for_agent.len(), 2);
assert!(matches!(ops_for_agent[0], Op::ResetConversation));
assert!(matches!(ops_for_agent[1], Op::UserInput { .. }));
let _ = thread
.thread
.submit(Op::Shutdown {})
.await
.expect("shutdown should submit");
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct WaitResult {
status: HashMap<ThreadId, AgentStatus>,

View File

@@ -503,6 +503,15 @@ fn create_send_input_tool() -> ToolSpec {
),
},
);
properties.insert(
"fresh_context".to_string(),
JsonSchema::Boolean {
description: Some(
"When true, clear the agent's in-memory context before sending this message."
.to_string(),
),
},
);
ToolSpec::Function(ResponsesApiTool {
name: "send_input".to_string(),

View File

@@ -97,6 +97,7 @@ Sub-agents are their to make you go fast and time is a big constraint so leverag
- When you ask sub-agent to do the work for you, your only role becomes to coordinate them. Do not perform the actual work while they are working.
- When you have plan with multiple step, process them in parallel by spawning one agent per step when this is possible.
- Choose the correct agent type.
- When reusing a small agent pool across many independent tasks, prefer `send_input` with `fresh_context=true` so each task runs with a clean context.
## Flow
1. Understand the task.

View File

@@ -281,6 +281,12 @@ pub enum Op {
/// responsible for undoing any edits on disk.
ThreadRollback { num_turns: u32 },
/// Reset the in-memory conversation context back to the initial session prefix.
///
/// This clears prior user/assistant turns without changing the session configuration.
/// Useful for reusing agents as stateless workers across many independent tasks.
ResetConversation,
/// Request a code review from the agent.
Review { review_request: ReviewRequest },