@geminilight/mindos 0.6.23 → 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.
Files changed (66) hide show
  1. package/README.md +19 -3
  2. package/README_zh.md +19 -3
  3. package/app/app/.well-known/agent-card.json/route.ts +34 -0
  4. package/app/app/api/a2a/discover/route.ts +23 -0
  5. package/app/app/api/a2a/route.ts +100 -0
  6. package/app/components/Backlinks.tsx +2 -2
  7. package/app/components/Breadcrumb.tsx +1 -1
  8. package/app/components/CreateSpaceModal.tsx +1 -0
  9. package/app/components/CsvView.tsx +41 -19
  10. package/app/components/DirView.tsx +2 -2
  11. package/app/components/GuideCard.tsx +6 -2
  12. package/app/components/HomeContent.tsx +1 -1
  13. package/app/components/ImportModal.tsx +3 -0
  14. package/app/components/OnboardingView.tsx +1 -0
  15. package/app/components/RightAskPanel.tsx +4 -2
  16. package/app/components/SearchModal.tsx +3 -3
  17. package/app/components/SidebarLayout.tsx +11 -2
  18. package/app/components/SyncStatusBar.tsx +2 -2
  19. package/app/components/agents/DiscoverAgentModal.tsx +149 -0
  20. package/app/components/ask/AskContent.tsx +22 -10
  21. package/app/components/ask/MentionPopover.tsx +2 -2
  22. package/app/components/ask/SessionTabBar.tsx +70 -0
  23. package/app/components/ask/SlashCommandPopover.tsx +1 -1
  24. package/app/components/echo/EchoInsightCollapsible.tsx +4 -0
  25. package/app/components/explore/UseCaseCard.tsx +2 -2
  26. package/app/components/help/HelpContent.tsx +6 -1
  27. package/app/components/panels/AgentsPanel.tsx +25 -2
  28. package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
  29. package/app/components/panels/DiscoverPanel.tsx +3 -3
  30. package/app/components/panels/PanelNavRow.tsx +2 -2
  31. package/app/components/panels/PluginsPanel.tsx +1 -1
  32. package/app/components/panels/SearchPanel.tsx +3 -3
  33. package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
  34. package/app/components/renderers/workflow/WorkflowRenderer.tsx +5 -0
  35. package/app/components/settings/AiTab.tsx +5 -4
  36. package/app/components/settings/KnowledgeTab.tsx +3 -1
  37. package/app/components/settings/McpTab.tsx +22 -4
  38. package/app/components/settings/SyncTab.tsx +2 -0
  39. package/app/components/settings/UpdateTab.tsx +1 -1
  40. package/app/components/setup/StepDots.tsx +5 -1
  41. package/app/components/setup/index.tsx +9 -3
  42. package/app/components/walkthrough/WalkthroughProvider.tsx +2 -2
  43. package/app/data/skills/mindos/SKILL.md +186 -0
  44. package/app/data/skills/mindos-zh/SKILL.md +185 -0
  45. package/app/hooks/useA2aRegistry.ts +53 -0
  46. package/app/hooks/useAskSession.ts +44 -25
  47. package/app/lib/a2a/a2a-tools.ts +212 -0
  48. package/app/lib/a2a/agent-card.ts +107 -0
  49. package/app/lib/a2a/client.ts +207 -0
  50. package/app/lib/a2a/index.ts +31 -0
  51. package/app/lib/a2a/orchestrator.ts +255 -0
  52. package/app/lib/a2a/task-handler.ts +228 -0
  53. package/app/lib/a2a/types.ts +212 -0
  54. package/app/lib/agent/tools.ts +6 -4
  55. package/app/lib/i18n-en.ts +52 -0
  56. package/app/lib/i18n-zh.ts +52 -0
  57. package/app/next-env.d.ts +1 -1
  58. package/bin/cli.js +183 -164
  59. package/bin/commands/agent.js +110 -0
  60. package/bin/commands/api.js +60 -0
  61. package/bin/commands/ask.js +3 -3
  62. package/bin/commands/file.js +13 -13
  63. package/bin/commands/search.js +51 -0
  64. package/bin/commands/space.js +64 -10
  65. package/bin/lib/command.js +10 -0
  66. package/package.json +1 -1
@@ -0,0 +1,31 @@
1
+ export { buildAgentCard } from './agent-card';
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';
6
+ export { A2A_ERRORS } from './types';
7
+ export type {
8
+ AgentCard,
9
+ AgentInterface,
10
+ AgentCapabilities,
11
+ AgentSkill,
12
+ SecurityScheme,
13
+ JsonRpcRequest,
14
+ JsonRpcResponse,
15
+ JsonRpcError,
16
+ TaskState,
17
+ MessageRole,
18
+ MessagePart,
19
+ A2AMessage,
20
+ TaskStatus,
21
+ TaskArtifact,
22
+ A2ATask,
23
+ SendMessageParams,
24
+ GetTaskParams,
25
+ CancelTaskParams,
26
+ RemoteAgent,
27
+ SubTask,
28
+ ExecutionStrategy,
29
+ OrchestrationPlan,
30
+ SkillMatch,
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
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * A2A Task handler for MindOS.
3
+ * Routes A2A SendMessage requests to internal MCP tools.
4
+ */
5
+
6
+ import { randomUUID } from 'crypto';
7
+ import type {
8
+ A2ATask,
9
+ A2AMessage,
10
+ SendMessageParams,
11
+ GetTaskParams,
12
+ CancelTaskParams,
13
+ TaskState,
14
+ } from './types';
15
+
16
+ /* ── In-memory Task Store (Phase 1) ───────────────────────────────────── */
17
+ // NOTE: In-memory Map is lost on serverless cold starts / process restarts.
18
+ // Acceptable for Phase 1. Phase 2 should use persistent storage if needed.
19
+
20
+ const tasks = new Map<string, A2ATask>();
21
+ const MAX_TASKS = 1000;
22
+
23
+ function pruneOldTasks() {
24
+ if (tasks.size <= MAX_TASKS) return;
25
+ // Remove oldest completed tasks first
26
+ const entries = [...tasks.entries()].sort((a, b) =>
27
+ new Date(a[1].status.timestamp).getTime() - new Date(b[1].status.timestamp).getTime()
28
+ );
29
+ const toRemove = entries.slice(0, tasks.size - MAX_TASKS);
30
+ for (const [id] of toRemove) tasks.delete(id);
31
+ }
32
+
33
+ /* ── Skill Router ─────────────────────────────────────────────────────── */
34
+
35
+ interface SkillRoute {
36
+ pattern: RegExp;
37
+ tool: string;
38
+ extractParams: (text: string) => Record<string, string>;
39
+ }
40
+
41
+ const SKILL_ROUTES: SkillRoute[] = [
42
+ {
43
+ pattern: /^(?:search|find|look\s*up|query)\b/i,
44
+ tool: 'search_notes',
45
+ extractParams: (text) => ({ q: text.replace(/^(?:search|find|look\s*up|query)\s+(?:for\s+)?/i, '').trim() }),
46
+ },
47
+ {
48
+ pattern: /^(?:read|get|show|open|view)\s+(?:the\s+)?(?:file\s+)?(?:at\s+)?(.+\.(?:md|csv))/i,
49
+ tool: 'read_file',
50
+ extractParams: (text) => {
51
+ const match = text.match(/(?:at\s+)?([^\s]+\.(?:md|csv))/i);
52
+ return { path: match?.[1] ?? '' };
53
+ },
54
+ },
55
+ {
56
+ pattern: /^(?:list|show|tree)\s+(?:files|spaces|structure)/i,
57
+ tool: 'list_files',
58
+ extractParams: () => ({}),
59
+ },
60
+ {
61
+ pattern: /^(?:list|show)\s+spaces/i,
62
+ tool: 'list_spaces',
63
+ extractParams: () => ({}),
64
+ },
65
+ ];
66
+
67
+ function routeToTool(text: string): { tool: string; params: Record<string, string> } | null {
68
+ for (const route of SKILL_ROUTES) {
69
+ if (route.pattern.test(text)) {
70
+ return { tool: route.tool, params: route.extractParams(text) };
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+
76
+ /* ── Execute via internal API ─────────────────────────────────────────── */
77
+
78
+ const TOOL_TIMEOUT_MS = 10_000;
79
+
80
+ async function fetchWithTimeout(url: string): Promise<Response> {
81
+ const controller = new AbortController();
82
+ const timeout = setTimeout(() => controller.abort(), TOOL_TIMEOUT_MS);
83
+ try {
84
+ const res = await fetch(url, { signal: controller.signal });
85
+ return res;
86
+ } finally {
87
+ clearTimeout(timeout);
88
+ }
89
+ }
90
+
91
+ /** Sanitize file path: reject traversal attempts */
92
+ function sanitizePath(p: string): string {
93
+ if (!p || p.includes('..') || p.includes('\0')) throw new Error('Invalid path');
94
+ // Normalize double slashes and strip leading slashes
95
+ return p.replace(/\/\//g, '/').replace(/^\/+/, '');
96
+ }
97
+
98
+ async function executeTool(tool: string, params: Record<string, string>): Promise<string> {
99
+ const baseUrl = `http://localhost:${process.env.PORT || 3456}`;
100
+
101
+ switch (tool) {
102
+ case 'search_notes': {
103
+ const q = (params.q || '').slice(0, 500); // limit query length
104
+ const res = await fetchWithTimeout(`${baseUrl}/api/search?q=${encodeURIComponent(q)}`);
105
+ if (!res.ok) throw new Error(`Search failed: ${res.status}`);
106
+ const data = await res.json();
107
+ return JSON.stringify(data, null, 2);
108
+ }
109
+ case 'read_file': {
110
+ const safePath = sanitizePath(params.path || '');
111
+ const res = await fetchWithTimeout(`${baseUrl}/api/file?path=${encodeURIComponent(safePath)}`);
112
+ if (!res.ok) throw new Error(`Read failed: ${res.status}`);
113
+ const data = await res.json();
114
+ return typeof data.content === 'string' ? data.content : JSON.stringify(data);
115
+ }
116
+ case 'list_files': {
117
+ const res = await fetchWithTimeout(`${baseUrl}/api/files`);
118
+ if (!res.ok) throw new Error(`List failed: ${res.status}`);
119
+ const data = await res.json();
120
+ return JSON.stringify(data, null, 2);
121
+ }
122
+ case 'list_spaces': {
123
+ const res = await fetchWithTimeout(`${baseUrl}/api/files`);
124
+ if (!res.ok) throw new Error(`List failed: ${res.status}`);
125
+ const data = await res.json();
126
+ const spaces = (data.tree ?? data.files ?? []).filter((n: { isSpace?: boolean }) => n.isSpace);
127
+ return JSON.stringify(spaces, null, 2);
128
+ }
129
+ default:
130
+ throw new Error(`Unknown tool: ${tool}`);
131
+ }
132
+ }
133
+
134
+ /* ── Public API ────────────────────────────────────────────────────────── */
135
+
136
+ export async function handleSendMessage(params: SendMessageParams): Promise<A2ATask> {
137
+ const taskId = randomUUID();
138
+ const now = new Date().toISOString();
139
+
140
+ // Extract text from message parts
141
+ const text = params.message.parts
142
+ .map(p => p.text ?? (p.data ? JSON.stringify(p.data) : ''))
143
+ .join(' ')
144
+ .trim();
145
+
146
+ if (!text) {
147
+ const failedTask = createTask(taskId, 'TASK_STATE_FAILED', 'Empty message — no text content found.', now);
148
+ tasks.set(taskId, failedTask);
149
+ return failedTask;
150
+ }
151
+
152
+ // Create task in WORKING state
153
+ const task = createTask(taskId, 'TASK_STATE_WORKING', undefined, now);
154
+ task.history = [params.message];
155
+ tasks.set(taskId, task);
156
+ pruneOldTasks();
157
+
158
+ // Route to tool
159
+ const route = routeToTool(text);
160
+
161
+ try {
162
+ let result: string;
163
+ if (route) {
164
+ result = await executeTool(route.tool, route.params);
165
+ } else {
166
+ // Fallback: treat as search query
167
+ result = await executeTool('search_notes', { q: text });
168
+ }
169
+
170
+ // Update task to completed
171
+ task.status = {
172
+ state: 'TASK_STATE_COMPLETED',
173
+ timestamp: new Date().toISOString(),
174
+ };
175
+ task.artifacts = [{
176
+ artifactId: randomUUID(),
177
+ name: 'result',
178
+ parts: [{ text: result, mediaType: 'text/plain' }],
179
+ }];
180
+ task.history.push({
181
+ role: 'ROLE_AGENT',
182
+ parts: [{ text: result }],
183
+ });
184
+
185
+ return task;
186
+ } catch (err) {
187
+ task.status = {
188
+ state: 'TASK_STATE_FAILED',
189
+ message: {
190
+ role: 'ROLE_AGENT',
191
+ parts: [{ text: `Error: ${(err as Error).message}` }],
192
+ },
193
+ timestamp: new Date().toISOString(),
194
+ };
195
+ return task;
196
+ }
197
+ }
198
+
199
+ export function handleGetTask(params: GetTaskParams): A2ATask | null {
200
+ return tasks.get(params.id) ?? null;
201
+ }
202
+
203
+ export function handleCancelTask(params: CancelTaskParams): A2ATask | null {
204
+ const task = tasks.get(params.id);
205
+ if (!task) return null;
206
+
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
209
+
210
+ task.status = {
211
+ state: 'TASK_STATE_CANCELED',
212
+ timestamp: new Date().toISOString(),
213
+ };
214
+ return task;
215
+ }
216
+
217
+ /* ── Helpers ───────────────────────────────────────────────────────────── */
218
+
219
+ function createTask(id: string, state: TaskState, errorMessage: string | undefined, timestamp: string): A2ATask {
220
+ return {
221
+ id,
222
+ status: {
223
+ state,
224
+ timestamp,
225
+ ...(errorMessage ? { message: { role: 'ROLE_AGENT', parts: [{ text: errorMessage }] } } : {}),
226
+ },
227
+ };
228
+ }