@geminilight/mindos 0.6.27 → 0.6.29

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 (67) hide show
  1. package/app/app/api/a2a/agents/route.ts +9 -0
  2. package/app/app/api/a2a/delegations/route.ts +9 -0
  3. package/app/app/api/a2a/discover/route.ts +2 -0
  4. package/app/app/api/a2a/route.ts +6 -6
  5. package/app/app/api/acp/detect/route.ts +91 -0
  6. package/app/app/api/acp/registry/route.ts +31 -0
  7. package/app/app/api/acp/session/route.ts +55 -0
  8. package/app/app/layout.tsx +2 -0
  9. package/app/components/DirView.tsx +64 -2
  10. package/app/components/FileTree.tsx +19 -0
  11. package/app/components/GuideCard.tsx +7 -17
  12. package/app/components/MarkdownView.tsx +2 -0
  13. package/app/components/SearchModal.tsx +234 -80
  14. package/app/components/agents/AgentDetailContent.tsx +51 -6
  15. package/app/components/agents/AgentsContentPage.tsx +24 -6
  16. package/app/components/agents/AgentsOverviewSection.tsx +11 -0
  17. package/app/components/agents/AgentsPanelA2aTab.tsx +445 -0
  18. package/app/components/agents/SkillDetailPopover.tsx +4 -9
  19. package/app/components/agents/agents-content-model.ts +2 -2
  20. package/app/components/ask/AskContent.tsx +8 -0
  21. package/app/components/help/HelpContent.tsx +74 -18
  22. package/app/components/panels/AgentsPanel.tsx +1 -0
  23. package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
  24. package/app/components/panels/AgentsPanelAgentListRow.tsx +10 -1
  25. package/app/components/panels/AgentsPanelHubNav.tsx +8 -1
  26. package/app/components/panels/EchoPanel.tsx +5 -1
  27. package/app/components/panels/EchoSidebarStats.tsx +136 -0
  28. package/app/components/settings/KnowledgeTab.tsx +3 -6
  29. package/app/components/settings/McpSkillsSection.tsx +4 -5
  30. package/app/components/settings/McpTab.tsx +6 -8
  31. package/app/components/setup/StepSecurity.tsx +4 -5
  32. package/app/components/setup/index.tsx +5 -11
  33. package/app/components/ui/Toaster.tsx +39 -0
  34. package/app/hooks/useA2aRegistry.ts +6 -1
  35. package/app/hooks/useAcpDetection.ts +65 -0
  36. package/app/hooks/useAcpRegistry.ts +51 -0
  37. package/app/hooks/useDelegationHistory.ts +49 -0
  38. package/app/lib/a2a/client.ts +49 -5
  39. package/app/lib/a2a/orchestrator.ts +0 -1
  40. package/app/lib/a2a/task-handler.ts +4 -4
  41. package/app/lib/a2a/types.ts +15 -0
  42. package/app/lib/acp/acp-tools.ts +93 -0
  43. package/app/lib/acp/bridge.ts +138 -0
  44. package/app/lib/acp/index.ts +24 -0
  45. package/app/lib/acp/registry.ts +135 -0
  46. package/app/lib/acp/session.ts +264 -0
  47. package/app/lib/acp/subprocess.ts +209 -0
  48. package/app/lib/acp/types.ts +136 -0
  49. package/app/lib/agent/tools.ts +2 -1
  50. package/app/lib/i18n/_core.ts +22 -0
  51. package/app/lib/i18n/index.ts +35 -0
  52. package/app/lib/i18n/modules/ai-chat.ts +215 -0
  53. package/app/lib/i18n/modules/common.ts +71 -0
  54. package/app/lib/i18n/modules/features.ts +153 -0
  55. package/app/lib/i18n/modules/knowledge.ts +425 -0
  56. package/app/lib/i18n/modules/navigation.ts +151 -0
  57. package/app/lib/i18n/modules/onboarding.ts +523 -0
  58. package/app/lib/i18n/modules/panels.ts +1052 -0
  59. package/app/lib/i18n/modules/settings.ts +585 -0
  60. package/app/lib/i18n-en.ts +2 -1518
  61. package/app/lib/i18n-zh.ts +2 -1542
  62. package/app/lib/i18n.ts +3 -6
  63. package/app/lib/toast.ts +79 -0
  64. package/bin/cli.js +25 -25
  65. package/bin/commands/file.js +29 -2
  66. package/bin/commands/space.js +249 -91
  67. package/package.json +1 -1
@@ -0,0 +1,39 @@
1
+ 'use client';
2
+
3
+ import { useSyncExternalStore } from 'react';
4
+ import { Check, AlertCircle, Info, X } from 'lucide-react';
5
+ import { subscribe, getSnapshot, dismiss, type Toast } from '@/lib/toast';
6
+
7
+ const icons: Record<Toast['type'], React.ReactNode> = {
8
+ success: <Check size={15} className="text-success shrink-0" />,
9
+ error: <AlertCircle size={15} className="text-error shrink-0" />,
10
+ info: <Info size={15} className="text-muted-foreground shrink-0" />,
11
+ };
12
+
13
+ export default function Toaster() {
14
+ const toasts = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
15
+
16
+ if (toasts.length === 0) return null;
17
+
18
+ return (
19
+ <div className="fixed bottom-4 right-4 z-50 flex flex-col-reverse gap-2 pointer-events-none" aria-live="polite">
20
+ {toasts.map((t) => (
21
+ <div
22
+ key={t.id}
23
+ className="pointer-events-auto flex items-center gap-2.5 bg-card border border-border rounded-lg shadow-lg px-4 py-2.5 min-w-[180px] max-w-[320px] animate-in slide-in-from-right-4 fade-in duration-200"
24
+ >
25
+ {icons[t.type]}
26
+ <span className="text-sm text-foreground flex-1 truncate">{t.message}</span>
27
+ <button
28
+ type="button"
29
+ onClick={() => dismiss(t.id)}
30
+ className="shrink-0 p-0.5 rounded text-muted-foreground hover:text-foreground transition-colors"
31
+ aria-label="Dismiss"
32
+ >
33
+ <X size={13} />
34
+ </button>
35
+ </div>
36
+ ))}
37
+ </div>
38
+ );
39
+ }
@@ -8,6 +8,7 @@ interface A2aRegistry {
8
8
  discovering: boolean;
9
9
  error: string | null;
10
10
  discover: (url: string) => Promise<RemoteAgent | null>;
11
+ remove: (id: string) => void;
11
12
  refresh: () => void;
12
13
  }
13
14
 
@@ -44,10 +45,14 @@ export function useA2aRegistry(): A2aRegistry {
44
45
  }
45
46
  }, []);
46
47
 
48
+ const remove = useCallback((id: string) => {
49
+ setAgents(prev => prev.filter(a => a.id !== id));
50
+ }, []);
51
+
47
52
  const refresh = useCallback(() => {
48
53
  setAgents([]);
49
54
  setError(null);
50
55
  }, []);
51
56
 
52
- return { agents, discovering, error, discover, refresh };
57
+ return { agents, discovering, error, discover, remove, refresh };
53
58
  }
@@ -0,0 +1,65 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+
5
+ export interface DetectedAgent {
6
+ id: string;
7
+ name: string;
8
+ binaryPath: string;
9
+ }
10
+
11
+ export interface NotInstalledAgent {
12
+ id: string;
13
+ name: string;
14
+ installCmd: string;
15
+ }
16
+
17
+ interface AcpDetectionState {
18
+ installedAgents: DetectedAgent[];
19
+ notInstalledAgents: NotInstalledAgent[];
20
+ loading: boolean;
21
+ error: string | null;
22
+ refresh: () => void;
23
+ }
24
+
25
+ export function useAcpDetection(): AcpDetectionState {
26
+ const [installedAgents, setInstalledAgents] = useState<DetectedAgent[]>([]);
27
+ const [notInstalledAgents, setNotInstalledAgents] = useState<NotInstalledAgent[]>([]);
28
+ const [loading, setLoading] = useState(true);
29
+ const [error, setError] = useState<string | null>(null);
30
+ const [trigger, setTrigger] = useState(0);
31
+
32
+ const refresh = useCallback(() => {
33
+ setTrigger((n) => n + 1);
34
+ }, []);
35
+
36
+ useEffect(() => {
37
+ let cancelled = false;
38
+ setLoading(true);
39
+ setError(null);
40
+
41
+ fetch('/api/acp/detect')
42
+ .then((res) => {
43
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
44
+ return res.json();
45
+ })
46
+ .then((data) => {
47
+ if (cancelled) return;
48
+ setInstalledAgents(data.installed ?? []);
49
+ setNotInstalledAgents(data.notInstalled ?? []);
50
+ })
51
+ .catch((err) => {
52
+ if (cancelled) return;
53
+ setError((err as Error).message);
54
+ })
55
+ .finally(() => {
56
+ if (!cancelled) setLoading(false);
57
+ });
58
+
59
+ return () => {
60
+ cancelled = true;
61
+ };
62
+ }, [trigger]);
63
+
64
+ return { installedAgents, notInstalledAgents, loading, error, refresh };
65
+ }
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import type { AcpRegistryEntry } from '@/lib/acp/types';
5
+
6
+ interface AcpRegistryState {
7
+ agents: AcpRegistryEntry[];
8
+ loading: boolean;
9
+ error: string | null;
10
+ retry: () => void;
11
+ }
12
+
13
+ export function useAcpRegistry(): AcpRegistryState {
14
+ const [agents, setAgents] = useState<AcpRegistryEntry[]>([]);
15
+ const [loading, setLoading] = useState(true);
16
+ const [error, setError] = useState<string | null>(null);
17
+ const [trigger, setTrigger] = useState(0);
18
+
19
+ const retry = useCallback(() => {
20
+ setTrigger((n) => n + 1);
21
+ }, []);
22
+
23
+ useEffect(() => {
24
+ let cancelled = false;
25
+ setLoading(true);
26
+ setError(null);
27
+
28
+ fetch('/api/acp/registry')
29
+ .then((res) => {
30
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
31
+ return res.json();
32
+ })
33
+ .then((data) => {
34
+ if (cancelled) return;
35
+ setAgents(data.registry?.agents ?? []);
36
+ })
37
+ .catch((err) => {
38
+ if (cancelled) return;
39
+ setError((err as Error).message);
40
+ })
41
+ .finally(() => {
42
+ if (!cancelled) setLoading(false);
43
+ });
44
+
45
+ return () => {
46
+ cancelled = true;
47
+ };
48
+ }, [trigger]);
49
+
50
+ return { agents, loading, error, retry };
51
+ }
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import type { DelegationRecord } from '@/lib/a2a/types';
5
+
6
+ interface DelegationHistory {
7
+ delegations: DelegationRecord[];
8
+ loading: boolean;
9
+ refresh: () => void;
10
+ }
11
+
12
+ const POLL_INTERVAL_MS = 5_000;
13
+
14
+ export function useDelegationHistory(active: boolean): DelegationHistory {
15
+ const [delegations, setDelegations] = useState<DelegationRecord[]>([]);
16
+ const [loading, setLoading] = useState(false);
17
+ const mountedRef = useRef(true);
18
+
19
+ const fetchHistory = useCallback(async () => {
20
+ setLoading(true);
21
+ try {
22
+ const res = await fetch('/api/a2a/delegations');
23
+ if (!res.ok) return;
24
+ const data = await res.json();
25
+ if (mountedRef.current) {
26
+ setDelegations(data.delegations ?? []);
27
+ }
28
+ } catch {
29
+ // silently ignore fetch errors
30
+ } finally {
31
+ if (mountedRef.current) setLoading(false);
32
+ }
33
+ }, []);
34
+
35
+ useEffect(() => {
36
+ mountedRef.current = true;
37
+ return () => { mountedRef.current = false; };
38
+ }, []);
39
+
40
+ // Fetch on mount and poll while active
41
+ useEffect(() => {
42
+ if (!active) return;
43
+ fetchHistory();
44
+ const id = setInterval(fetchHistory, POLL_INTERVAL_MS);
45
+ return () => clearInterval(id);
46
+ }, [active, fetchHistory]);
47
+
48
+ return { delegations, loading, refresh: fetchHistory };
49
+ }
@@ -10,6 +10,7 @@ import type {
10
10
  JsonRpcRequest,
11
11
  JsonRpcResponse,
12
12
  SendMessageParams,
13
+ DelegationRecord,
13
14
  } from './types';
14
15
 
15
16
  /* ── Constants ─────────────────────────────────────────────────────────── */
@@ -22,6 +23,20 @@ const CARD_CACHE_TTL_MS = 5 * 60 * 1000; // 5 min
22
23
 
23
24
  const registry = new Map<string, RemoteAgent>();
24
25
 
26
+ /* ── Delegation History ────────────────────────────────────────────────── */
27
+
28
+ const delegationHistory: DelegationRecord[] = [];
29
+
30
+ /** Get all delegation history records */
31
+ export function getDelegationHistory(): DelegationRecord[] {
32
+ return [...delegationHistory];
33
+ }
34
+
35
+ /** Clear all delegation history records */
36
+ export function clearDelegationHistory(): void {
37
+ delegationHistory.length = 0;
38
+ }
39
+
25
40
  /** Derive a stable ID from a URL (includes protocol to avoid collisions) */
26
41
  function urlToId(url: string): string {
27
42
  try {
@@ -152,6 +167,19 @@ export async function delegateTask(
152
167
  if (!agent) throw new Error(`Agent not found: ${agentId}`);
153
168
  if (!agent.reachable) throw new Error(`Agent not reachable: ${agent.card.name}`);
154
169
 
170
+ const record: DelegationRecord = {
171
+ id: `del-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
172
+ agentId,
173
+ agentName: agent.card.name,
174
+ message,
175
+ status: 'pending',
176
+ startedAt: new Date().toISOString(),
177
+ completedAt: null,
178
+ result: null,
179
+ error: null,
180
+ };
181
+ delegationHistory.push(record);
182
+
155
183
  const params: SendMessageParams = {
156
184
  message: {
157
185
  role: 'ROLE_USER',
@@ -160,13 +188,29 @@ export async function delegateTask(
160
188
  configuration: { blocking: true },
161
189
  };
162
190
 
163
- const response = await jsonRpcCall(agent.endpoint, 'SendMessage', params, token);
191
+ try {
192
+ const response = await jsonRpcCall(agent.endpoint, 'SendMessage', params, token);
164
193
 
165
- if (response.error) {
166
- throw new Error(`A2A error [${response.error.code}]: ${response.error.message}`);
167
- }
194
+ if (response.error) {
195
+ record.status = 'failed';
196
+ record.completedAt = new Date().toISOString();
197
+ record.error = `A2A error [${response.error.code}]: ${response.error.message}`;
198
+ throw new Error(record.error);
199
+ }
168
200
 
169
- return response.result as A2ATask;
201
+ const task = response.result as A2ATask;
202
+ record.status = 'completed';
203
+ record.completedAt = new Date().toISOString();
204
+ record.result = task.artifacts?.[0]?.parts?.[0]?.text ?? null;
205
+ return task;
206
+ } catch (err) {
207
+ if (record.status === 'pending') {
208
+ record.status = 'failed';
209
+ record.completedAt = new Date().toISOString();
210
+ record.error = (err as Error).message;
211
+ }
212
+ throw err;
213
+ }
170
214
  }
171
215
 
172
216
  /**
@@ -5,7 +5,6 @@
5
5
 
6
6
  import { randomUUID } from 'crypto';
7
7
  import type {
8
- RemoteAgent,
9
8
  SubTask,
10
9
  OrchestrationPlan,
11
10
  ExecutionStrategy,
@@ -200,18 +200,18 @@ export function handleGetTask(params: GetTaskParams): A2ATask | null {
200
200
  return tasks.get(params.id) ?? null;
201
201
  }
202
202
 
203
- export function handleCancelTask(params: CancelTaskParams): A2ATask | null {
203
+ export function handleCancelTask(params: CancelTaskParams): { task: A2ATask | null; reason: 'not_found' | 'not_cancelable' | 'ok' } {
204
204
  const task = tasks.get(params.id);
205
- if (!task) return null;
205
+ if (!task) return { task: null, reason: 'not_found' };
206
206
 
207
207
  const terminalStates: TaskState[] = ['TASK_STATE_COMPLETED', 'TASK_STATE_FAILED', 'TASK_STATE_CANCELED', 'TASK_STATE_REJECTED'];
208
- if (terminalStates.includes(task.status.state)) return null; // not cancelable
208
+ if (terminalStates.includes(task.status.state)) return { task: null, reason: 'not_cancelable' };
209
209
 
210
210
  task.status = {
211
211
  state: 'TASK_STATE_CANCELED',
212
212
  timestamp: new Date().toISOString(),
213
213
  };
214
- return task;
214
+ return { task, reason: 'ok' };
215
215
  }
216
216
 
217
217
  /* ── Helpers ───────────────────────────────────────────────────────────── */
@@ -210,3 +210,18 @@ export interface SkillMatch {
210
210
  skillName: string;
211
211
  confidence: number;
212
212
  }
213
+
214
+ /* ── Delegation History (Phase 3 UI) ─────────────────────────────────── */
215
+
216
+ /** A recorded delegation attempt for the history log */
217
+ export interface DelegationRecord {
218
+ id: string;
219
+ agentId: string;
220
+ agentName: string;
221
+ message: string;
222
+ status: 'pending' | 'completed' | 'failed';
223
+ startedAt: string;
224
+ completedAt: string | null;
225
+ result: string | null;
226
+ error: string | null;
227
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * ACP Agent Tools — Expose ACP capabilities as tools
3
+ * for the MindOS built-in agent to discover and invoke ACP agents.
4
+ */
5
+
6
+ import { Type, type Static } from '@sinclair/typebox';
7
+ import type { AgentTool } from '@mariozechner/pi-agent-core';
8
+ import { getAcpAgents, findAcpAgent } from './registry';
9
+ import { createSessionFromEntry, prompt, closeSession } from './session';
10
+
11
+ function textResult(text: string) {
12
+ return { content: [{ type: 'text' as const, text }], details: {} };
13
+ }
14
+
15
+ /* ── Parameter Schemas ─────────────────────────────────────────────────── */
16
+
17
+ const ListAcpAgentsParams = Type.Object({
18
+ tag: Type.Optional(Type.String({ description: 'Optional tag to filter agents by (e.g. "coding", "search")' })),
19
+ });
20
+
21
+ const CallAcpAgentParams = Type.Object({
22
+ agent_id: Type.String({ description: 'ID of the ACP agent from the registry (from list_acp_agents)' }),
23
+ message: Type.String({ description: 'Natural language message to send to the ACP agent' }),
24
+ });
25
+
26
+ /* ── Tool Implementations ──────────────────────────────────────────────── */
27
+
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ export const acpTools: AgentTool<any>[] = [
30
+ {
31
+ name: 'list_acp_agents',
32
+ label: 'List ACP Agents',
33
+ description: 'List available ACP (Agent Client Protocol) agents from the public registry. These are local subprocess-based agents like Gemini CLI, Claude, Copilot, etc. Optionally filter by tag.',
34
+ parameters: ListAcpAgentsParams,
35
+ execute: async (_id: string, params: Static<typeof ListAcpAgentsParams>) => {
36
+ try {
37
+ let agents = await getAcpAgents();
38
+
39
+ if (params.tag) {
40
+ const tag = params.tag.toLowerCase();
41
+ agents = agents.filter(a =>
42
+ a.tags?.some(t => t.toLowerCase().includes(tag))
43
+ );
44
+ }
45
+
46
+ if (agents.length === 0) {
47
+ return textResult(
48
+ params.tag
49
+ ? `No ACP agents found with tag "${params.tag}". Try list_acp_agents without a tag filter.`
50
+ : 'No ACP agents found in the registry. The registry may be unavailable.'
51
+ );
52
+ }
53
+
54
+ const lines = agents.map(a => {
55
+ const tags = a.tags?.join(', ') || 'none';
56
+ return `- **${a.name}** (id: \`${a.id}\`, transport: ${a.transport})\n ${a.description}\n Tags: ${tags}`;
57
+ });
58
+
59
+ return textResult(`Available ACP agents (${agents.length}):\n\n${lines.join('\n\n')}`);
60
+ } catch (err) {
61
+ return textResult(`Failed to list ACP agents: ${(err as Error).message}`);
62
+ }
63
+ },
64
+ },
65
+
66
+ {
67
+ name: 'call_acp_agent',
68
+ label: 'Call ACP Agent',
69
+ description: 'Spawn an ACP agent, send it a message, and return the result. The agent runs as a local subprocess. Use list_acp_agents first to see available agents.',
70
+ parameters: CallAcpAgentParams,
71
+ execute: async (_id: string, params: Static<typeof CallAcpAgentParams>) => {
72
+ try {
73
+ const entry = await findAcpAgent(params.agent_id);
74
+ if (!entry) {
75
+ return textResult(`ACP agent not found: ${params.agent_id}. Use list_acp_agents to see available agents.`);
76
+ }
77
+
78
+ const session = await createSessionFromEntry(entry);
79
+
80
+ try {
81
+ const response = await prompt(session.id, params.message);
82
+ return textResult(
83
+ `**${entry.name}** responded:\n\n${response.text || '(empty response)'}`
84
+ );
85
+ } finally {
86
+ await closeSession(session.id).catch(() => {});
87
+ }
88
+ } catch (err) {
89
+ return textResult(`ACP call failed: ${(err as Error).message}`);
90
+ }
91
+ },
92
+ },
93
+ ];
@@ -0,0 +1,138 @@
1
+ /**
2
+ * A2A-ACP Bridge — Translate between A2A protocol and ACP protocol.
3
+ * Allows A2A agents to delegate to ACP agents transparently,
4
+ * and ACP session results to be returned as A2A tasks.
5
+ */
6
+
7
+ import type { A2AMessage, A2ATask, TaskState } from '@/lib/a2a/types';
8
+ import type { AcpPromptResponse, AcpSessionUpdate } from './types';
9
+ import { createSession, prompt, closeSession } from './session';
10
+
11
+ /* ── A2A → ACP ─────────────────────────────────────────────────────────── */
12
+
13
+ /**
14
+ * Bridge an A2A SendMessage request to an ACP session/prompt.
15
+ * Creates a session, sends the message, returns the result, then closes.
16
+ */
17
+ export async function bridgeA2aToAcp(
18
+ a2aMessage: A2AMessage,
19
+ acpAgentId: string,
20
+ ): Promise<A2ATask> {
21
+ const taskId = `acp-bridge-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
22
+
23
+ // Extract text from A2A message parts
24
+ const text = a2aMessage.parts
25
+ .map(p => p.text ?? '')
26
+ .filter(Boolean)
27
+ .join('\n');
28
+
29
+ if (!text) {
30
+ return makeFailedTask(taskId, 'No text content in A2A message');
31
+ }
32
+
33
+ let session;
34
+ try {
35
+ session = await createSession(acpAgentId);
36
+ } catch (err) {
37
+ return makeFailedTask(taskId, `Failed to create ACP session: ${(err as Error).message}`);
38
+ }
39
+
40
+ try {
41
+ const response = await prompt(session.id, text);
42
+ return bridgeAcpResponseToA2a(taskId, response);
43
+ } catch (err) {
44
+ return makeFailedTask(taskId, `ACP prompt failed: ${(err as Error).message}`);
45
+ } finally {
46
+ await closeSession(session.id).catch(() => {});
47
+ }
48
+ }
49
+
50
+ /* ── ACP → A2A ─────────────────────────────────────────────────────────── */
51
+
52
+ /**
53
+ * Convert a completed ACP prompt response to an A2A task result.
54
+ */
55
+ export function bridgeAcpResponseToA2a(
56
+ taskId: string,
57
+ response: AcpPromptResponse,
58
+ ): A2ATask {
59
+ return {
60
+ id: taskId,
61
+ status: {
62
+ state: 'TASK_STATE_COMPLETED',
63
+ message: {
64
+ role: 'ROLE_AGENT',
65
+ parts: [{ text: response.text }],
66
+ },
67
+ timestamp: new Date().toISOString(),
68
+ },
69
+ artifacts: response.text ? [{
70
+ artifactId: `${taskId}-artifact`,
71
+ parts: [{ text: response.text }],
72
+ }] : undefined,
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Convert a stream of ACP session updates to A2A task updates.
78
+ * Aggregates text updates and maps final state.
79
+ */
80
+ export function bridgeAcpUpdatesToA2a(
81
+ taskId: string,
82
+ updates: AcpSessionUpdate[],
83
+ ): A2ATask {
84
+ let aggregatedText = '';
85
+ let finalState: TaskState = 'TASK_STATE_WORKING';
86
+ let errorMessage = '';
87
+
88
+ for (const update of updates) {
89
+ switch (update.type) {
90
+ case 'text':
91
+ aggregatedText += update.text ?? '';
92
+ break;
93
+ case 'done':
94
+ finalState = 'TASK_STATE_COMPLETED';
95
+ break;
96
+ case 'error':
97
+ finalState = 'TASK_STATE_FAILED';
98
+ errorMessage = update.error ?? 'Unknown error';
99
+ break;
100
+ }
101
+ }
102
+
103
+ if (finalState === 'TASK_STATE_FAILED') {
104
+ return makeFailedTask(taskId, errorMessage);
105
+ }
106
+
107
+ return {
108
+ id: taskId,
109
+ status: {
110
+ state: finalState,
111
+ message: aggregatedText ? {
112
+ role: 'ROLE_AGENT',
113
+ parts: [{ text: aggregatedText }],
114
+ } : undefined,
115
+ timestamp: new Date().toISOString(),
116
+ },
117
+ artifacts: aggregatedText ? [{
118
+ artifactId: `${taskId}-artifact`,
119
+ parts: [{ text: aggregatedText }],
120
+ }] : undefined,
121
+ };
122
+ }
123
+
124
+ /* ── Internal ──────────────────────────────────────────────────────────── */
125
+
126
+ function makeFailedTask(taskId: string, error: string): A2ATask {
127
+ return {
128
+ id: taskId,
129
+ status: {
130
+ state: 'TASK_STATE_FAILED',
131
+ message: {
132
+ role: 'ROLE_AGENT',
133
+ parts: [{ text: error }],
134
+ },
135
+ timestamp: new Date().toISOString(),
136
+ },
137
+ };
138
+ }
@@ -0,0 +1,24 @@
1
+ export { fetchAcpRegistry, getAcpAgents, findAcpAgent, clearRegistryCache } from './registry';
2
+ export { spawnAcpAgent, sendMessage, sendAndWait, onMessage, killAgent, killAllAgents, getProcess, getActiveProcesses } from './subprocess';
3
+ export { createSession, createSessionFromEntry, prompt, promptStream, cancelPrompt, closeSession, getSession, getActiveSessions, closeAllSessions } from './session';
4
+ export { bridgeA2aToAcp, bridgeAcpResponseToA2a, bridgeAcpUpdatesToA2a } from './bridge';
5
+ export { acpTools } from './acp-tools';
6
+ export { ACP_ERRORS } from './types';
7
+ export type {
8
+ AcpCapabilities,
9
+ AcpSessionState,
10
+ AcpSession,
11
+ AcpJsonRpcRequest,
12
+ AcpJsonRpcResponse,
13
+ AcpJsonRpcError,
14
+ AcpPromptRequest,
15
+ AcpPromptResponse,
16
+ AcpUpdateType,
17
+ AcpSessionUpdate,
18
+ AcpToolCall,
19
+ AcpToolResult,
20
+ AcpRegistryEntry,
21
+ AcpRegistry,
22
+ AcpTransportType,
23
+ } from './types';
24
+ export type { AcpProcess } from './subprocess';