@geminilight/mindos 0.6.25 → 0.6.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/README_zh.md +19 -3
- package/app/app/api/a2a/discover/route.ts +23 -0
- package/app/components/CreateSpaceModal.tsx +1 -0
- package/app/components/ImportModal.tsx +3 -0
- package/app/components/OnboardingView.tsx +1 -0
- package/app/components/RightAskPanel.tsx +4 -2
- package/app/components/SidebarLayout.tsx +11 -2
- package/app/components/agents/DiscoverAgentModal.tsx +149 -0
- package/app/components/ask/AskContent.tsx +21 -9
- package/app/components/ask/SessionTabBar.tsx +70 -0
- package/app/components/echo/EchoInsightCollapsible.tsx +4 -0
- package/app/components/panels/AgentsPanel.tsx +25 -2
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +5 -0
- package/app/components/settings/AiTab.tsx +1 -0
- package/app/components/settings/KnowledgeTab.tsx +2 -0
- package/app/components/settings/SyncTab.tsx +2 -0
- package/app/components/setup/StepDots.tsx +5 -1
- package/app/data/skills/mindos/SKILL.md +186 -0
- package/app/data/skills/mindos-zh/SKILL.md +185 -0
- package/app/hooks/useA2aRegistry.ts +53 -0
- package/app/hooks/useAskSession.ts +44 -25
- package/app/lib/a2a/a2a-tools.ts +212 -0
- package/app/lib/a2a/client.ts +207 -0
- package/app/lib/a2a/index.ts +8 -0
- package/app/lib/a2a/orchestrator.ts +255 -0
- package/app/lib/a2a/types.ts +54 -0
- package/app/lib/agent/tools.ts +6 -4
- package/app/lib/i18n-en.ts +52 -0
- package/app/lib/i18n-zh.ts +52 -0
- package/app/next-env.d.ts +1 -1
- package/bin/cli.js +180 -171
- package/bin/commands/agent.js +110 -18
- package/bin/commands/api.js +5 -3
- package/bin/commands/ask.js +3 -3
- package/bin/commands/file.js +13 -13
- package/bin/commands/search.js +2 -2
- package/bin/commands/space.js +64 -10
- package/bin/lib/command.js +10 -0
- package/package.json +1 -1
|
@@ -78,13 +78,19 @@ export function useAskSession(currentFile?: string) {
|
|
|
78
78
|
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
|
79
79
|
const persistTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
80
80
|
|
|
81
|
-
/** Load sessions from server, pick the matching one or create fresh. */
|
|
81
|
+
/** Load sessions from server, pick the matching one or create fresh. Prunes stale empty sessions. */
|
|
82
82
|
const initSessions = useCallback(async () => {
|
|
83
|
-
const
|
|
83
|
+
const all = (await fetchSessions())
|
|
84
84
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
85
85
|
.slice(0, MAX_SESSIONS);
|
|
86
|
-
setSessions(sorted);
|
|
87
86
|
|
|
87
|
+
// Prune any empty sessions that leaked to server (from older versions)
|
|
88
|
+
const emptyIds = all.filter((s) => s.messages.length === 0).map((s) => s.id);
|
|
89
|
+
const sorted = emptyIds.length > 0 ? all.filter((s) => !emptyIds.includes(s.id)) : all;
|
|
90
|
+
if (emptyIds.length > 0) void removeSessions(emptyIds);
|
|
91
|
+
|
|
92
|
+
// Always prepend a fresh empty session in memory (never persisted until first message)
|
|
93
|
+
const fresh = createSession(currentFile);
|
|
88
94
|
const matched = currentFile
|
|
89
95
|
? sorted.find((sess) => sess.currentFile === currentFile)
|
|
90
96
|
: sorted[0];
|
|
@@ -92,20 +98,19 @@ export function useAskSession(currentFile?: string) {
|
|
|
92
98
|
if (matched) {
|
|
93
99
|
setActiveSessionId(matched.id);
|
|
94
100
|
setMessages(matched.messages);
|
|
101
|
+
setSessions([...sorted]);
|
|
95
102
|
} else {
|
|
96
|
-
const fresh = createSession(currentFile);
|
|
97
103
|
setActiveSessionId(fresh.id);
|
|
98
104
|
setMessages([]);
|
|
99
|
-
|
|
100
|
-
setSessions(
|
|
101
|
-
await upsertSession(fresh);
|
|
105
|
+
// Empty session lives only in memory — no upsertSession call
|
|
106
|
+
setSessions([fresh, ...sorted].slice(0, MAX_SESSIONS));
|
|
102
107
|
}
|
|
103
108
|
}, [currentFile]);
|
|
104
109
|
|
|
105
|
-
/** Persist current session (debounced). */
|
|
110
|
+
/** Persist current session (debounced). Only persists if session has messages. */
|
|
106
111
|
const persistSession = useCallback(
|
|
107
112
|
(msgs: Message[], sessionId: string | null) => {
|
|
108
|
-
if (!sessionId) return;
|
|
113
|
+
if (!sessionId || msgs.length === 0) return;
|
|
109
114
|
let sessionToPersist: ChatSession | null = null;
|
|
110
115
|
setSessions((prev) => {
|
|
111
116
|
const now = Date.now();
|
|
@@ -135,35 +140,48 @@ export function useAskSession(currentFile?: string) {
|
|
|
135
140
|
}
|
|
136
141
|
}, []);
|
|
137
142
|
|
|
138
|
-
/** Create a brand-new session. */
|
|
143
|
+
/** Create a brand-new session (memory only). If current session is already empty, reuse it. */
|
|
139
144
|
const resetSession = useCallback(() => {
|
|
140
|
-
const fresh = createSession(currentFile);
|
|
141
|
-
setActiveSessionId(fresh.id);
|
|
142
|
-
setMessages([]);
|
|
143
145
|
setSessions((prev) => {
|
|
144
|
-
const
|
|
146
|
+
const active = prev.find((s) => s.id === activeSessionId);
|
|
147
|
+
// Already on an empty session — just clear input, don't create another
|
|
148
|
+
if (active && active.messages.length === 0) return prev;
|
|
149
|
+
|
|
150
|
+
const fresh = createSession(currentFile);
|
|
151
|
+
setActiveSessionId(fresh.id);
|
|
152
|
+
setMessages([]);
|
|
153
|
+
// Memory only — no upsertSession call. Will be persisted on first message.
|
|
154
|
+
return [fresh, ...prev]
|
|
145
155
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
146
156
|
.slice(0, MAX_SESSIONS);
|
|
147
|
-
void upsertSession(fresh);
|
|
148
|
-
return next;
|
|
149
157
|
});
|
|
150
|
-
}, [currentFile]);
|
|
158
|
+
}, [currentFile, activeSessionId]);
|
|
151
159
|
|
|
152
|
-
/** Switch to an existing session. */
|
|
160
|
+
/** Switch to an existing session. Auto-drops abandoned empty sessions from memory. */
|
|
153
161
|
const loadSession = useCallback(
|
|
154
162
|
(id: string) => {
|
|
155
163
|
const target = sessions.find((s) => s.id === id);
|
|
156
164
|
if (!target) return;
|
|
165
|
+
|
|
166
|
+
// Drop the session we're leaving if it's empty (it was never persisted, just remove from memory)
|
|
167
|
+
const leaving = activeSessionId ? sessions.find((s) => s.id === activeSessionId) : null;
|
|
168
|
+
if (leaving && leaving.messages.length === 0 && leaving.id !== id) {
|
|
169
|
+
setSessions((prev) => prev.filter((s) => s.id !== leaving.id));
|
|
170
|
+
}
|
|
171
|
+
|
|
157
172
|
setActiveSessionId(target.id);
|
|
158
173
|
setMessages(target.messages);
|
|
159
174
|
},
|
|
160
|
-
[sessions],
|
|
175
|
+
[sessions, activeSessionId],
|
|
161
176
|
);
|
|
162
177
|
|
|
163
|
-
/** Delete a session. If it's the active one, create fresh. */
|
|
178
|
+
/** Delete a session. If it's the active one, create fresh (memory only). */
|
|
164
179
|
const deleteSession = useCallback(
|
|
165
180
|
(id: string) => {
|
|
166
|
-
|
|
181
|
+
const target = sessions.find((s) => s.id === id);
|
|
182
|
+
// Only call removeSession if the session has messages (i.e. was persisted)
|
|
183
|
+
if (target && target.messages.length > 0) void removeSession(id);
|
|
184
|
+
|
|
167
185
|
const remaining = sessions.filter((s) => s.id !== id);
|
|
168
186
|
setSessions(remaining);
|
|
169
187
|
|
|
@@ -172,21 +190,22 @@ export function useAskSession(currentFile?: string) {
|
|
|
172
190
|
setActiveSessionId(fresh.id);
|
|
173
191
|
setMessages([]);
|
|
174
192
|
setSessions([fresh, ...remaining].slice(0, MAX_SESSIONS));
|
|
175
|
-
|
|
193
|
+
// No upsertSession — memory only
|
|
176
194
|
}
|
|
177
195
|
},
|
|
178
196
|
[activeSessionId, currentFile, sessions],
|
|
179
197
|
);
|
|
180
198
|
|
|
181
199
|
const clearAllSessions = useCallback(() => {
|
|
182
|
-
|
|
183
|
-
|
|
200
|
+
// Only delete sessions that have messages (were persisted)
|
|
201
|
+
const persistedIds = sessions.filter(s => s.messages.length > 0).map(s => s.id);
|
|
202
|
+
if (persistedIds.length > 0) void removeSessions(persistedIds);
|
|
184
203
|
|
|
185
204
|
const fresh = createSession(currentFile);
|
|
186
205
|
setActiveSessionId(fresh.id);
|
|
187
206
|
setMessages([]);
|
|
188
207
|
setSessions([fresh]);
|
|
189
|
-
|
|
208
|
+
// No upsertSession — memory only
|
|
190
209
|
}, [currentFile, sessions]);
|
|
191
210
|
|
|
192
211
|
return {
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Agent Tools — Expose A2A client capabilities as tools
|
|
3
|
+
* for the MindOS built-in agent to discover and delegate to external agents.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Type, type Static } from '@sinclair/typebox';
|
|
7
|
+
import type { AgentTool } from '@mariozechner/pi-agent-core';
|
|
8
|
+
import {
|
|
9
|
+
discoverAgent,
|
|
10
|
+
discoverAgents,
|
|
11
|
+
delegateTask,
|
|
12
|
+
checkRemoteTaskStatus,
|
|
13
|
+
getDiscoveredAgents,
|
|
14
|
+
} from './client';
|
|
15
|
+
import { createPlan, executePlan } from './orchestrator';
|
|
16
|
+
|
|
17
|
+
function textResult(text: string) {
|
|
18
|
+
return { content: [{ type: 'text' as const, text }], details: {} };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* ── Parameter Schemas ─────────────────────────────────────────────────── */
|
|
22
|
+
|
|
23
|
+
const DiscoverAgentParams = Type.Object({
|
|
24
|
+
url: Type.String({ description: 'Base URL of the agent to discover (e.g. http://localhost:3456)' }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const DiscoverMultipleParams = Type.Object({
|
|
28
|
+
urls: Type.Array(Type.String(), { description: 'List of base URLs to discover agents from' }),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const DelegateParams = Type.Object({
|
|
32
|
+
agent_id: Type.String({ description: 'ID of the target agent (from list_remote_agents)' }),
|
|
33
|
+
message: Type.String({ description: 'Natural language message to send to the agent' }),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const CheckStatusParams = Type.Object({
|
|
37
|
+
agent_id: Type.String({ description: 'ID of the agent that owns the task' }),
|
|
38
|
+
task_id: Type.String({ description: 'Task ID returned by delegate_to_agent' }),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const OrchestrateParams = Type.Object({
|
|
42
|
+
request: Type.String({ description: 'The complex request to decompose and execute across multiple agents' }),
|
|
43
|
+
subtasks: Type.Optional(Type.Array(Type.String(), { description: 'Pre-decomposed subtask descriptions. If omitted, auto-decomposition is used.' })),
|
|
44
|
+
strategy: Type.Optional(Type.Union([
|
|
45
|
+
Type.Literal('parallel'),
|
|
46
|
+
Type.Literal('sequential'),
|
|
47
|
+
], { description: 'Execution strategy: parallel (default) or sequential', default: 'parallel' })),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/* ── Tool Implementations ──────────────────────────────────────────────── */
|
|
51
|
+
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
+
export const a2aTools: AgentTool<any>[] = [
|
|
54
|
+
{
|
|
55
|
+
name: 'list_remote_agents',
|
|
56
|
+
label: 'List Remote Agents',
|
|
57
|
+
description: 'List all discovered remote A2A agents and their capabilities. Shows agent ID, name, skills, and reachability status. Call discover_agent first to add agents.',
|
|
58
|
+
parameters: Type.Object({}),
|
|
59
|
+
execute: async (_id: string) => {
|
|
60
|
+
const agents = getDiscoveredAgents();
|
|
61
|
+
if (agents.length === 0) {
|
|
62
|
+
return textResult('No remote agents discovered yet. Use discover_agent with a URL to find agents.');
|
|
63
|
+
}
|
|
64
|
+
const lines = agents.map(a => {
|
|
65
|
+
const skills = a.card.skills.map(s => s.name).join(', ');
|
|
66
|
+
const status = a.reachable ? 'reachable' : 'unreachable';
|
|
67
|
+
return `- **${a.card.name}** (id: ${a.id}, ${status})\n Skills: ${skills || 'none declared'}`;
|
|
68
|
+
});
|
|
69
|
+
return textResult(`Discovered agents:\n\n${lines.join('\n\n')}`);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
name: 'discover_agent',
|
|
75
|
+
label: 'Discover Remote Agent',
|
|
76
|
+
description: 'Discover a remote A2A agent by fetching its agent card from the given URL. The agent card describes the agent\'s capabilities and skills.',
|
|
77
|
+
parameters: DiscoverAgentParams,
|
|
78
|
+
execute: async (_id: string, params: Static<typeof DiscoverAgentParams>) => {
|
|
79
|
+
try {
|
|
80
|
+
const agent = await discoverAgent(params.url);
|
|
81
|
+
if (!agent) {
|
|
82
|
+
return textResult(`No A2A agent found at ${params.url}. The server may not support the A2A protocol.`);
|
|
83
|
+
}
|
|
84
|
+
const skills = agent.card.skills.map(s => ` - ${s.name}: ${s.description}`).join('\n');
|
|
85
|
+
return textResult(
|
|
86
|
+
`Discovered agent: **${agent.card.name}** (v${agent.card.version})\n` +
|
|
87
|
+
`ID: ${agent.id}\n` +
|
|
88
|
+
`Description: ${agent.card.description}\n` +
|
|
89
|
+
`Endpoint: ${agent.endpoint}\n` +
|
|
90
|
+
`Skills:\n${skills || ' (none declared)'}`
|
|
91
|
+
);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return textResult(`Failed to discover agent at ${params.url}: ${(err as Error).message}`);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
{
|
|
99
|
+
name: 'discover_agents',
|
|
100
|
+
label: 'Discover Multiple Agents',
|
|
101
|
+
description: 'Discover multiple remote A2A agents by fetching their agent cards concurrently. Returns all successfully discovered agents.',
|
|
102
|
+
parameters: DiscoverMultipleParams,
|
|
103
|
+
execute: async (_id: string, params: Static<typeof DiscoverMultipleParams>) => {
|
|
104
|
+
try {
|
|
105
|
+
const agents = await discoverAgents(params.urls);
|
|
106
|
+
if (agents.length === 0) {
|
|
107
|
+
return textResult(`No A2A agents found at any of the ${params.urls.length} URLs.`);
|
|
108
|
+
}
|
|
109
|
+
const lines = agents.map(a =>
|
|
110
|
+
`- **${a.card.name}** (id: ${a.id}) — ${a.card.skills.length} skills`
|
|
111
|
+
);
|
|
112
|
+
return textResult(`Discovered ${agents.length}/${params.urls.length} agents:\n\n${lines.join('\n')}`);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
return textResult(`Discovery failed: ${(err as Error).message}`);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
{
|
|
120
|
+
name: 'delegate_to_agent',
|
|
121
|
+
label: 'Delegate Task to Agent',
|
|
122
|
+
description: 'Send a task to a remote A2A agent. The agent will process the message and return a result. Use list_remote_agents to see available agents and their skills first.',
|
|
123
|
+
parameters: DelegateParams,
|
|
124
|
+
execute: async (_id: string, params: Static<typeof DelegateParams>) => {
|
|
125
|
+
try {
|
|
126
|
+
const task = await delegateTask(params.agent_id, params.message);
|
|
127
|
+
|
|
128
|
+
if (task.status.state === 'TASK_STATE_COMPLETED') {
|
|
129
|
+
const result = task.artifacts?.[0]?.parts?.[0]?.text
|
|
130
|
+
?? task.history?.find(m => m.role === 'ROLE_AGENT')?.parts?.[0]?.text
|
|
131
|
+
?? 'Task completed (no text result)';
|
|
132
|
+
return textResult(`Agent completed task (id: ${task.id}):\n\n${result}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (task.status.state === 'TASK_STATE_FAILED') {
|
|
136
|
+
const errMsg = task.status.message?.parts?.[0]?.text ?? 'Unknown error';
|
|
137
|
+
return textResult(`Agent failed task (id: ${task.id}): ${errMsg}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Task is still in progress (non-blocking)
|
|
141
|
+
return textResult(
|
|
142
|
+
`Task submitted (id: ${task.id}, state: ${task.status.state}).\n` +
|
|
143
|
+
`Use check_task_status to poll for completion.`
|
|
144
|
+
);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return textResult(`Delegation failed: ${(err as Error).message}`);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
name: 'check_task_status',
|
|
153
|
+
label: 'Check Remote Task Status',
|
|
154
|
+
description: 'Check the status of a task previously delegated to a remote agent. Returns the current state, any results, or error information.',
|
|
155
|
+
parameters: CheckStatusParams,
|
|
156
|
+
execute: async (_id: string, params: Static<typeof CheckStatusParams>) => {
|
|
157
|
+
try {
|
|
158
|
+
const task = await checkRemoteTaskStatus(params.agent_id, params.task_id);
|
|
159
|
+
|
|
160
|
+
const state = task.status.state;
|
|
161
|
+
if (state === 'TASK_STATE_COMPLETED') {
|
|
162
|
+
const result = task.artifacts?.[0]?.parts?.[0]?.text ?? 'Completed (no text)';
|
|
163
|
+
return textResult(`Task ${params.task_id} completed:\n\n${result}`);
|
|
164
|
+
}
|
|
165
|
+
if (state === 'TASK_STATE_FAILED') {
|
|
166
|
+
const errMsg = task.status.message?.parts?.[0]?.text ?? 'Unknown error';
|
|
167
|
+
return textResult(`Task ${params.task_id} failed: ${errMsg}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return textResult(`Task ${params.task_id} state: ${state}`);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
return textResult(`Status check failed: ${(err as Error).message}`);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
{
|
|
178
|
+
name: 'orchestrate',
|
|
179
|
+
label: 'Orchestrate Multi-Agent Task',
|
|
180
|
+
description: 'Decompose a complex request into subtasks and execute them across multiple remote agents. Auto-matches subtasks to the best available agent based on skills. Use discover_agent first to register agents.',
|
|
181
|
+
parameters: OrchestrateParams,
|
|
182
|
+
execute: async (_id: string, params: Static<typeof OrchestrateParams>) => {
|
|
183
|
+
try {
|
|
184
|
+
const strategy = params.strategy ?? 'parallel';
|
|
185
|
+
const plan = createPlan(params.request, strategy, params.subtasks);
|
|
186
|
+
|
|
187
|
+
const assigned = plan.subtasks.filter(st => st.assignedAgentId);
|
|
188
|
+
if (assigned.length === 0) {
|
|
189
|
+
return textResult(
|
|
190
|
+
`Created plan with ${plan.subtasks.length} subtasks but no agents matched any skill.\n` +
|
|
191
|
+
`Subtasks: ${plan.subtasks.map(st => st.description).join(', ')}\n\n` +
|
|
192
|
+
'Discover agents first using discover_agent, then retry.'
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result = await executePlan(plan);
|
|
197
|
+
|
|
198
|
+
const summary = plan.subtasks.map(st => {
|
|
199
|
+
const icon = st.status === 'completed' ? '[OK]' : st.status === 'failed' ? '[FAIL]' : '[?]';
|
|
200
|
+
return `${icon} ${st.description}`;
|
|
201
|
+
}).join('\n');
|
|
202
|
+
|
|
203
|
+
return textResult(
|
|
204
|
+
`Orchestration ${result.status} (${strategy}, ${plan.subtasks.length} subtasks):\n\n` +
|
|
205
|
+
`${summary}\n\n---\n\n${result.aggregatedResult ?? '(no result)'}`
|
|
206
|
+
);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
return textResult(`Orchestration failed: ${(err as Error).message}`);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
];
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Client — Discover external agents and delegate tasks via A2A protocol.
|
|
3
|
+
* Phase 2: MindOS as an A2A Client (orchestrator).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
AgentCard,
|
|
8
|
+
RemoteAgent,
|
|
9
|
+
A2ATask,
|
|
10
|
+
JsonRpcRequest,
|
|
11
|
+
JsonRpcResponse,
|
|
12
|
+
SendMessageParams,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
/* ── Constants ─────────────────────────────────────────────────────────── */
|
|
16
|
+
|
|
17
|
+
const DISCOVERY_TIMEOUT_MS = 5_000;
|
|
18
|
+
const RPC_TIMEOUT_MS = 30_000;
|
|
19
|
+
const CARD_CACHE_TTL_MS = 5 * 60 * 1000; // 5 min
|
|
20
|
+
|
|
21
|
+
/* ── Agent Registry (in-memory cache) ──────────────────────────────────── */
|
|
22
|
+
|
|
23
|
+
const registry = new Map<string, RemoteAgent>();
|
|
24
|
+
|
|
25
|
+
/** Derive a stable ID from a URL (includes protocol to avoid collisions) */
|
|
26
|
+
function urlToId(url: string): string {
|
|
27
|
+
try {
|
|
28
|
+
const u = new URL(url);
|
|
29
|
+
const proto = u.protocol.replace(':', '');
|
|
30
|
+
const port = u.port || (u.protocol === 'https:' ? '443' : '80');
|
|
31
|
+
return `${proto}-${u.hostname}-${port}`;
|
|
32
|
+
} catch {
|
|
33
|
+
return url.replace(/[^a-zA-Z0-9]/g, '-');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ── HTTP helpers ──────────────────────────────────────────────────────── */
|
|
38
|
+
|
|
39
|
+
async function fetchWithTimeout(url: string, opts: RequestInit & { timeoutMs?: number } = {}): Promise<Response> {
|
|
40
|
+
const { timeoutMs = DISCOVERY_TIMEOUT_MS, ...fetchOpts } = opts;
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
43
|
+
try {
|
|
44
|
+
return await fetch(url, { ...fetchOpts, signal: controller.signal });
|
|
45
|
+
} finally {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function jsonRpcCall(endpoint: string, method: string, params: unknown, token?: string): Promise<JsonRpcResponse> {
|
|
51
|
+
const body: JsonRpcRequest = {
|
|
52
|
+
jsonrpc: '2.0',
|
|
53
|
+
id: `mindos-${Date.now()}`,
|
|
54
|
+
method,
|
|
55
|
+
params: params as Record<string, unknown>,
|
|
56
|
+
};
|
|
57
|
+
const headers: Record<string, string> = {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
'A2A-Version': '1.0',
|
|
60
|
+
};
|
|
61
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
62
|
+
|
|
63
|
+
const res = await fetchWithTimeout(endpoint, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers,
|
|
66
|
+
body: JSON.stringify(body),
|
|
67
|
+
timeoutMs: RPC_TIMEOUT_MS,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
throw new Error(`A2A RPC failed: ${res.status} ${res.statusText}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return res.json();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ── Discovery ─────────────────────────────────────────────────────────── */
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Discover an A2A agent at the given base URL.
|
|
81
|
+
* Fetches /.well-known/agent-card.json and caches the result.
|
|
82
|
+
*/
|
|
83
|
+
export async function discoverAgent(baseUrl: string): Promise<RemoteAgent | null> {
|
|
84
|
+
const cleanUrl = baseUrl.replace(/\/+$/, '');
|
|
85
|
+
const cardUrl = `${cleanUrl}/.well-known/agent-card.json`;
|
|
86
|
+
const id = urlToId(cleanUrl);
|
|
87
|
+
|
|
88
|
+
// Check cache
|
|
89
|
+
const cached = registry.get(id);
|
|
90
|
+
if (cached && Date.now() - new Date(cached.discoveredAt).getTime() < CARD_CACHE_TTL_MS) {
|
|
91
|
+
return cached;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetchWithTimeout(cardUrl);
|
|
96
|
+
if (!res.ok) return null;
|
|
97
|
+
|
|
98
|
+
const card: AgentCard = await res.json();
|
|
99
|
+
// Validate minimum required fields
|
|
100
|
+
if (!card || typeof card.name !== 'string' || !card.name ||
|
|
101
|
+
!Array.isArray(card.supportedInterfaces) || card.supportedInterfaces.length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Find JSON-RPC endpoint
|
|
106
|
+
const jsonRpcInterface = card.supportedInterfaces.find(i => i.protocolBinding === 'JSONRPC');
|
|
107
|
+
if (!jsonRpcInterface) return null;
|
|
108
|
+
|
|
109
|
+
const agent: RemoteAgent = {
|
|
110
|
+
id,
|
|
111
|
+
card,
|
|
112
|
+
endpoint: jsonRpcInterface.url,
|
|
113
|
+
discoveredAt: new Date().toISOString(),
|
|
114
|
+
reachable: true,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
registry.set(id, agent);
|
|
118
|
+
return agent;
|
|
119
|
+
} catch {
|
|
120
|
+
// Mark as unreachable if previously cached
|
|
121
|
+
if (cached) {
|
|
122
|
+
cached.reachable = false;
|
|
123
|
+
return cached;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Discover agents from a list of URLs (concurrent, best-effort).
|
|
131
|
+
*/
|
|
132
|
+
export async function discoverAgents(urls: string[]): Promise<RemoteAgent[]> {
|
|
133
|
+
const results = await Promise.allSettled(urls.map(discoverAgent));
|
|
134
|
+
return results
|
|
135
|
+
.filter((r): r is PromiseFulfilledResult<RemoteAgent | null> => r.status === 'fulfilled')
|
|
136
|
+
.map(r => r.value)
|
|
137
|
+
.filter((a): a is RemoteAgent => a !== null);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ── Task Delegation ───────────────────────────────────────────────────── */
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Send a message to a remote agent via A2A JSON-RPC.
|
|
144
|
+
* Returns the resulting task.
|
|
145
|
+
*/
|
|
146
|
+
export async function delegateTask(
|
|
147
|
+
agentId: string,
|
|
148
|
+
message: string,
|
|
149
|
+
token?: string,
|
|
150
|
+
): Promise<A2ATask> {
|
|
151
|
+
const agent = registry.get(agentId);
|
|
152
|
+
if (!agent) throw new Error(`Agent not found: ${agentId}`);
|
|
153
|
+
if (!agent.reachable) throw new Error(`Agent not reachable: ${agent.card.name}`);
|
|
154
|
+
|
|
155
|
+
const params: SendMessageParams = {
|
|
156
|
+
message: {
|
|
157
|
+
role: 'ROLE_USER',
|
|
158
|
+
parts: [{ text: message }],
|
|
159
|
+
},
|
|
160
|
+
configuration: { blocking: true },
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const response = await jsonRpcCall(agent.endpoint, 'SendMessage', params, token);
|
|
164
|
+
|
|
165
|
+
if (response.error) {
|
|
166
|
+
throw new Error(`A2A error [${response.error.code}]: ${response.error.message}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return response.result as A2ATask;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check the status of a task on a remote agent.
|
|
174
|
+
*/
|
|
175
|
+
export async function checkRemoteTaskStatus(
|
|
176
|
+
agentId: string,
|
|
177
|
+
taskId: string,
|
|
178
|
+
token?: string,
|
|
179
|
+
): Promise<A2ATask> {
|
|
180
|
+
const agent = registry.get(agentId);
|
|
181
|
+
if (!agent) throw new Error(`Agent not found: ${agentId}`);
|
|
182
|
+
|
|
183
|
+
const response = await jsonRpcCall(agent.endpoint, 'GetTask', { id: taskId }, token);
|
|
184
|
+
|
|
185
|
+
if (response.error) {
|
|
186
|
+
throw new Error(`A2A error [${response.error.code}]: ${response.error.message}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return response.result as A2ATask;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* ── Registry Access ───────────────────────────────────────────────────── */
|
|
193
|
+
|
|
194
|
+
/** Get all discovered agents */
|
|
195
|
+
export function getDiscoveredAgents(): RemoteAgent[] {
|
|
196
|
+
return [...registry.values()];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Get a specific agent by ID */
|
|
200
|
+
export function getAgent(id: string): RemoteAgent | undefined {
|
|
201
|
+
return registry.get(id);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Clear the agent registry cache */
|
|
205
|
+
export function clearRegistry(): void {
|
|
206
|
+
registry.clear();
|
|
207
|
+
}
|
package/app/lib/a2a/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { buildAgentCard } from './agent-card';
|
|
2
2
|
export { handleSendMessage, handleGetTask, handleCancelTask } from './task-handler';
|
|
3
|
+
export { discoverAgent, discoverAgents, delegateTask, checkRemoteTaskStatus, getDiscoveredAgents, getAgent, clearRegistry } from './client';
|
|
4
|
+
export { matchSkill, decompose, createPlan, executePlan } from './orchestrator';
|
|
5
|
+
export { a2aTools } from './a2a-tools';
|
|
3
6
|
export { A2A_ERRORS } from './types';
|
|
4
7
|
export type {
|
|
5
8
|
AgentCard,
|
|
@@ -20,4 +23,9 @@ export type {
|
|
|
20
23
|
SendMessageParams,
|
|
21
24
|
GetTaskParams,
|
|
22
25
|
CancelTaskParams,
|
|
26
|
+
RemoteAgent,
|
|
27
|
+
SubTask,
|
|
28
|
+
ExecutionStrategy,
|
|
29
|
+
OrchestrationPlan,
|
|
30
|
+
SkillMatch,
|
|
23
31
|
} from './types';
|