Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4ac0499
docs(spec): A2A agent cards, peers, visible delegation (CLEAN-74)
maksymhryzodub-prog Sep 10, 2026
5f87cf0
docs(plan): A2A peers — research, data model, contracts, quickstart (…
maksymhryzodub-prog Sep 10, 2026
9c8c32f
docs(tasks): A2A peers task list (CLEAN-74)
maksymhryzodub-prog Sep 10, 2026
2ea2d42
docs(tasks): absolute paths on T028/T056 (CLEAN-74)
maksymhryzodub-prog Sep 10, 2026
76d2ef9
feat(peer): A2A foundations — models, protocol types, per-caller tool…
maksymhryzodub-prog Sep 14, 2026
5390935
feat(bridle): active-turn tracking and an extractable synchronous wai…
maksymhryzodub-prog Sep 14, 2026
217baa3
feat(peer): agent cards and the A2A server surface (CLEAN-74)
maksymhryzodub-prog Sep 14, 2026
34d2e25
feat(peer): connect, refresh and remove peers from the console (CLEAN…
maksymhryzodub-prog Sep 14, 2026
a30ccdc
feat(admin): Peers tab — agent card, picker with preview, delegations…
maksymhryzodub-prog Sep 14, 2026
9fe6192
feat(peer): ask_agent — delegation with a visible step (CLEAN-74)
maksymhryzodub-prog Sep 14, 2026
5c2461c
feat(admin): draw the delegation inside the thinking timeline (CLEAN-74)
maksymhryzodub-prog Sep 14, 2026
37781fc
docs(peer): README section, corrected research notes, lint pass (CLEA…
maksymhryzodub-prog Sep 14, 2026
1561f72
docs(tasks): mark delivered work and name what live validation is lef…
maksymhryzodub-prog Sep 14, 2026
666e43e
fix(peer): accept agent-<uuid> ids and add agent description field (C…
maksymhryzodub-prog Sep 15, 2026
d5d9ee9
feat(admin): Peers tab redesign — network tree + live delegation feed…
maksymhryzodub-prog Sep 15, 2026
456b436
Merge remote-tracking branch 'origin/main' into fix/CLEAN-90-peer-id-…
maksymhryzodub-prog Sep 16, 2026
388253a
chore(api-clients): regenerate admin/app SDK after merging main (CLEA…
maksymhryzodub-prog Sep 16, 2026
0a44de8
Merge remote-tracking branch 'origin/main' into feat/CLEAN-74-a2a-age…
maksymhryzodub-prog Sep 16, 2026
bc6ddd5
chore(api-clients): regenerate admin/app SDK after merging main (CLEA…
maksymhryzodub-prog Sep 16, 2026
7cbc8b3
Merge branch 'feat/CLEAN-74-a2a-agent-peers' into fix/CLEAN-90-peer-i…
maksymhryzodub-prog Sep 16, 2026
4cd8c92
feat(admin): expand peer card inline under the clicked agent (CLEAN-94)
maksymhryzodub-prog Sep 16, 2026
8a7210d
fix(admin): one-line relative times in the peer row and the feed (CLE…
maksymhryzodub-prog Sep 16, 2026
9d4c17e
refactor(admin): DateTimeAgoInline on vueuse instead of a hand-rolled…
maksymhryzodub-prog Sep 16, 2026
9753b98
chore: bump version to 0.3.52 (CLEAN-94)
maksymhryzodub-prog Sep 16, 2026
95a2a3b
docs(spec): A2A v2 — external agents and delegation that fires, draft…
maksymhryzodub-prog Sep 16, 2026
928b1ac
docs(spec): resolve Q1-Q3 — reimport-only, delegate-before-giving-up,…
maksymhryzodub-prog Sep 16, 2026
e94b225
docs(plan): A2A v2 design — R1-R7, data model, contracts, quickstart …
maksymhryzodub-prog Sep 16, 2026
fe178e4
docs(tasks): A2A v2 task list; R5 amended — tab key becomes a2a with …
maksymhryzodub-prog Sep 16, 2026
ab06173
feat(peer): external-peer foundations — schema, types, serve-state, F…
maksymhryzodub-prog Sep 16, 2026
f1ea399
fix(peer): harden the external-address SSRF guard (CLEAN-95)
maksymhryzodub-prog Sep 16, 2026
5439998
feat(peer): peers/state, connect-by-URL with preview, external delega…
maksymhryzodub-prog Sep 16, 2026
8866b78
feat(admin): A2A tab — armed indicator, Restart now, external import …
maksymhryzodub-prog Sep 16, 2026
4b6f72a
docs(tasks): mark CLEAN-95 delivered; README A2A section; regen app S…
maksymhryzodub-prog Sep 16, 2026
167bb8a
feat(admin): explain empty cards where the operator can act (CLEAN-95)
maksymhryzodub-prog Sep 16, 2026
4dde390
Merge remote-tracking branch 'origin/main' into feat/CLEAN-95-a2a-pro…
maksymhryzodub-prog Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"feature_directory": "specs/013-a2a-agent-peers"
"feature_directory": "specs/014-a2a-protocol-upgrade"
}
31 changes: 20 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,22 +94,31 @@ that is templates.
Edit any of those and the card follows. It is served at the A2A standard
path, `/a2a/agents/<id>/.well-known/agent-card.json`, and needs a
credential — there is no anonymous discovery.
- **Connecting a peer** happens in the admin console, on an agent's **Peers**
tab. Pick another agent, read its card, confirm. Connections are one-way:
connecting B to A says nothing about what B may ask of A.
- **Connecting a peer** happens in the admin console, on an agent's **A2A**
tab (formerly Peers — old `?tab=peers` links still land). Pick another
agent of this ranch, or **import an external A2A agent by its address** —
paste the URL, read its card, optionally give the access credential it
expects, confirm. Re-importing the same address updates the entry in place.
Connections are one-way: connecting B to A says nothing about what B may
ask of A.
- **Delegating** is a tool the API serves to the runtime, so no agent image
changes. A pod reads its tool list once at boot, so **restart the agent
after connecting or removing a peer** — the console says so too.
changes. A pod reads its tool list once at boot, so a new peer needs a
restart — the tab shows **armed / pending restart** and the banner carries
a one-click **Restart now**. The tool also carries the delegation policy:
an agent that cannot answer must try a plausible peer before saying
"I don't know".
- **Watching it happen**: a delegation appears in the chat's thinking
timeline while it runs, naming the peer, what its card promised, why it was
chosen and how long the wait is. Past delegations are listed on the Peers
tab.
- **Limits**: peers are agents of the same installation; a chain may be three
hops long; an agent already in a chain refuses to re-enter it.
chosen and how long the wait is. Past delegations are listed on the A2A
tab; external peers are marked in both places.
- **Limits**: a chain may be three hops long; an agent already in a chain
refuses to re-enter it. External addresses must be publicly reachable —
private hosts are refused (`A2A_ALLOW_PRIVATE_PEERS=true` lifts that for
local development).

Set `PUBLIC_API_URL` to the origin other agents reach this API on — every
card URL is built from it. See `specs/013-a2a-agent-peers/` for the full
specification.
card URL is built from it. See `specs/013-a2a-agent-peers/` and
`specs/014-a2a-protocol-upgrade/` for the full specifications.

## Project Structure

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const restartUnderway = computed(

<AgentKnowledgeTab v-else-if="tab === 'knowledge'" :agent="agent" />

<PeerTab v-else-if="tab === 'peers'" :agent="agent" />
<PeerTab v-else-if="tab === 'a2a'" :agent="agent" />

<Card v-else-if="tab === 'files'">
<CardHeader>
Expand Down
23 changes: 14 additions & 9 deletions admin/slices/agent/agent/components/agent/workspace/sections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
*
* The `value` strings are a URL contract: `?tab=<value>` deep links predate
* this screen, so the original nine are byte-identical — including `chat`,
* which is the default tab again. `logs` is the one new value.
* which is the default tab again. `logs` is the one new value, and `peers`
* became `a2a` (CLEAN-95) with a legacy alias in `toAgentTab`, so the old
* links keep landing.
*/

/** Tabs that can say how much they hold before you open them. */
export type SectionCountKey =
| 'knowledge'
| 'peers'
| 'a2a'
| 'files'
| 'secrets'
| 'channels';
Expand Down Expand Up @@ -42,10 +44,10 @@ export const AGENT_TABS = [
primary: true,
},
{
value: 'peers',
title: 'Peers',
desc: 'Other agents this one can delegate to.',
countKey: 'peers',
value: 'a2a',
title: 'A2A',
desc: 'Agent-to-agent: card, peers & delegations.',
countKey: 'a2a',
primary: true,
},
{
Expand Down Expand Up @@ -124,9 +126,12 @@ const TAB_VALUES: readonly string[] = AGENT_TABS.map((t) => t.value);
* the default rather than erroring — a stale link should land somewhere
* sensible, not on a broken screen.
*/
// Renamed values old deep links may still carry (CLEAN-95).
const LEGACY_TAB_ALIASES: Record<string, AgentTab> = { peers: 'a2a' };

export function toAgentTab(value: unknown): AgentTab {
const v = Array.isArray(value) ? value[0] : value;
return typeof v === 'string' && TAB_VALUES.includes(v)
? (v as AgentTab)
: DEFAULT_TAB;
if (typeof v !== 'string') return DEFAULT_TAB;
if (v in LEGACY_TAB_ALIASES) return LEGACY_TAB_ALIASES[v];
return TAB_VALUES.includes(v) ? (v as AgentTab) : DEFAULT_TAB;
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export function useAgentSectionCounts(
files: files.value,
secrets: secrets.value,
channels: channels.value,
peers: peers.value,
a2a: peers.value,
};
});

Expand Down
10 changes: 5 additions & 5 deletions admin/slices/agent/peer/components/peer/CardView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ function skillTag(tags: string[]): string | null {
</Badge>
</li>
</ul>
<!-- A skill-less agent is a valid peer; saying so beats an empty box,
because "nothing advertised" is exactly what a caller needs to know
before wondering why it is never chosen. -->
<!-- A skill-less agent is a valid peer; saying so beats an empty box.
Just the fact here — the surfaces that embed this view carry the
what-to-do-about-it guidance (CLEAN-95). -->
<p v-else class="text-sm text-muted-foreground">
Nothing advertised. Another agent has no way to tell when to ask this
one — give it template skills or a knowledge base.
Nothing advertised — this text is all a delegating agent gets to
match a question against.
</p>
</div>

Expand Down
19 changes: 15 additions & 4 deletions admin/slices/agent/peer/components/peer/Delegations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ const rows = computed(() => store.delegations(props.agentId));
const shown = computed(() =>
rows.value.filter(
(d) =>
(!props.peerFilter || d.peerAgentId === props.peerFilter.id) &&
// The connection id works for both origins — external peers have no
// agent id in this installation (CLEAN-95).
(!props.peerFilter || d.peerId === props.peerFilter.id) &&
(outcomeFilter.value === 'all' ||
(outcomeFilter.value === 'answered'
? d.status === 'answered'
Expand Down Expand Up @@ -192,8 +194,17 @@ function toggle(id: string) {
@click="toggle(row.id)"
>
<div class="flex items-center gap-2.5">
<Badge variant="secondary" class="flex-none">
{{ row.peerName }}
<Badge
variant="secondary"
class="flex-none"
:title="row.peerAgentId === null ? 'External agent' : undefined"
>
{{ row.peerName
}}<span
v-if="row.peerAgentId === null"
class="ml-1 text-sky-700 dark:text-sky-400"
>· ext</span
>
</Badge>
<span
class="min-w-0 flex-1 text-sm"
Expand All @@ -212,7 +223,7 @@ function toggle(id: string) {
<div
class="mt-1.5 flex items-center gap-2.5 text-xs text-muted-foreground"
>
<DateTimeAgo :date="row.startedAt" class="!items-start" />
<DateTimeAgoInline :date="row.startedAt" />
<span>·</span>
<span class="tabular-nums">{{ duration(row.durationMs) }}</span>
<span
Expand Down
Loading
Loading