@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.
Files changed (38) hide show
  1. package/README.md +19 -3
  2. package/README_zh.md +19 -3
  3. package/app/app/api/a2a/discover/route.ts +23 -0
  4. package/app/components/CreateSpaceModal.tsx +1 -0
  5. package/app/components/ImportModal.tsx +3 -0
  6. package/app/components/OnboardingView.tsx +1 -0
  7. package/app/components/RightAskPanel.tsx +4 -2
  8. package/app/components/SidebarLayout.tsx +11 -2
  9. package/app/components/agents/DiscoverAgentModal.tsx +149 -0
  10. package/app/components/ask/AskContent.tsx +21 -9
  11. package/app/components/ask/SessionTabBar.tsx +70 -0
  12. package/app/components/echo/EchoInsightCollapsible.tsx +4 -0
  13. package/app/components/panels/AgentsPanel.tsx +25 -2
  14. package/app/components/renderers/workflow/WorkflowRenderer.tsx +5 -0
  15. package/app/components/settings/AiTab.tsx +1 -0
  16. package/app/components/settings/KnowledgeTab.tsx +2 -0
  17. package/app/components/settings/SyncTab.tsx +2 -0
  18. package/app/components/setup/StepDots.tsx +5 -1
  19. package/app/hooks/useA2aRegistry.ts +53 -0
  20. package/app/hooks/useAskSession.ts +44 -25
  21. package/app/lib/a2a/a2a-tools.ts +212 -0
  22. package/app/lib/a2a/client.ts +207 -0
  23. package/app/lib/a2a/index.ts +8 -0
  24. package/app/lib/a2a/orchestrator.ts +255 -0
  25. package/app/lib/a2a/types.ts +54 -0
  26. package/app/lib/agent/tools.ts +6 -4
  27. package/app/lib/i18n-en.ts +52 -0
  28. package/app/lib/i18n-zh.ts +52 -0
  29. package/app/next-env.d.ts +1 -1
  30. package/bin/cli.js +180 -171
  31. package/bin/commands/agent.js +110 -18
  32. package/bin/commands/api.js +5 -3
  33. package/bin/commands/ask.js +3 -3
  34. package/bin/commands/file.js +13 -13
  35. package/bin/commands/search.js +2 -2
  36. package/bin/commands/space.js +64 -10
  37. package/bin/lib/command.js +10 -0
  38. package/package.json +1 -1
@@ -3,6 +3,7 @@
3
3
  import { useMemo, useState, useRef, useCallback } from 'react';
4
4
  import { Play, SkipForward, RotateCcw, CheckCircle2, Circle, Loader2, AlertCircle, ChevronDown, Sparkles } from 'lucide-react';
5
5
  import type { RendererContext } from '@/lib/renderers/registry';
6
+ import { useLocale } from '@/lib/LocaleContext';
6
7
 
7
8
  // ─── Types ────────────────────────────────────────────────────────────────────
8
9
 
@@ -171,6 +172,7 @@ function StepCard({
171
172
  onSkip: () => void;
172
173
  canRun: boolean;
173
174
  }) {
175
+ const { t } = useLocale();
174
176
  const [expanded, setExpanded] = useState(false);
175
177
  const hasBody = step.body.trim().length > 0;
176
178
  const hasOutput = step.output.length > 0;
@@ -201,6 +203,7 @@ function StepCard({
201
203
  <button
202
204
  onClick={onRun}
203
205
  disabled={!canRun}
206
+ title={!canRun ? t.hints.workflowStepRunning : undefined}
204
207
  style={{
205
208
  display: 'flex', alignItems: 'center', gap: 4,
206
209
  padding: '3px 10px', borderRadius: 6, fontSize: '0.72rem',
@@ -268,6 +271,7 @@ function StepCard({
268
271
  // ─── Main renderer ────────────────────────────────────────────────────────────
269
272
 
270
273
  export function WorkflowRenderer({ filePath, content }: RendererContext) {
274
+ const { t } = useLocale();
271
275
  const parsed = useMemo(() => parseWorkflow(content), [content]);
272
276
  const [steps, setSteps] = useState<WorkflowStep[]>(() => parsed.steps);
273
277
  const [running, setRunning] = useState(false);
@@ -357,6 +361,7 @@ export function WorkflowRenderer({ filePath, content }: RendererContext) {
357
361
  <button
358
362
  onClick={() => runStep(nextPendingIdx)}
359
363
  disabled={running}
364
+ title={running ? t.hints.workflowRunning : undefined}
360
365
  style={{
361
366
  display: 'flex', alignItems: 'center', gap: 5,
362
367
  padding: '4px 12px', borderRadius: 7, fontSize: '0.75rem',
@@ -125,6 +125,7 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
125
125
  <button
126
126
  type="button"
127
127
  disabled={disabled}
128
+ title={disabled ? t.hints.testInProgressOrNoKey : undefined}
128
129
  onClick={() => handleTestKey(providerName)}
129
130
  className="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
130
131
  >
@@ -173,6 +173,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
173
173
  <button
174
174
  onClick={() => setShowCleanupConfirm(true)}
175
175
  disabled={cleaningUp}
176
+ title={cleaningUp ? t.hints.cleanupInProgress : undefined}
176
177
  className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0 disabled:opacity-50"
177
178
  >
178
179
  {cleaningUp ? <Loader2 size={12} className="animate-spin" /> : <Trash2 size={12} />}
@@ -249,6 +250,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
249
250
  type="button"
250
251
  onClick={handleResetToken}
251
252
  disabled={resetting}
253
+ title={resetting ? t.hints.tokenResetInProgress : undefined}
252
254
  className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
253
255
  >
254
256
  <RefreshCw size={12} className={resetting ? 'animate-spin' : ''} />
@@ -302,6 +302,7 @@ export function SyncTab({ t }: SyncTabProps) {
302
302
  type="button"
303
303
  onClick={handleSyncNow}
304
304
  disabled={syncing}
305
+ title={syncing ? t.hints.syncInProgress : undefined}
305
306
  className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
306
307
  >
307
308
  <RefreshCw size={12} className={syncing ? 'animate-spin' : ''} />
@@ -311,6 +312,7 @@ export function SyncTab({ t }: SyncTabProps) {
311
312
  type="button"
312
313
  onClick={handleToggle}
313
314
  disabled={toggling}
315
+ title={toggling ? t.hints.toggleInProgress : undefined}
314
316
  className={`flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${
315
317
  status.enabled
316
318
  ? 'border-border text-muted-foreground hover:text-destructive hover:border-destructive/50'
@@ -1,5 +1,7 @@
1
1
  'use client';
2
2
 
3
+ import { useLocale } from '@/lib/LocaleContext';
4
+
3
5
  export interface StepDotsProps {
4
6
  step: number;
5
7
  setStep: (s: number) => void;
@@ -8,6 +10,7 @@ export interface StepDotsProps {
8
10
  }
9
11
 
10
12
  export default function StepDots({ step, setStep, stepTitles, disabled }: StepDotsProps) {
13
+ const { t } = useLocale();
11
14
  return (
12
15
  <div className="flex items-center gap-2 mb-8" role="navigation" aria-label="Setup steps">
13
16
  {stepTitles.map((title: string, i: number) => (
@@ -17,7 +20,8 @@ export default function StepDots({ step, setStep, stepTitles, disabled }: StepDo
17
20
  aria-current={i === step ? 'step' : undefined}
18
21
  aria-label={title}
19
22
  className="flex flex-col items-center gap-1 p-1 -m-1 disabled:cursor-not-allowed disabled:opacity-60"
20
- disabled={disabled || i >= step}>
23
+ disabled={disabled || i >= step}
24
+ title={(disabled || i >= step) ? t.hints.cannotJumpForward : undefined}>
21
25
  <div
22
26
  className="w-6 h-6 rounded-full text-xs font-medium flex items-center justify-center transition-colors"
23
27
  style={{
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+ import type { RemoteAgent } from '@/lib/a2a/types';
5
+
6
+ interface A2aRegistry {
7
+ agents: RemoteAgent[];
8
+ discovering: boolean;
9
+ error: string | null;
10
+ discover: (url: string) => Promise<RemoteAgent | null>;
11
+ refresh: () => void;
12
+ }
13
+
14
+ export function useA2aRegistry(): A2aRegistry {
15
+ const [agents, setAgents] = useState<RemoteAgent[]>([]);
16
+ const [discovering, setDiscovering] = useState(false);
17
+ const [error, setError] = useState<string | null>(null);
18
+
19
+ const discover = useCallback(async (url: string): Promise<RemoteAgent | null> => {
20
+ setDiscovering(true);
21
+ setError(null);
22
+ try {
23
+ const res = await fetch('/api/a2a/discover', {
24
+ method: 'POST',
25
+ headers: { 'Content-Type': 'application/json' },
26
+ body: JSON.stringify({ url }),
27
+ });
28
+ const data = await res.json();
29
+ if (data.agent) {
30
+ setAgents(prev => {
31
+ const exists = prev.some(a => a.id === data.agent.id);
32
+ if (exists) return prev.map(a => a.id === data.agent.id ? data.agent : a);
33
+ return [...prev, data.agent];
34
+ });
35
+ return data.agent as RemoteAgent;
36
+ }
37
+ setError(data.error || 'Discovery failed');
38
+ return null;
39
+ } catch (err) {
40
+ setError((err as Error).message);
41
+ return null;
42
+ } finally {
43
+ setDiscovering(false);
44
+ }
45
+ }, []);
46
+
47
+ const refresh = useCallback(() => {
48
+ setAgents([]);
49
+ setError(null);
50
+ }, []);
51
+
52
+ return { agents, discovering, error, discover, refresh };
53
+ }
@@ -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 sorted = (await fetchSessions())
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
- const next = [fresh, ...sorted].slice(0, MAX_SESSIONS);
100
- setSessions(next);
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 next = [fresh, ...prev]
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
- void removeSession(id);
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
- void upsertSession(fresh);
193
+ // No upsertSession — memory only
176
194
  }
177
195
  },
178
196
  [activeSessionId, currentFile, sessions],
179
197
  );
180
198
 
181
199
  const clearAllSessions = useCallback(() => {
182
- const allIds = sessions.map(s => s.id);
183
- void removeSessions(allIds);
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
- void upsertSession(fresh);
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
+ ];