mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Wave 2 features: A2A live view (CEO chime-in + reply budget) and prompter memory (#297)
* feat(a2a): live view — watch fleet conversations, CEO chime-in, reply budget A2A_MESSAGE_SENT published from A2AService.send (excerpt-capped) and fanned through the existing /ws/system bridge; CEO-only admin REST for conversations/messages + a reply route on the publish-bearing send path; panel /a2a page with live transcript and a composer gated on task-linked conversations. The matrix gains its one asymmetric rule: CEO may message anyone, nobody may target the CEO — and agent replies inside a CEO-opened conversation are hard-budgeted to one per CEO message (per conversation, per agent), rejected with wait-don't-retry guidance. Built subagent-driven (Sonnet 5), reviewed; v1 seams documented in the map delta. * feat(prompter): intake remembers the task history Intake spawns now carry a per-project chronological digest of recent tasks (capped: 15 lines/project, 4000 chars total — ~300-1000 tokens) merged into the ambient layer, and the interviewer gets a bounded search_past_tasks tool (one shared implementation behind the grok MCP tool and the Claude SDK in-process tool) to check precedent mid-conversation. Informational memory only — the sequencing analyzer keeps ownership of ordering. Built subagent-driven (Sonnet 5), reviewed; pre-existing conventions-ambient MegaTask-scope gap flagged, untouched. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { a2aApi, type AdminReplyRequest } from "@/lib/api/a2a";
|
||||
|
||||
export const a2aLiveKeys = {
|
||||
all: ["a2a-live"] as const,
|
||||
conversations: ["a2a-live", "conversations"] as const,
|
||||
messages: (conversationId: string) =>
|
||||
["a2a-live", "messages", conversationId] as const,
|
||||
};
|
||||
|
||||
// Conversation list — refreshed by WS `a2a.message` invalidation and the
|
||||
// manual Refresh button; a short staleTime keeps remounts reasonably fresh.
|
||||
export function useA2AConversations(limit?: number) {
|
||||
return useQuery({
|
||||
queryKey: [...a2aLiveKeys.conversations, limit ?? 50],
|
||||
queryFn: () => a2aApi.listAdminConversations(limit),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
// Transcript for one conversation. WS frames for the selected conversation
|
||||
// invalidate this key; full bodies always come from REST (excerpts are capped).
|
||||
export function useA2AMessages(conversationId: string | null) {
|
||||
return useQuery({
|
||||
queryKey: a2aLiveKeys.messages(conversationId || ""),
|
||||
queryFn: () => a2aApi.listAdminMessages(conversationId!),
|
||||
enabled: !!conversationId,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
export interface ReplyAsCeoVariables extends AdminReplyRequest {
|
||||
conversationId: string;
|
||||
}
|
||||
|
||||
// CEO chime-in. The reply is a direct CEO->to_agent message (pairwise model);
|
||||
// invalidate the list (the CEO<->agent conversation appears/updates there) and
|
||||
// the watched transcript's messages.
|
||||
export function useReplyAsCeo() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ conversationId, ...reply }: ReplyAsCeoVariables) =>
|
||||
a2aApi.replyAsCeo(conversationId, reply),
|
||||
onSuccess: (_sent, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: a2aLiveKeys.conversations });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: a2aLiveKeys.messages(variables.conversationId),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -44,6 +44,18 @@ export interface NotificationMessage {
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface A2ASystemMessage {
|
||||
type: "connected" | "a2a.message";
|
||||
conversation_id?: string;
|
||||
message_id?: string;
|
||||
task_id?: string;
|
||||
from_agent?: string;
|
||||
to_agent?: string;
|
||||
skill?: string | null;
|
||||
body_excerpt?: string;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface SessionMessage {
|
||||
type: "connected" | "message.new";
|
||||
message_id?: string;
|
||||
@@ -296,6 +308,38 @@ export function useNotificationStream() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to live A2A traffic on the operator stream (`/ws/system`).
|
||||
*
|
||||
* The bridge publishes an `a2a.message` frame for every persisted
|
||||
* agent<->agent chat message. Frames carry a capped `body_excerpt` only —
|
||||
* consumers invalidate their REST queries for full bodies (the A2A live view
|
||||
* idiom), never render the excerpt as the message.
|
||||
*/
|
||||
export function useA2ALiveStream() {
|
||||
const {
|
||||
state,
|
||||
lastMessage,
|
||||
messages,
|
||||
clearMessages,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
} = useWebSocket<A2ASystemMessage>("/system", undefined, true);
|
||||
|
||||
// Filter to A2A frames (the system stream also carries rate-limit/usage).
|
||||
const a2aMessages = messages.filter((m) => m.type === "a2a.message");
|
||||
|
||||
return {
|
||||
state,
|
||||
lastMessage,
|
||||
a2aMessages,
|
||||
allMessages: messages,
|
||||
clearMessages,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Connection Status Hook (for UI indicators)
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user