feat(observe): surface delegation + A2A in the live world feed via audit poll
Edge-initiated inter-agent events (gated delegation, A2A ingress) bypass the
run loop, so the world SSE now polls the append-only audit log (cursor on the
BIGINT id, seeded to max on first pass) and emits:
- delegation.invoked -> agent.delegate {fromAgentId,toAgentId,toName,task}
- a2a.invoked -> a2a.invoked {agentId}
TeamObserver renders agent.delegate as an A->B handoff in the team timeline;
adds the agent.delegate taxonomy type. Reliable live (the synthesized-run path
never streamed — active_runs + first-sight cursor jump skip it).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7352ed47ab
commit
85b0e1ac33
@@ -287,6 +287,9 @@ pub async fn world_live(State(state): State<AppState>, Authed(user): Authed) ->
|
||||
let mut last: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
||||
// Per-run journal cursor so we stream only NEW run_events each poll.
|
||||
let mut cursors: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
|
||||
// Audit-log cursor for edge-initiated inter-agent events (delegation,
|
||||
// A2A) that bypass the run loop. -1 until seeded on the first pass.
|
||||
let mut audit_cursor: i64 = -1;
|
||||
loop {
|
||||
let roster = match cm_db::repo::agents::roster(&pool, ws).await {
|
||||
Ok(r) => r,
|
||||
@@ -387,6 +390,62 @@ pub async fn world_live(State(state): State<AppState>, Authed(user): Authed) ->
|
||||
json!({ "doorsPending": doors_pending(&pool, ws).await, "loops": runs.len() }),
|
||||
);
|
||||
|
||||
// Edge-initiated inter-agent events (gated delegation, A2A ingress)
|
||||
// bypass the run loop, so surface them from the append-only audit log.
|
||||
// On first sight jump the cursor to the current max so we stream
|
||||
// forward instead of replaying history.
|
||||
if audit_cursor < 0 {
|
||||
audit_cursor = sqlx::query_scalar(
|
||||
"SELECT coalesce(max(id), 0) FROM audit_log WHERE workspace_id = $1",
|
||||
)
|
||||
.bind(ws.as_uuid())
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
} else {
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, actor_id, event_type, subject_id, detail FROM audit_log
|
||||
WHERE workspace_id = $1 AND id > $2
|
||||
AND event_type IN ('delegation.invoked', 'a2a.invoked')
|
||||
ORDER BY id ASC LIMIT 100",
|
||||
)
|
||||
.bind(ws.as_uuid())
|
||||
.bind(audit_cursor)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
for row in &rows {
|
||||
let id: i64 = row.get("id");
|
||||
let et: String = row.get("event_type");
|
||||
let actor: Option<uuid::Uuid> = row.get("actor_id");
|
||||
let subject: String = row.get("subject_id");
|
||||
let detail: Value = row.get("detail");
|
||||
match et.as_str() {
|
||||
"delegation.invoked" => {
|
||||
yield sse("agent.delegate", json!({
|
||||
"fromAgentId": actor.map(|u| u.to_string()).unwrap_or_default(),
|
||||
"toAgentId": detail.get("to_id").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
"toName": subject,
|
||||
"task": detail.get("task").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
}));
|
||||
}
|
||||
"a2a.invoked" => {
|
||||
// subject_id is the claw_<id> alias → surface the target agent.
|
||||
let agent_id = subject
|
||||
.strip_prefix("claw_")
|
||||
.and_then(|h| uuid::Uuid::parse_str(h).ok())
|
||||
.map(|u| u.to_string())
|
||||
.unwrap_or_else(|| subject.clone());
|
||||
yield sse("a2a.invoked", json!({ "agentId": agent_id }));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if id > audit_cursor {
|
||||
audit_cursor = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
first = false;
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user