Skip to content

Commit 1e883f3

Browse files
authored
Add agent context handoff
Adds Agent Context Handoff v0 and follow-up review fixes.
1 parent c00fc90 commit 1e883f3

3 files changed

Lines changed: 639 additions & 5 deletions

File tree

native/agent-server-rust/src/main.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use anyhow::{Context, Result};
22
use chrono::{SecondsFormat, Utc};
3-
use native_host_rust::context_packet::{context_packet_paths, generate_source_context};
3+
use native_host_rust::context_packet::{
4+
context_packet_paths, generate_source_context, prepare_agent_context_handoff,
5+
AgentContextHandoff, AgentContextHandoffInput,
6+
};
47
use native_host_rust::protocol::{SourceContextResult, SummaryProviderSettings};
58
use serde::{Deserialize, Serialize};
69
use std::fs;
@@ -25,6 +28,10 @@ struct AgentRequest {
2528
id: String,
2629
command: AgentCommand,
2730
target_directory: Option<String>,
31+
meetings_root: Option<String>,
32+
task: Option<String>,
33+
project_hint: Option<String>,
34+
working_directory: Option<String>,
2835
summary_settings: Option<SummaryProviderSettings>,
2936
}
3037

@@ -33,6 +40,7 @@ struct AgentRequest {
3340
enum AgentCommand {
3441
Capabilities,
3542
GenerateContext,
43+
PrepareAgentContext,
3644
JobStatus,
3745
Shutdown,
3846
}
@@ -58,6 +66,8 @@ struct AgentPayload {
5866
job: Option<JobStatus>,
5967
#[serde(skip_serializing_if = "Option::is_none")]
6068
source_context: Option<SourceContextResult>,
69+
#[serde(skip_serializing_if = "Option::is_none")]
70+
handoff: Option<AgentContextHandoff>,
6171
}
6272

6373
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -156,6 +166,7 @@ fn handle_request(request: AgentRequest, running: &Arc<AtomicBool>) -> AgentResp
156166
agent_version: Some(env!("CARGO_PKG_VERSION")),
157167
job: None,
158168
source_context: None,
169+
handoff: None,
159170
}),
160171
),
161172
AgentCommand::GenerateContext => match generate_context_job(&request) {
@@ -165,6 +176,19 @@ fn handle_request(request: AgentRequest, running: &Arc<AtomicBool>) -> AgentResp
165176
agent_version: None,
166177
job: Some(job),
167178
source_context: Some(result),
179+
handoff: None,
180+
}),
181+
),
182+
Err(error) => failure_response(request.id, format!("{error:#}")),
183+
},
184+
AgentCommand::PrepareAgentContext => match prepare_agent_context(&request) {
185+
Ok(handoff) => success_response(
186+
request.id,
187+
Some(AgentPayload {
188+
agent_version: None,
189+
job: None,
190+
source_context: None,
191+
handoff: Some(handoff),
168192
}),
169193
),
170194
Err(error) => failure_response(request.id, format!("{error:#}")),
@@ -176,6 +200,7 @@ fn handle_request(request: AgentRequest, running: &Arc<AtomicBool>) -> AgentResp
176200
agent_version: None,
177201
job,
178202
source_context: None,
203+
handoff: None,
179204
}),
180205
),
181206
Err(error) => failure_response(request.id, format!("{error:#}")),
@@ -188,12 +213,34 @@ fn handle_request(request: AgentRequest, running: &Arc<AtomicBool>) -> AgentResp
188213
agent_version: Some(env!("CARGO_PKG_VERSION")),
189214
job: None,
190215
source_context: None,
216+
handoff: None,
191217
}),
192218
)
193219
}
194220
}
195221
}
196222

223+
fn prepare_agent_context(request: &AgentRequest) -> Result<AgentContextHandoff> {
224+
let meetings_root = request
225+
.meetings_root
226+
.as_deref()
227+
.context("A meetings root directory is required.")?;
228+
let task = request
229+
.task
230+
.as_deref()
231+
.filter(|task| !task.trim().is_empty())
232+
.context("A task is required to prepare agent context.")?;
233+
let result = prepare_agent_context_handoff(
234+
Path::new(meetings_root),
235+
AgentContextHandoffInput {
236+
task: task.to_string(),
237+
project_hint: request.project_hint.clone(),
238+
working_directory: request.working_directory.clone(),
239+
},
240+
)?;
241+
Ok(result.handoff)
242+
}
243+
197244
fn generate_context_job(request: &AgentRequest) -> Result<(JobStatus, SourceContextResult)> {
198245
let target_directory = request
199246
.target_directory
@@ -364,4 +411,46 @@ mod tests {
364411
let temp = tempfile::tempdir().unwrap();
365412
assert!(read_job_status(temp.path()).unwrap().is_none());
366413
}
414+
415+
#[test]
416+
fn prepare_agent_context_request_writes_handoff_files() {
417+
let temp = tempfile::tempdir().unwrap();
418+
let meetings_root = temp.path();
419+
let project = meetings_root
420+
.join("projects")
421+
.join("mirrornote-agent-runtime");
422+
fs::create_dir_all(&project).unwrap();
423+
fs::write(
424+
project.join("source-index.jsonl"),
425+
"{\"id\":\"meeting:note-1\",\"type\":\"meeting\",\"title\":\"Agent Runtime Sync\",\"observedAt\":\"2026-05-05T01:00:00Z\",\"projectHint\":\"MirrorNote Agent Runtime\"}\n",
426+
)
427+
.unwrap();
428+
fs::write(
429+
project.join("memory-objects.jsonl"),
430+
"{\"id\":\"meeting:note-1::decision-1\",\"type\":\"decision\",\"title\":\"Keep context automatic\",\"body\":\"Keep context automatic\",\"status\":\"active\",\"confidence\":0.95,\"evidenceCoverage\":\"direct\",\"sourceRefs\":[{\"sourceId\":\"meeting:note-1\",\"sourceType\":\"meeting\",\"location\":{\"kind\":\"metadata\"}}]}\n",
431+
)
432+
.unwrap();
433+
434+
let response = handle_request(
435+
AgentRequest {
436+
id: "request-1".to_string(),
437+
command: AgentCommand::PrepareAgentContext,
438+
target_directory: None,
439+
meetings_root: Some(meetings_root.to_string_lossy().into_owned()),
440+
task: Some("Run Codex with prior context".to_string()),
441+
project_hint: Some("MirrorNote Agent Runtime".to_string()),
442+
working_directory: Some("/tmp/MirrorNote".to_string()),
443+
summary_settings: None,
444+
},
445+
&Arc::new(AtomicBool::new(true)),
446+
);
447+
448+
assert!(response.ok);
449+
let payload = response.payload.unwrap();
450+
let handoff = payload.handoff.unwrap();
451+
assert_eq!(handoff.project_id, "mirrornote-agent-runtime");
452+
assert_eq!(handoff.decisions.len(), 1);
453+
assert!(project.join("handoffs").join("handoff.json").exists());
454+
assert!(project.join("handoffs").join("handoff.md").exists());
455+
}
367456
}

0 commit comments

Comments
 (0)