@geminilight/mindos 0.6.25 → 0.7.0
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/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
|
@@ -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';
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Orchestrator — Multi-agent task decomposition and execution.
|
|
3
|
+
* Phase 3: Breaks complex requests into sub-tasks, matches to agents, executes, aggregates.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import type {
|
|
8
|
+
RemoteAgent,
|
|
9
|
+
SubTask,
|
|
10
|
+
OrchestrationPlan,
|
|
11
|
+
ExecutionStrategy,
|
|
12
|
+
SkillMatch,
|
|
13
|
+
AgentSkill,
|
|
14
|
+
} from './types';
|
|
15
|
+
import { getDiscoveredAgents, delegateTask } from './client';
|
|
16
|
+
|
|
17
|
+
/* ── Constants ─────────────────────────────────────────────────────────── */
|
|
18
|
+
|
|
19
|
+
const MAX_SUBTASKS = 10;
|
|
20
|
+
|
|
21
|
+
/* ── Skill Matcher ─────────────────────────────────────────────────────── */
|
|
22
|
+
|
|
23
|
+
/** Score how well a skill matches a task description (keyword overlap, deduplicated) */
|
|
24
|
+
function scoreSkillMatch(taskDesc: string, skill: AgentSkill): number {
|
|
25
|
+
const taskWords = new Set(taskDesc.toLowerCase().split(/\s+/));
|
|
26
|
+
// Deduplicate skill words to avoid double-counting from name + description overlap
|
|
27
|
+
const skillWords = new Set([
|
|
28
|
+
...skill.name.toLowerCase().split(/\s+/),
|
|
29
|
+
...skill.description.toLowerCase().split(/\s+/),
|
|
30
|
+
...(skill.tags ?? []).map(t => t.toLowerCase()),
|
|
31
|
+
]);
|
|
32
|
+
let matches = 0;
|
|
33
|
+
for (const w of skillWords) {
|
|
34
|
+
if (w.length > 2 && taskWords.has(w)) matches++;
|
|
35
|
+
}
|
|
36
|
+
return matches;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Find the best agent+skill match for a sub-task description.
|
|
41
|
+
* Returns null if no agent has a relevant skill.
|
|
42
|
+
*/
|
|
43
|
+
export function matchSkill(taskDescription: string): SkillMatch | null {
|
|
44
|
+
const agents = getDiscoveredAgents().filter(a => a.reachable);
|
|
45
|
+
if (agents.length === 0) return null;
|
|
46
|
+
|
|
47
|
+
let best: SkillMatch | null = null;
|
|
48
|
+
let bestScore = 0;
|
|
49
|
+
|
|
50
|
+
for (const agent of agents) {
|
|
51
|
+
for (const skill of agent.card.skills) {
|
|
52
|
+
const score = scoreSkillMatch(taskDescription, skill);
|
|
53
|
+
if (score > bestScore) {
|
|
54
|
+
bestScore = score;
|
|
55
|
+
best = {
|
|
56
|
+
agentId: agent.id,
|
|
57
|
+
agentName: agent.card.name,
|
|
58
|
+
skillId: skill.id,
|
|
59
|
+
skillName: skill.name,
|
|
60
|
+
confidence: Math.min(score / 3, 1),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return best;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ── Task Decomposer ───────────────────────────────────────────────────── */
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Decompose a complex request into sub-tasks.
|
|
73
|
+
* Uses simple heuristics: split on sentence boundaries and conjunctions.
|
|
74
|
+
* For LLM-based decomposition, the agent tool can call this with pre-decomposed parts.
|
|
75
|
+
*/
|
|
76
|
+
export function decompose(request: string, subtaskDescriptions?: string[]): SubTask[] {
|
|
77
|
+
let descriptions: string[];
|
|
78
|
+
|
|
79
|
+
if (subtaskDescriptions && subtaskDescriptions.length > 0) {
|
|
80
|
+
descriptions = subtaskDescriptions;
|
|
81
|
+
} else {
|
|
82
|
+
descriptions = splitIntoSubtasks(request);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return descriptions.slice(0, MAX_SUBTASKS).map((desc, i) => ({
|
|
86
|
+
id: `st-${randomUUID().slice(0, 8)}`,
|
|
87
|
+
description: desc.trim(),
|
|
88
|
+
assignedAgentId: null,
|
|
89
|
+
matchedSkillId: null,
|
|
90
|
+
status: 'pending' as const,
|
|
91
|
+
result: null,
|
|
92
|
+
error: null,
|
|
93
|
+
dependsOn: [],
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Simple heuristic: split on "and then", "then", "also", numbered lists, semicolons */
|
|
98
|
+
function splitIntoSubtasks(text: string): string[] {
|
|
99
|
+
// Try numbered list first: split on "N. " pattern at boundaries
|
|
100
|
+
const numbered = text.split(/(?:^|\s)(?=\d+\.\s)/m).map(s => s.replace(/^\d+\.\s*/, '').trim()).filter(Boolean);
|
|
101
|
+
if (numbered.length >= 2) return numbered;
|
|
102
|
+
|
|
103
|
+
// Try splitting on conjunctions/semicolons
|
|
104
|
+
const parts = text.split(/;\s*|\.\s+(?:then|and then|also|next|finally)\s+/i).filter(Boolean);
|
|
105
|
+
if (parts.length >= 2) return parts;
|
|
106
|
+
|
|
107
|
+
// Fallback: treat as single task
|
|
108
|
+
return [text];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ── Execution Engine ──────────────────────────────────────────────────── */
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create an orchestration plan from a request.
|
|
115
|
+
*/
|
|
116
|
+
export function createPlan(
|
|
117
|
+
request: string,
|
|
118
|
+
strategy: ExecutionStrategy = 'parallel',
|
|
119
|
+
subtaskDescriptions?: string[],
|
|
120
|
+
): OrchestrationPlan {
|
|
121
|
+
const subtasks = decompose(request, subtaskDescriptions);
|
|
122
|
+
|
|
123
|
+
// Auto-match skills to agents
|
|
124
|
+
for (const st of subtasks) {
|
|
125
|
+
const match = matchSkill(st.description);
|
|
126
|
+
if (match) {
|
|
127
|
+
st.assignedAgentId = match.agentId;
|
|
128
|
+
st.matchedSkillId = match.skillId;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
id: `plan-${randomUUID().slice(0, 8)}`,
|
|
134
|
+
originalRequest: request,
|
|
135
|
+
strategy,
|
|
136
|
+
subtasks,
|
|
137
|
+
createdAt: new Date().toISOString(),
|
|
138
|
+
completedAt: null,
|
|
139
|
+
status: 'planning',
|
|
140
|
+
aggregatedResult: null,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Execute a single sub-task by delegating to its assigned agent.
|
|
146
|
+
*/
|
|
147
|
+
async function executeSubtask(subtask: SubTask, token?: string): Promise<void> {
|
|
148
|
+
if (!subtask.assignedAgentId) {
|
|
149
|
+
subtask.status = 'failed';
|
|
150
|
+
subtask.error = 'No agent assigned to this subtask';
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
subtask.status = 'running';
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// delegateTask has its own 30s RPC timeout via fetchWithTimeout in client.ts
|
|
158
|
+
const task = await delegateTask(subtask.assignedAgentId, subtask.description, token);
|
|
159
|
+
|
|
160
|
+
if (task.status.state === 'TASK_STATE_COMPLETED') {
|
|
161
|
+
subtask.status = 'completed';
|
|
162
|
+
subtask.result = task.artifacts?.[0]?.parts?.[0]?.text
|
|
163
|
+
?? task.history?.find(m => m.role === 'ROLE_AGENT')?.parts?.[0]?.text
|
|
164
|
+
?? 'Completed (no text result)';
|
|
165
|
+
} else if (task.status.state === 'TASK_STATE_FAILED') {
|
|
166
|
+
subtask.status = 'failed';
|
|
167
|
+
subtask.error = task.status.message?.parts?.[0]?.text ?? 'Agent reported failure';
|
|
168
|
+
} else {
|
|
169
|
+
subtask.status = 'completed';
|
|
170
|
+
subtask.result = `Task in progress (state: ${task.status.state})`;
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
subtask.status = 'failed';
|
|
174
|
+
subtask.error = (err as Error).message;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Execute all sub-tasks in an orchestration plan.
|
|
180
|
+
*/
|
|
181
|
+
export async function executePlan(plan: OrchestrationPlan, token?: string): Promise<OrchestrationPlan> {
|
|
182
|
+
plan.status = 'executing';
|
|
183
|
+
|
|
184
|
+
const unassigned = plan.subtasks.filter(st => !st.assignedAgentId);
|
|
185
|
+
if (unassigned.length === plan.subtasks.length) {
|
|
186
|
+
plan.status = 'failed';
|
|
187
|
+
plan.aggregatedResult = 'No agents available for any subtask. Discover agents first using discover_agent.';
|
|
188
|
+
return plan;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Mark unassigned subtasks as failed before execution
|
|
192
|
+
for (const st of plan.subtasks) {
|
|
193
|
+
if (!st.assignedAgentId) {
|
|
194
|
+
st.status = 'failed';
|
|
195
|
+
st.error = 'No matching agent found for this subtask';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const assignedTasks = plan.subtasks.filter(st => st.assignedAgentId);
|
|
200
|
+
|
|
201
|
+
if (plan.strategy === 'parallel') {
|
|
202
|
+
await Promise.allSettled(
|
|
203
|
+
assignedTasks.map(st => executeSubtask(st, token))
|
|
204
|
+
);
|
|
205
|
+
} else {
|
|
206
|
+
// Sequential or dependency-based
|
|
207
|
+
for (const st of plan.subtasks) {
|
|
208
|
+
if (!st.assignedAgentId) {
|
|
209
|
+
st.status = 'failed';
|
|
210
|
+
st.error = 'No agent assigned';
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check dependencies
|
|
215
|
+
if (st.dependsOn.length > 0) {
|
|
216
|
+
const deps = st.dependsOn.map(id => plan.subtasks.find(s => s.id === id));
|
|
217
|
+
const allDone = deps.every(d => d?.status === 'completed');
|
|
218
|
+
if (!allDone) {
|
|
219
|
+
st.status = 'failed';
|
|
220
|
+
st.error = 'Dependencies not met';
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await executeSubtask(st, token);
|
|
226
|
+
|
|
227
|
+
// Stop on failure in sequential mode
|
|
228
|
+
if (plan.strategy === 'sequential' && st.status === 'failed') break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Aggregate results
|
|
233
|
+
const completed = plan.subtasks.filter(st => st.status === 'completed');
|
|
234
|
+
const failed = plan.subtasks.filter(st => st.status === 'failed');
|
|
235
|
+
|
|
236
|
+
if (failed.length === plan.subtasks.length) {
|
|
237
|
+
plan.status = 'failed';
|
|
238
|
+
plan.aggregatedResult = `All ${failed.length} subtasks failed:\n` +
|
|
239
|
+
failed.map(st => `- ${st.description}: ${st.error}`).join('\n');
|
|
240
|
+
} else {
|
|
241
|
+
plan.status = 'completed';
|
|
242
|
+
const parts: string[] = [];
|
|
243
|
+
for (const st of plan.subtasks) {
|
|
244
|
+
if (st.status === 'completed' && st.result) {
|
|
245
|
+
parts.push(`## ${st.description}\n\n${st.result}`);
|
|
246
|
+
} else if (st.status === 'failed') {
|
|
247
|
+
parts.push(`## ${st.description}\n\n[Failed: ${st.error}]`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
plan.aggregatedResult = parts.join('\n\n---\n\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
plan.completedAt = new Date().toISOString();
|
|
254
|
+
return plan;
|
|
255
|
+
}
|
package/app/lib/a2a/types.ts
CHANGED
|
@@ -156,3 +156,57 @@ export const A2A_ERRORS = {
|
|
|
156
156
|
INVALID_PARAMS: { code: -32602, message: 'Invalid params' },
|
|
157
157
|
INTERNAL_ERROR: { code: -32603, message: 'Internal error' },
|
|
158
158
|
} as const;
|
|
159
|
+
|
|
160
|
+
/* ── A2A Client Types (Phase 2) ────────────────────────────────────────── */
|
|
161
|
+
|
|
162
|
+
/** A discovered remote agent with its card and endpoint */
|
|
163
|
+
export interface RemoteAgent {
|
|
164
|
+
/** Unique identifier (derived from agent card URL) */
|
|
165
|
+
id: string;
|
|
166
|
+
/** The agent's card metadata */
|
|
167
|
+
card: AgentCard;
|
|
168
|
+
/** The JSON-RPC endpoint URL */
|
|
169
|
+
endpoint: string;
|
|
170
|
+
/** When this card was last fetched */
|
|
171
|
+
discoveredAt: string;
|
|
172
|
+
/** Whether the agent is currently reachable */
|
|
173
|
+
reachable: boolean;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* ── Orchestration Types (Phase 3) ─────────────────────────────────────── */
|
|
177
|
+
|
|
178
|
+
/** A sub-task decomposed from a user request */
|
|
179
|
+
export interface SubTask {
|
|
180
|
+
id: string;
|
|
181
|
+
description: string;
|
|
182
|
+
assignedAgentId: string | null;
|
|
183
|
+
matchedSkillId: string | null;
|
|
184
|
+
status: 'pending' | 'running' | 'completed' | 'failed';
|
|
185
|
+
result: string | null;
|
|
186
|
+
error: string | null;
|
|
187
|
+
dependsOn: string[];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Execution strategy for sub-tasks */
|
|
191
|
+
export type ExecutionStrategy = 'parallel' | 'sequential' | 'dependency';
|
|
192
|
+
|
|
193
|
+
/** An orchestration plan produced by the decomposer */
|
|
194
|
+
export interface OrchestrationPlan {
|
|
195
|
+
id: string;
|
|
196
|
+
originalRequest: string;
|
|
197
|
+
strategy: ExecutionStrategy;
|
|
198
|
+
subtasks: SubTask[];
|
|
199
|
+
createdAt: string;
|
|
200
|
+
completedAt: string | null;
|
|
201
|
+
status: 'planning' | 'executing' | 'completed' | 'failed';
|
|
202
|
+
aggregatedResult: string | null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Result of matching a sub-task to an agent skill */
|
|
206
|
+
export interface SkillMatch {
|
|
207
|
+
agentId: string;
|
|
208
|
+
agentName: string;
|
|
209
|
+
skillId: string;
|
|
210
|
+
skillName: string;
|
|
211
|
+
confidence: number;
|
|
212
|
+
}
|
package/app/lib/agent/tools.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from '@/lib/fs';
|
|
10
10
|
import { readSkillContentByName, scanSkillDirs } from '@/lib/pi-integration/skills';
|
|
11
11
|
import { callMcporterTool, createMcporterAgentTools, listMcporterServers, listMcporterTools } from '@/lib/pi-integration/mcporter';
|
|
12
|
+
import { a2aTools } from '@/lib/a2a/a2a-tools';
|
|
12
13
|
|
|
13
14
|
// Max chars per file to avoid token overflow (~100k chars ≈ ~25k tokens)
|
|
14
15
|
const MAX_FILE_CHARS = 20_000;
|
|
@@ -186,10 +187,11 @@ export function getOrganizeTools(): AgentTool<any>[] {
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
export async function getRequestScopedTools(): Promise<AgentTool<any>[]> {
|
|
190
|
+
const baseTools = [...knowledgeBaseTools, ...a2aTools];
|
|
189
191
|
try {
|
|
190
192
|
const result = await listMcporterServers();
|
|
191
193
|
const okServers = (result.servers ?? []).filter((server) => server.status === 'ok');
|
|
192
|
-
if (okServers.length === 0) return
|
|
194
|
+
if (okServers.length === 0) return baseTools;
|
|
193
195
|
|
|
194
196
|
const detailedServers = await Promise.all(okServers.map(async (server) => {
|
|
195
197
|
try {
|
|
@@ -200,10 +202,10 @@ export async function getRequestScopedTools(): Promise<AgentTool<any>[]> {
|
|
|
200
202
|
}));
|
|
201
203
|
|
|
202
204
|
const dynamicMcpTools = createMcporterAgentTools(detailedServers);
|
|
203
|
-
if (dynamicMcpTools.length === 0) return
|
|
204
|
-
return [...
|
|
205
|
+
if (dynamicMcpTools.length === 0) return baseTools;
|
|
206
|
+
return [...baseTools, ...dynamicMcpTools];
|
|
205
207
|
} catch {
|
|
206
|
-
return
|
|
208
|
+
return baseTools;
|
|
207
209
|
}
|
|
208
210
|
}
|
|
209
211
|
|
package/app/lib/i18n-en.ts
CHANGED
|
@@ -241,6 +241,27 @@ export const en = {
|
|
|
241
241
|
agentDetailPanelAria: 'Agent connection configuration',
|
|
242
242
|
agentDetailTransport: 'Transport',
|
|
243
243
|
agentDetailSnippet: 'Config snippet',
|
|
244
|
+
// A2A
|
|
245
|
+
a2aLabel: 'A2A',
|
|
246
|
+
a2aReady: 'A2A Ready',
|
|
247
|
+
a2aChecking: 'Checking A2A...',
|
|
248
|
+
a2aUnavailable: 'A2A Unavailable',
|
|
249
|
+
a2aRemote: 'Remote',
|
|
250
|
+
a2aDiscover: 'Discover Remote Agent',
|
|
251
|
+
a2aDiscoverHint: 'Connect to an external A2A agent by URL',
|
|
252
|
+
a2aDiscoverPlaceholder: 'https://agent.example.com',
|
|
253
|
+
a2aDiscovering: 'Discovering...',
|
|
254
|
+
a2aDiscoverSuccess: 'Agent discovered!',
|
|
255
|
+
a2aDiscoverFailed: 'No A2A agent found at this URL',
|
|
256
|
+
a2aDiscoverFailedHint: 'The server may not support the A2A protocol. Check the URL and try again.',
|
|
257
|
+
a2aSkills: 'Skills',
|
|
258
|
+
a2aEndpoint: 'Endpoint',
|
|
259
|
+
a2aVersion: 'Version',
|
|
260
|
+
a2aCapabilities: 'A2A Capabilities',
|
|
261
|
+
a2aStatus: 'Status',
|
|
262
|
+
a2aConnected: 'Connected & A2A Ready',
|
|
263
|
+
a2aNoRemote: 'No remote agents',
|
|
264
|
+
a2aNoRemoteHint: 'Discover remote agents to enable cross-agent delegation.',
|
|
244
265
|
},
|
|
245
266
|
plugins: {
|
|
246
267
|
title: 'Plugins',
|
|
@@ -1463,4 +1484,35 @@ prompt: "Here's my resume, read it and organize my info into MindOS.",
|
|
|
1463
1484
|
],
|
|
1464
1485
|
},
|
|
1465
1486
|
},
|
|
1487
|
+
|
|
1488
|
+
/** Disabled-state and contextual tooltip hints */
|
|
1489
|
+
hints: {
|
|
1490
|
+
noValidFiles: 'No valid files selected',
|
|
1491
|
+
aiOrganizing: 'AI is organizing',
|
|
1492
|
+
importInProgress: 'Import in progress',
|
|
1493
|
+
templateInitializing: 'Another template is being initialized',
|
|
1494
|
+
configureAiKey: 'Configure API key in Settings → AI',
|
|
1495
|
+
syncInProgress: 'Sync already in progress',
|
|
1496
|
+
toggleInProgress: 'Toggle operation in progress',
|
|
1497
|
+
typeMessage: 'Type a message',
|
|
1498
|
+
mentionInProgress: 'Mention or command in progress',
|
|
1499
|
+
cleanupInProgress: 'Cleanup already in progress',
|
|
1500
|
+
tokenResetInProgress: 'Token reset in progress',
|
|
1501
|
+
aiNotConfigured: 'AI not configured or generation in progress',
|
|
1502
|
+
generationInProgress: 'Generation in progress or AI not configured',
|
|
1503
|
+
cannotJumpForward: 'Cannot jump forward in setup',
|
|
1504
|
+
testInProgressOrNoKey: 'Test in progress or no API key',
|
|
1505
|
+
workflowStepRunning: 'Workflow step already running',
|
|
1506
|
+
workflowRunning: 'Workflow step is running',
|
|
1507
|
+
sessionHistory: 'Session history',
|
|
1508
|
+
newSession: 'New session',
|
|
1509
|
+
attachFile: 'Attach local file',
|
|
1510
|
+
maximizePanel: 'Maximize panel',
|
|
1511
|
+
restorePanel: 'Restore panel',
|
|
1512
|
+
dockToSide: 'Dock to side panel',
|
|
1513
|
+
openAsPopup: 'Open as popup',
|
|
1514
|
+
closePanel: 'Close',
|
|
1515
|
+
newChat: 'New chat',
|
|
1516
|
+
closeSession: 'Close session',
|
|
1517
|
+
},
|
|
1466
1518
|
} as const;
|