diff --git a/src/components/gitAgent/GitAgentA2A.tsx b/src/components/gitAgent/GitAgentA2A.tsx new file mode 100644 index 0000000..e5d89ec --- /dev/null +++ b/src/components/gitAgent/GitAgentA2A.tsx @@ -0,0 +1,275 @@ +import { motion } from "framer-motion"; +import { CodeBlock } from "@/components/gitAgent/CodeBlock"; + +const agentYaml = `# agent.yaml +a2a_agents: + research-agent: + url: https://research.example.com # base URL; Agent Card resolved from + # /.well-known/agent-card.json by default + headers: # optional; \${VAR} interpolated from your shell env + Authorization: "Bearer \${RESEARCH_TOKEN}" + timeoutMs: 30000 # optional; connect + per-call timeout (default 30000) + stream: true # optional; use SSE streaming when supported (default true) + cardPath: /.well-known/agent-card.json # optional; override the Agent Card path`; + +const sdkCode = `import { query } from "@open-gitagent/gitagent"; + +for await (const msg of query({ + prompt: "Get the research agent's take on this", + a2aAgents: { + "research-agent": { url: "https://research.example.com" }, + }, +})) { + if (msg.type === "tool_use") console.log(\`calling \${msg.toolName}\`); +}`; + +const configFields = [ + { field: "url", required: "yes", notes: "Base URL of the remote agent; agents without a url are skipped with a warning." }, + { field: "headers", required: "no", notes: "Sent on every request (e.g. auth); ${VAR} values are interpolated from process.env." }, + { field: "timeoutMs", required: "no", notes: "Default 30000ms — wraps the connect step (the one real Agent Card fetch) and each subsequent call." }, + { field: "stream", required: "no", notes: "Default true — only actually streams if the remote agent's card advertises capabilities.streaming: true." }, + { field: "cardPath", required: "no", notes: "Override if the agent doesn't serve its card at the well-known path." }, +]; + +const runtimeSteps = [ + { step: 1, label: "Discovery (startup)", text: "GitAgent fetches each configured agent's Agent Card via a single GET /.well-known/agent-card.json request (or cardPath if set). From the card it reads the agent's name, description, and declared skills." }, + { step: 2, label: "Tool registration", text: "Each skill becomes one tool named __, where both halves are sanitized to [a-zA-Z0-9_] (non-matching characters, including -, become _). So a research-agent key produces research_agent__web_search — worth knowing if you use kebab-case keys. An agent that declares zero skills is exposed as a single tool named after the sanitized agent key. Every A2A tool takes one parameter: message (free-text task/question for the remote agent)." }, + { step: 3, label: "Delegation (runtime)", text: "When the model calls one of these tools, GitAgent sends message to the remote agent — via SSE streaming (message/stream) if stream !== false locally and the card advertises capabilities.streaming: true, otherwise a blocking call (message/send). Streaming partial output is surfaced live as it arrives." }, + { step: 4, label: "Result handling", text: "For a completed task, GitAgent prefers the task's returned artifacts, falling back to its status message. File/binary parts are summarized (e.g. [file: report.pdf]), not inlined, to protect the token budget; data parts are rendered as pretty-printed JSON." }, +]; + +const nonFatal = [ + { label: "Missing url", desc: "That agent is skipped with a warning — others still load." }, + { label: "Connection / card-fetch failure", desc: "Bad URL, timeout, or server down → skipped with a warning; the rest of the session works normally." }, + { label: "Per-call failure", desc: "A remote agent that errors mid-task is returned to the model as text (A2A call to \"\" failed: ), not thrown — so the agent can see the failure and react." }, + { label: "Tool name collision", desc: "A tool name colliding with an existing tool is skipped with a warning rather than silently overwritten." }, +]; + +const differences = [ + { label: "No teardown", desc: "A2A holds no persistent connection between calls — each call opens its own request/stream — so there's nothing to close on session end. Don't expect a teardown log line the way you might see for a persistent connection." }, + { label: "Silent env substitution", desc: "A2A's ${VAR} interpolation silently substitutes an empty string for an unset variable and does not print a warning. If a header looks empty at runtime, check your env directly rather than watching for a log message." }, +]; + +const tryYaml = `# agent.yaml +a2a_agents: + research-agent: + url: https://your-real-a2a-agent.example.com`; + +const tryRunCmd = `gitagent -d /path/to/your/agent \\ + --prompt "List every tool you have available, including any starting with research_agent__"`; + +const troubleshooting = [ + { q: "Agent's tools not showing up?", a: "Check stderr for [a2a:] connection failed: … — skipping or [a2a:] missing \"url\" — skipping." }, + { q: "${VAR} came through empty in a header?", a: "No warning is logged for this (unlike MCP) — verify the env var is actually exported in your shell." }, + { q: "Tool name collision?", a: "Look for [a2a:] tool \"\" collides — skipping in stderr." }, + { q: "Streaming not showing partial output?", a: "Confirm the remote agent's Agent Card actually advertises capabilities.streaming: true — if not, GitAgent silently falls back to a blocking call regardless of your local stream: true setting." }, + { q: "Slow remote agent timing out?", a: "Raise timeoutMs in that agent's config block (default 30000ms) — it wraps the connect step (the one real Agent Card fetch) and each subsequent call; the post-connect card read is already-cached and effectively instant." }, +]; + +export function GitAgentA2A() { + return ( +
+
+ {/* Section heading */} + +

+ A2A Client +

+

+ GitAgent can call other AI agents that speak the{" "} + A2A (Agent2Agent) protocol{" "} + — the Linux Foundation's agent-interop standard. Delegate a task to an agent built on a completely different framework (LangGraph, CrewAI, Google ADK, …) and fold the result back into GitAgent's own reasoning. +

+

+ Distinction from MCP: A2A is outbound-only — GitAgent never runs a server. It connects out to a remote agent's public Agent Card like any HTTP client. With nothing configured, zero network calls happen and the default CLI is unchanged. +

+
+

+ Mental model: local tools give the agent capabilities; A2A gives it peers. Once connected, delegating to a remote agent looks exactly like calling any other tool. +

+
+
+ + {/* A. Configure in agent.yaml */} + +

+ Configure a remote agent in agent.yaml +

+ +
+ {configFields.map((f, i) => ( + +
+ {f.field} + + {f.required === "yes" ? "required" : "optional"} + +

{f.notes}

+
+
+ ))} +
+
+ + {/* B. Via the SDK */} + +

+ Via the SDK +

+ +

+ SDK a2aAgents are merged with agent.yaml's a2a_agents — the SDK value wins on a key collision (same merge convention as mcpServers). +

+
+ + {/* C. What happens at runtime */} + +

+ What happens at runtime +

+
+ {runtimeSteps.map((s, i) => ( + + + {s.step} + +
+ {s.label} +

{s.text}

+
+
+ ))} +
+

Non-fatal by design

+
+ {nonFatal.map((b, i) => ( + +

{b.label}

+

{b.desc}

+
+ ))} +
+

+ Opt-in: with no a2a_agents configured, no network calls happen and A2A setup returns immediately. +

+
+ + {/* D. Differences from MCP */} + +

+ Differences from MCP worth knowing +

+
+ {differences.map((d, i) => ( + +

{d.label}

+

{d.desc}

+
+ ))} +
+
+ + {/* E. Try it yourself */} + +

+ Try it yourself +

+

+ Unlike MCP (which has a trivial, official, no-auth public test server), there isn't an equivalent zero-setup public A2A agent to point at. The realistic path is to point GitAgent at a real A2A agent you or your team has deployed — for example, one exposed via LangGraph's or Google ADK's A2A support. +

+

Add it to a test agent's agent.yaml with its real URL:

+ +

Then confirm the remote skills show up namespaced correctly:

+ +

+ You should see the remote agent's skills registered as research_agent__<skill> tools — exactly like the MCP walkthrough, but calling out to a peer agent instead of a local server. +

+
+ + {/* F. Troubleshooting */} + +

+ Troubleshooting checklist +

+
+ {troubleshooting.map((t, i) => ( + +

{t.q}

+

{t.a}

+
+ ))} +
+
+
+
+ ); +} diff --git a/src/components/gitAgent/GitAgentSidebar.tsx b/src/components/gitAgent/GitAgentSidebar.tsx index 71ac967..906627f 100644 --- a/src/components/gitAgent/GitAgentSidebar.tsx +++ b/src/components/gitAgent/GitAgentSidebar.tsx @@ -48,6 +48,7 @@ export const sidebarGroups = [ slug: "capabilities", items: [ { id: "tools", label: "Tools" }, + { id: "a2a", label: "A2A Client" }, { id: "skills", label: "Skills" }, { id: "workflows", label: "Workflows" }, { id: "hooks", label: "Hooks" }, diff --git a/src/pages/GitAgentDocsPage.tsx b/src/pages/GitAgentDocsPage.tsx index 4ddb76d..d7ab3ce 100644 --- a/src/pages/GitAgentDocsPage.tsx +++ b/src/pages/GitAgentDocsPage.tsx @@ -20,6 +20,7 @@ import { GitAgentCLI } from "@/components/gitAgent/GitAgentCLI"; import { GitAgentModels } from "@/components/gitAgent/GitAgentModels"; import { GitAgentWebUI } from "@/components/gitAgent/GitAgentWebUI"; import { GitAgentTools } from "@/components/gitAgent/GitAgentTools"; +import { GitAgentA2A } from "@/components/gitAgent/GitAgentA2A"; import { GitAgentSkills } from "@/components/gitAgent/GitAgentSkills"; import { GitAgentWorkflows } from "@/components/gitAgent/GitAgentWorkflows"; import { GitAgentHooks } from "@/components/gitAgent/GitAgentHooks"; @@ -47,6 +48,7 @@ const SECTION_COMPONENTS: Record = { webui: GitAgentWebUI, messaging: GitAgentMessaging, tools: GitAgentTools, + a2a: GitAgentA2A, skills: GitAgentSkills, workflows: GitAgentWorkflows, hooks: GitAgentHooks,