Skip to content

Commit 7372de9

Browse files
committed
Make remote agent task refs followable
1 parent 8edd220 commit 7372de9

3 files changed

Lines changed: 123 additions & 12 deletions

File tree

‎crates/mesh-agents-cli/src/a2a.rs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ impl LocalA2aTools {
270270
.with_context(|| {
271271
format!("remote task `{task_id}` was not found for agent `{agent_id}`")
272272
})?;
273-
return serde_json::from_value(remote.result)
273+
return decode_remote_task(remote.result)
274274
.with_context(|| format!("failed to decode remote task `{task_id}`"));
275275
}
276276
let path = agent_task_store_path(&self.data_dir, agent_id);
@@ -313,6 +313,13 @@ impl LocalA2aTools {
313313
}
314314
}
315315

316+
fn decode_remote_task(value: Value) -> Result<Task> {
317+
if let Some(task) = value.get("task") {
318+
return serde_json::from_value(task.clone()).context("failed to decode wrapped task");
319+
}
320+
serde_json::from_value(value).context("failed to decode task")
321+
}
322+
316323
#[derive(Clone, Debug, Default)]
317324
struct AgentGates {
318325
gates: Arc<Mutex<std::collections::HashMap<String, Arc<AgentGate>>>>,

‎crates/mesh-agents-cli/src/mesh.rs‎

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,24 +137,46 @@ impl RemoteTaskCache {
137137
}
138138

139139
pub(crate) fn upsert(&mut self, task: RemoteTaskRecord) {
140-
if let Some(existing) = self
141-
.tasks
142-
.iter_mut()
143-
.find(|existing| existing.agent_id == task.agent_id && existing.task_id == task.task_id)
144-
{
140+
if let Some(existing) = self.tasks.iter_mut().find(|existing| {
141+
existing.agent_id == task.agent_id
142+
&& (existing.task_id == task.task_id
143+
|| same_nonempty(&existing.correlation_id, &task.correlation_id))
144+
}) {
145145
*existing = task;
146146
} else {
147147
self.tasks.push(task);
148148
}
149149
}
150150

151+
pub(crate) fn upsert_pending(
152+
&mut self,
153+
agent_id: String,
154+
peer_id: String,
155+
correlation_id: String,
156+
result: Value,
157+
) {
158+
self.upsert(RemoteTaskRecord {
159+
agent_id,
160+
task_id: correlation_id.clone(),
161+
peer_id,
162+
correlation_id,
163+
result,
164+
updated_at_ms: now_ms(),
165+
});
166+
}
167+
151168
pub(crate) fn get(&self, agent_id: &str, task_id: &str) -> Option<&RemoteTaskRecord> {
152-
self.tasks
153-
.iter()
154-
.find(|task| task.agent_id == agent_id && task.task_id == task_id)
169+
self.tasks.iter().find(|task| {
170+
task.agent_id == agent_id
171+
&& (task.task_id == task_id || same_nonempty(&task.correlation_id, task_id))
172+
})
155173
}
156174
}
157175

176+
fn same_nonempty(left: &str, right: &str) -> bool {
177+
!left.is_empty() && left == right
178+
}
179+
158180
pub(crate) fn local_advertisements(
159181
agents_dir: &Path,
160182
source_peer_id: &str,
@@ -321,4 +343,53 @@ public_mesh = false
321343
assert_eq!(summaries[0]["agent_id"], "private");
322344
assert_eq!(summaries[0]["location"], "local");
323345
}
346+
347+
#[test]
348+
fn remote_tasks_can_be_read_by_correlation_id() {
349+
let mut cache = RemoteTaskCache::default();
350+
cache.upsert(RemoteTaskRecord {
351+
agent_id: "pr-review".to_string(),
352+
task_id: "task-1".to_string(),
353+
peer_id: "peer-a".to_string(),
354+
correlation_id: "remote-1".to_string(),
355+
result: json!({ "task": { "id": "task-1" } }),
356+
updated_at_ms: 1,
357+
});
358+
359+
let task = cache
360+
.get("pr-review", "remote-1")
361+
.expect("correlation id should resolve");
362+
363+
assert_eq!(task.task_id, "task-1");
364+
}
365+
366+
#[test]
367+
fn remote_task_response_replaces_pending_correlation_record() {
368+
let mut cache = RemoteTaskCache::default();
369+
cache.upsert_pending(
370+
"pr-review".to_string(),
371+
"peer-a".to_string(),
372+
"remote-1".to_string(),
373+
json!({ "task": { "id": "remote-1" } }),
374+
);
375+
376+
cache.upsert(RemoteTaskRecord {
377+
agent_id: "pr-review".to_string(),
378+
task_id: "task-1".to_string(),
379+
peer_id: "peer-a".to_string(),
380+
correlation_id: "remote-1".to_string(),
381+
result: json!({ "task": { "id": "task-1" } }),
382+
updated_at_ms: 2,
383+
});
384+
385+
assert_eq!(cache.tasks.len(), 1);
386+
assert_eq!(
387+
cache.get("pr-review", "remote-1").unwrap().task_id,
388+
"task-1"
389+
);
390+
assert_eq!(
391+
cache.get("pr-review", "task-1").unwrap().correlation_id,
392+
"remote-1"
393+
);
394+
}
324395
}

‎crates/mesh-agents-cli/src/plugin.rs‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -422,13 +422,24 @@ async fn plugin_send_message(
422422
)?;
423423
message.correlation_id = correlation_id.clone();
424424
context.send_channel_message(message).await?;
425+
let result = pending_remote_task_result(&args.agent_id, &correlation_id, &remote.peer_id);
426+
let mut cache = RemoteTaskCache::load(&state.data_dir)?;
427+
cache.upsert_pending(
428+
args.agent_id.clone(),
429+
remote.peer_id.clone(),
430+
correlation_id.clone(),
431+
result.clone(),
432+
);
433+
cache.save(&state.data_dir)?;
425434
Ok(json!({
426435
"agent_id": args.agent_id,
427436
"location": "remote",
428437
"peer_id": remote.peer_id,
438+
"task_id": correlation_id.clone(),
429439
"correlation_id": correlation_id,
430440
"status": "submitted",
431-
"note": "remote task result will be available through agents.get_task after the owner node replies",
441+
"result": result,
442+
"note": "task_id is a followable pending task reference; agents.get_task will resolve it to the final A2A task after the owner node replies",
432443
}))
433444
}
434445

@@ -449,6 +460,8 @@ async fn plugin_get_task(state: &PluginState, agent_id: &str, task_id: &str) ->
449460
"agent_id": agent_id,
450461
"location": "remote",
451462
"peer_id": remote.peer_id,
463+
"task_id": remote.task_id,
464+
"correlation_id": remote.correlation_id,
452465
"task": remote.result,
453466
}))
454467
}
@@ -530,13 +543,32 @@ fn decode_task_value(value: Value) -> Result<Task> {
530543
serde_json::from_value(value).context("failed to decode task")
531544
}
532545

546+
fn pending_remote_task_result(agent_id: &str, correlation_id: &str, peer_id: &str) -> Value {
547+
json!({
548+
"task": {
549+
"id": correlation_id,
550+
"contextId": correlation_id,
551+
"status": {
552+
"state": "TASK_STATE_SUBMITTED",
553+
},
554+
"metadata": {
555+
"mesh_llm_pending_remote": true,
556+
"agent_id": agent_id,
557+
"peer_id": peer_id,
558+
"correlation_id": correlation_id,
559+
},
560+
},
561+
})
562+
}
563+
533564
async fn send_remote_message_stream(
534565
state: &PluginState,
535566
remote: &crate::mesh::RemoteAgentAd,
536567
args: &SendMessageArgs,
537568
context: &mut PluginContext<'_>,
538569
) -> Result<Value> {
539570
let stream_id = format!("a2a-{}-{}", args.agent_id, now_ms());
571+
let correlation_id = format!("remote-{}", now_ms());
540572
let metadata = MeshStreamRequest::SendMessage {
541573
agent_id: args.agent_id.clone(),
542574
message: args.message.clone(),
@@ -552,7 +584,7 @@ async fn send_remote_message_stream(
552584
mode: mesh_llm_plugin::proto::StreamMode::EventStream as i32,
553585
bidirectional: false,
554586
content_type: Some("text/event-stream".to_string()),
555-
correlation_id: Some(format!("remote-{}", now_ms())),
587+
correlation_id: Some(correlation_id.clone()),
556588
metadata_json: Some(serde_json::to_string(&metadata)?),
557589
expected_bytes: None,
558590
idle_timeout_ms: Some(300_000),
@@ -579,7 +611,7 @@ async fn send_remote_message_stream(
579611
agent_id: args.agent_id.clone(),
580612
task_id: task_id.clone(),
581613
peer_id: remote.peer_id.clone(),
582-
correlation_id: String::new(),
614+
correlation_id: correlation_id.clone(),
583615
result: final_result.clone(),
584616
updated_at_ms: now_ms(),
585617
});
@@ -592,6 +624,7 @@ async fn send_remote_message_stream(
592624
"peer_id": remote.peer_id,
593625
"transport": "mesh_stream",
594626
"task_id": task_id,
627+
"correlation_id": correlation_id,
595628
"result": final_result,
596629
"events": events,
597630
}))

0 commit comments

Comments
 (0)