@a2anet/a2a-utils 0.4.0 → 0.6.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.
@@ -1,14 +1,89 @@
1
1
  // SPDX-FileCopyrightText: 2025-present A2A Net <hello@a2anet.com>
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
+ import { z } from "zod";
4
5
  import { DataArtifacts, TextArtifacts, minimizeArtifacts } from "../artifacts/index.js";
5
6
  import { ArtifactSettings } from "../types.js";
6
7
  export const TEXT_MINIMIZED_TIP = "Text was minimized. Call view_text_artifact() to view specific line ranges.";
7
8
  export const DATA_MINIMIZED_TIP = "Data was minimized. Call view_data_artifact() to navigate to specific data.";
9
+ // -- Zod schemas (one per tool) --
10
+ const getAgentsSchema = z.object({});
11
+ const getAgentSchema = z.object({
12
+ agentId: z.string().describe("The agent's unique identifier (from get_agents)."),
13
+ });
14
+ const sendMessageSchema = z.object({
15
+ agentId: z.string().describe("ID of the agent to message (from get_agents)."),
16
+ message: z.string().describe("The message content to send."),
17
+ contextId: z
18
+ .string()
19
+ .describe("Continue an existing conversation by providing its context ID.")
20
+ .optional(),
21
+ taskId: z
22
+ .string()
23
+ .describe("Attach to an existing task (for input_required flows).")
24
+ .optional(),
25
+ timeout: z.number().describe("Override the default timeout in seconds.").optional(),
26
+ data: z
27
+ .array(z.unknown())
28
+ .describe("Structured data to include with the message. " +
29
+ "Each item is sent as a separate JSON object alongside the text.")
30
+ .optional(),
31
+ files: z
32
+ .array(z.string())
33
+ .describe("Files to include with the message. " +
34
+ "Accepts local file paths (read and sent as binary, max 1MB) " +
35
+ "or URLs (sent as references for the remote agent to fetch).")
36
+ .optional(),
37
+ });
38
+ const getTaskSchema = z.object({
39
+ agentId: z.string().describe("ID of the agent that owns the task."),
40
+ taskId: z.string().describe("Task ID from a previous send_message response."),
41
+ timeout: z.number().describe("Override the monitoring timeout in seconds.").optional(),
42
+ pollInterval: z
43
+ .number()
44
+ .describe("Override the interval between status checks in seconds.")
45
+ .optional(),
46
+ });
47
+ const viewTextArtifactSchema = z.object({
48
+ agentId: z.string().describe("ID of the agent that produced the artifact."),
49
+ taskId: z.string().describe("Task ID containing the artifact."),
50
+ artifactId: z
51
+ .string()
52
+ .describe("The artifact's unique identifier (from the task's artifacts list)."),
53
+ lineStart: z.number().describe("Starting line number (1-based, inclusive).").optional(),
54
+ lineEnd: z.number().describe("Ending line number (1-based, inclusive).").optional(),
55
+ characterStart: z
56
+ .number()
57
+ .describe("Starting character index (0-based, inclusive).")
58
+ .optional(),
59
+ characterEnd: z.number().describe("Ending character index (0-based, exclusive).").optional(),
60
+ });
61
+ const viewDataArtifactSchema = z.object({
62
+ agentId: z.string().describe("ID of the agent that produced the artifact."),
63
+ taskId: z.string().describe("Task ID containing the artifact."),
64
+ artifactId: z
65
+ .string()
66
+ .describe("The artifact's unique identifier (from the task's artifacts list)."),
67
+ jsonPath: z
68
+ .string()
69
+ .describe('Dot-separated path to navigate into the data (e.g. "results.items").')
70
+ .optional(),
71
+ rows: z
72
+ .string()
73
+ .describe('Row selection for list data. Examples: "0" (single), "0-10" (range), "0,2,5" (specific), "all".')
74
+ .optional(),
75
+ columns: z
76
+ .string()
77
+ .describe('Column selection for tabular data. Examples: "name" (single), "name,age" (multiple), "all".')
78
+ .optional(),
79
+ });
8
80
  /**
9
81
  * LLM-friendly tools that can be used out-of-the-box with agent frameworks.
10
82
  *
11
- * Each method has LLM-friendly docstrings, returns JSON-serialisable objects, and returns actionable error messages.
83
+ * Each tool has LLM-friendly docstrings, returns JSON-serialisable objects, and returns actionable error messages.
84
+ *
85
+ * Each tool is an instance property with `name`, `description`, `schema` (Zod),
86
+ * and `execute`. Use the `toolDefinitions` getter for the full array.
12
87
  */
13
88
  export class A2ATools {
14
89
  session;
@@ -17,264 +92,244 @@ export class A2ATools {
17
92
  this.session = session;
18
93
  this.artifactSettings = opts?.artifactSettings ?? new ArtifactSettings();
19
94
  }
20
- /**
21
- * List all available agents with their names and descriptions.
22
- *
23
- * Use this first to discover what agents are available before sending messages.
24
- * Each agent has a unique ID (the key) that you'll need for other tools like
25
- * send_message and get_agent.
26
- *
27
- * Returns an object mapping agent IDs to their name and description.
28
- * If any agents failed to load, an "errors" field is included with details.
29
- */
30
- async getAgents() {
31
- try {
32
- const result = await this.session.agents.getAgentsForLlm("basic");
33
- const initErrors = this.session.agents.initializationErrors;
34
- if (Object.keys(result).length === 0 && Object.keys(initErrors).length > 0) {
35
- const errors = {};
36
- for (const [agentId, error] of Object.entries(initErrors)) {
37
- errors[agentId] = `Failed to load agent: ${error}`;
95
+ // -- Tool definitions --
96
+ getAgents = {
97
+ name: "get_agents",
98
+ description: "List all available agents with their names and descriptions. " +
99
+ "Use this first to discover what agents are available before sending messages. " +
100
+ "Each agent has a unique ID (the key) that you'll need for other tools like " +
101
+ "send_message and get_agent. " +
102
+ "Returns an object mapping agent IDs to their name and description. " +
103
+ 'If any agents failed to load, an "errors" field is included with details.',
104
+ schema: getAgentsSchema,
105
+ execute: async () => {
106
+ try {
107
+ const result = await this.session.agents.getAgentsForLlm("basic");
108
+ const initErrors = this.session.agents.initializationErrors;
109
+ if (Object.keys(result).length === 0 && Object.keys(initErrors).length > 0) {
110
+ const errors = {};
111
+ for (const [agentId, error] of Object.entries(initErrors)) {
112
+ errors[agentId] = `Failed to load agent: ${error}`;
113
+ }
114
+ return { agents: result, errors };
38
115
  }
39
- return { agents: result, errors };
116
+ return result;
40
117
  }
41
- return result;
42
- }
43
- catch (e) {
44
- return { error: true, error_message: `Failed to list agents: ${e}` };
45
- }
46
- }
47
- /**
48
- * Get detailed information about a specific agent, including its skills.
49
- *
50
- * Use this after get_agents to learn more about what a specific agent can do.
51
- * The response includes the agent's name, description, and a list of skills
52
- * with their descriptions.
53
- *
54
- * @param agentId - The agent's unique identifier (from get_agents).
55
- */
56
- async getAgent(agentId) {
57
- try {
58
- const result = await this.session.agents.getAgentForLlm(agentId, "full");
59
- if (result === null) {
60
- const available = Object.keys(await this.session.agents.getAgents()).sort();
61
- return {
62
- error: true,
63
- error_message: `Agent '${agentId}' not found. Use get_agents to see available agents. Available: ${available.length > 0 ? available.join(", ") : "(none)"}`,
64
- };
118
+ catch (e) {
119
+ return { error: true, error_message: `Failed to list agents: ${e}` };
65
120
  }
66
- return result;
67
- }
68
- catch (e) {
69
- return { error: true, error_message: `Failed to get agent info: ${e}` };
70
- }
71
- }
72
- /**
73
- * Send a message to an agent and receive a structured response.
74
- *
75
- * This is the primary way to communicate with agents. The response includes
76
- * the agent's reply and any generated artifacts.
77
- *
78
- * Artifact data in responses may be minimized for display. Fields prefixed
79
- * with "_" indicate metadata about minimized content. Use view_text_artifact
80
- * or view_data_artifact to access full artifact data.
81
- *
82
- * If the task is still in progress after the timeout, the response includes
83
- * a task_id. Use get_task with that task_id to continue monitoring.
84
- *
85
- * @param agentId - ID of the agent to message (from get_agents).
86
- * @param message - The message content to send.
87
- * @param contextId - Continue an existing conversation by providing its context ID.
88
- * Omit to start a new conversation.
89
- * @param taskId - Attach to an existing task (for input_required flows).
90
- * @param timeout - Override the default timeout in seconds.
91
- */
92
- async sendMessage(agentId, message, contextId, taskId, timeout) {
93
- try {
94
- const result = await this.session.sendMessage(agentId, message, {
95
- contextId,
96
- taskId,
97
- timeout,
98
- });
99
- let llmResult;
100
- if (result.kind === "task") {
101
- llmResult = await this.buildTaskForLlm(result);
121
+ },
122
+ };
123
+ getAgent = {
124
+ name: "get_agent",
125
+ description: "Get detailed information about a specific agent, including its skills. " +
126
+ "Use this after get_agents to learn more about what a specific agent can do. " +
127
+ "The response includes the agent's name, description, and a list of skills " +
128
+ "with their descriptions.",
129
+ schema: getAgentSchema,
130
+ execute: async ({ agentId }) => {
131
+ try {
132
+ const result = await this.session.agents.getAgentForLlm(agentId, "full");
133
+ if (result === null) {
134
+ const available = Object.keys(await this.session.agents.getAgents()).sort();
135
+ return {
136
+ error: true,
137
+ error_message: `Agent '${agentId}' not found. Use get_agents to see available agents. Available: ${available.length > 0 ? available.join(", ") : "(none)"}`,
138
+ };
139
+ }
140
+ return result;
102
141
  }
103
- else {
104
- llmResult = this.buildMessageForLlm(result);
142
+ catch (e) {
143
+ return { error: true, error_message: `Failed to get agent info: ${e}` };
105
144
  }
106
- return llmResult;
107
- }
108
- catch (e) {
109
- const errorMsg = String(e);
110
- if (errorMsg.toLowerCase().includes("not found")) {
111
- return {
112
- error: true,
113
- error_message: `${errorMsg} Use get_agents to see available agents.`,
114
- };
145
+ },
146
+ };
147
+ sendMessage = {
148
+ name: "send_message",
149
+ description: "Send a message to an agent and receive a structured response. " +
150
+ "This is the primary way to communicate with agents. The response includes " +
151
+ "the agent's reply and any generated artifacts. " +
152
+ 'Artifact data in responses may be minimized for display. Fields prefixed with "_" ' +
153
+ "indicate metadata about minimized content. Use view_text_artifact " +
154
+ "or view_data_artifact to access full artifact data. " +
155
+ "If the task is still in progress after the timeout, the response includes " +
156
+ "a task_id. Use get_task with that task_id to continue monitoring.",
157
+ schema: sendMessageSchema,
158
+ execute: async ({ agentId, message, contextId, taskId, timeout, data, files, }) => {
159
+ try {
160
+ const result = await this.session.sendMessage(agentId, message, {
161
+ contextId: contextId ?? null,
162
+ taskId: taskId ?? null,
163
+ timeout: timeout ?? null,
164
+ data: data,
165
+ files,
166
+ });
167
+ let llmResult;
168
+ if (result.kind === "task") {
169
+ llmResult = await this.buildTaskForLlm(result);
170
+ }
171
+ else {
172
+ llmResult = await this.buildMessageForLlm(result);
173
+ }
174
+ return llmResult;
115
175
  }
116
- if (e instanceof DOMException && e.name === "TimeoutError") {
117
- return {
118
- error: true,
119
- error_message: "Request timed out. You can retry with a longer timeout, " +
120
- "or if a task_id was returned earlier, use get_task to check progress.",
121
- };
176
+ catch (e) {
177
+ const errorMsg = String(e);
178
+ if (errorMsg.toLowerCase().includes("not found")) {
179
+ return {
180
+ error: true,
181
+ error_message: `${errorMsg} Use get_agents to see available agents.`,
182
+ };
183
+ }
184
+ if (e instanceof DOMException && e.name === "TimeoutError") {
185
+ return {
186
+ error: true,
187
+ error_message: "Request timed out. You can retry with a longer timeout, " +
188
+ "or if a task_id was returned earlier, use get_task to check progress.",
189
+ };
190
+ }
191
+ return { error: true, error_message: `Failed to send message: ${e}` };
122
192
  }
123
- return { error: true, error_message: `Failed to send message: ${e}` };
124
- }
125
- }
126
- /**
127
- * Check the progress of a task that is still in progress.
128
- *
129
- * Use this after send_message returns a task in a non-terminal state
130
- * (e.g. "working") to monitor its progress.
131
- *
132
- * If the task is still running after the timeout, the current state is
133
- * returned. Call get_task again to continue monitoring.
134
- *
135
- * @param agentId - ID of the agent that owns the task.
136
- * @param taskId - Task ID from a previous send_message response.
137
- * @param timeout - Override the monitoring timeout in seconds.
138
- * @param pollInterval - Override the interval between status checks in seconds.
139
- */
140
- async getTask(agentId, taskId, timeout, pollInterval) {
141
- try {
142
- const result = await this.session.getTask(agentId, taskId, {
143
- timeout,
144
- pollInterval,
145
- });
146
- const llmResult = await this.buildTaskForLlm(result);
147
- return llmResult;
148
- }
149
- catch (e) {
150
- const errorMsg = String(e);
151
- if (errorMsg.toLowerCase().includes("not found")) {
152
- return {
153
- error: true,
154
- error_message: `${errorMsg} Use get_agents to see available agents.`,
155
- };
193
+ },
194
+ };
195
+ getTask = {
196
+ name: "get_task",
197
+ description: "Check the progress of a task that is still in progress. " +
198
+ 'Use this after send_message returns a task in a non-terminal state (e.g. "working") ' +
199
+ "to monitor its progress. " +
200
+ "If the task is still running after the timeout, the current state is returned. " +
201
+ "Call get_task again to continue monitoring.",
202
+ schema: getTaskSchema,
203
+ execute: async ({ agentId, taskId, timeout, pollInterval, }) => {
204
+ try {
205
+ const result = await this.session.getTask(agentId, taskId, {
206
+ timeout,
207
+ pollInterval,
208
+ });
209
+ const llmResult = await this.buildTaskForLlm(result);
210
+ return llmResult;
156
211
  }
157
- if (e instanceof DOMException && e.name === "TimeoutError") {
158
- return {
159
- error: true,
160
- error_message: "Request timed out. You can retry with a longer timeout, " +
161
- "or use get_task again to continue monitoring.",
212
+ catch (e) {
213
+ const errorMsg = String(e);
214
+ if (errorMsg.toLowerCase().includes("not found")) {
215
+ return {
216
+ error: true,
217
+ error_message: `${errorMsg} Use get_agents to see available agents.`,
218
+ };
219
+ }
220
+ if (e instanceof DOMException && e.name === "TimeoutError") {
221
+ return {
222
+ error: true,
223
+ error_message: "Request timed out. You can retry with a longer timeout, " +
224
+ "or use get_task again to continue monitoring.",
225
+ };
226
+ }
227
+ return { error: true, error_message: `Failed to get task: ${e}` };
228
+ }
229
+ },
230
+ };
231
+ viewTextArtifact = {
232
+ name: "view_text_artifact",
233
+ description: "View text content from an artifact, optionally selecting a range. " +
234
+ "Use this for artifacts containing text (documents, logs, code, etc.). " +
235
+ "You can select by line range OR character range, but not both.",
236
+ schema: viewTextArtifactSchema,
237
+ execute: async ({ agentId, taskId, artifactId, lineStart, lineEnd, characterStart, characterEnd, }) => {
238
+ try {
239
+ const artifact = await this.getArtifact(agentId, taskId, artifactId);
240
+ const text = A2ATools.extractText(artifact);
241
+ const filtered = TextArtifacts.view(text, {
242
+ lineStart,
243
+ lineEnd,
244
+ characterStart,
245
+ characterEnd,
246
+ characterLimit: this.artifactSettings.viewArtifactCharacterLimit,
247
+ });
248
+ const result = {
249
+ artifactId: artifact.artifactId,
250
+ description: artifact.description ?? null,
251
+ name: artifact.name ?? null,
252
+ parts: [{ kind: "text", text: filtered }],
162
253
  };
254
+ return result;
163
255
  }
164
- return { error: true, error_message: `Failed to get task: ${e}` };
165
- }
166
- }
167
- /**
168
- * View text content from an artifact, optionally selecting a range.
169
- *
170
- * Use this for artifacts containing text (documents, logs, code, etc.).
171
- * You can select by line range OR character range, but not both.
172
- *
173
- * @param agentId - ID of the agent that produced the artifact.
174
- * @param taskId - Task ID containing the artifact.
175
- * @param artifactId - The artifact's unique identifier (from the task's artifacts list).
176
- * @param lineStart - Starting line number (1-based, inclusive).
177
- * @param lineEnd - Ending line number (1-based, inclusive).
178
- * @param characterStart - Starting character index (0-based, inclusive).
179
- * @param characterEnd - Ending character index (0-based, exclusive).
180
- */
181
- async viewTextArtifact(agentId, taskId, artifactId, lineStart, lineEnd, characterStart, characterEnd) {
182
- try {
183
- const artifact = await this.getArtifact(agentId, taskId, artifactId);
184
- const text = A2ATools.extractText(artifact);
185
- const filtered = TextArtifacts.view(text, {
186
- lineStart,
187
- lineEnd,
188
- characterStart,
189
- characterEnd,
190
- characterLimit: this.artifactSettings.viewArtifactCharacterLimit,
191
- });
192
- const result = {
193
- artifactId: artifact.artifactId,
194
- description: artifact.description ?? null,
195
- name: artifact.name ?? null,
196
- parts: [{ kind: "text", text: filtered }],
197
- };
198
- return result;
199
- }
200
- catch (e) {
201
- const errorMsg = String(e);
202
- if (errorMsg.toLowerCase().includes("not found")) {
203
- if (errorMsg.toLowerCase().includes("artifact")) {
256
+ catch (e) {
257
+ const errorMsg = String(e);
258
+ if (errorMsg.toLowerCase().includes("not found")) {
259
+ if (errorMsg.toLowerCase().includes("artifact")) {
260
+ return {
261
+ error: true,
262
+ error_message: `Artifact '${artifactId}' not found in task '${taskId}'. Check the task's artifacts list for valid artifact IDs.`,
263
+ };
264
+ }
204
265
  return {
205
266
  error: true,
206
- error_message: `Artifact '${artifactId}' not found in task '${taskId}'. Check the task's artifacts list for valid artifact IDs.`,
267
+ error_message: `${errorMsg} Use get_agents to see available agents.`,
207
268
  };
208
269
  }
209
- return {
210
- error: true,
211
- error_message: `${errorMsg} Use get_agents to see available agents.`,
270
+ return { error: true, error_message: `Failed to view text artifact: ${e}` };
271
+ }
272
+ },
273
+ };
274
+ viewDataArtifact = {
275
+ name: "view_data_artifact",
276
+ description: "View structured data from an artifact with optional filtering. " +
277
+ "Use this for artifacts containing JSON data (objects, arrays, tables). " +
278
+ "You can navigate to specific data with json_path, then filter with " +
279
+ "rows and columns for tabular data.",
280
+ schema: viewDataArtifactSchema,
281
+ execute: async ({ agentId, taskId, artifactId, jsonPath, rows, columns, }) => {
282
+ try {
283
+ const parsedRows = A2ATools.parseRows(rows ?? null);
284
+ const parsedColumns = A2ATools.parseColumns(columns ?? null);
285
+ const artifact = await this.getArtifact(agentId, taskId, artifactId);
286
+ const data = A2ATools.extractData(artifact);
287
+ const filtered = DataArtifacts.view(data, {
288
+ jsonPath,
289
+ rows: parsedRows,
290
+ columns: parsedColumns,
291
+ characterLimit: this.artifactSettings.viewArtifactCharacterLimit,
292
+ });
293
+ const result = {
294
+ artifactId: artifact.artifactId,
295
+ description: artifact.description ?? null,
296
+ name: artifact.name ?? null,
297
+ parts: [{ kind: "data", data: filtered }],
212
298
  };
299
+ return result;
213
300
  }
214
- return { error: true, error_message: `Failed to view text artifact: ${e}` };
215
- }
216
- }
217
- /**
218
- * View structured data from an artifact with optional filtering.
219
- *
220
- * Use this for artifacts containing JSON data (objects, arrays, tables).
221
- * You can navigate to specific data with json_path, then filter with
222
- * rows and columns for tabular data.
223
- *
224
- * @param agentId - ID of the agent that produced the artifact.
225
- * @param taskId - Task ID containing the artifact.
226
- * @param artifactId - The artifact's unique identifier (from the task's artifacts list).
227
- * @param jsonPath - Dot-separated path to navigate into the data (e.g. "results.items").
228
- * @param rows - Row selection for list data. Examples: "0" (single row), "0-10" (range),
229
- * "0,2,5" (specific rows), "all" (every row).
230
- * @param columns - Column selection for tabular data (list of objects). Examples:
231
- * "name" (single column), "name,age" (multiple columns), "all" (every column).
232
- */
233
- async viewDataArtifact(agentId, taskId, artifactId, jsonPath, rows, columns) {
234
- try {
235
- const parsedRows = A2ATools.parseRows(rows ?? null);
236
- const parsedColumns = A2ATools.parseColumns(columns ?? null);
237
- const artifact = await this.getArtifact(agentId, taskId, artifactId);
238
- const data = A2ATools.extractData(artifact);
239
- const filtered = DataArtifacts.view(data, {
240
- jsonPath,
241
- rows: parsedRows,
242
- columns: parsedColumns,
243
- characterLimit: this.artifactSettings.viewArtifactCharacterLimit,
244
- });
245
- const result = {
246
- artifactId: artifact.artifactId,
247
- description: artifact.description ?? null,
248
- name: artifact.name ?? null,
249
- parts: [{ kind: "data", data: filtered }],
250
- };
251
- return result;
252
- }
253
- catch (e) {
254
- const errorMsg = String(e);
255
- if (errorMsg.toLowerCase().includes("not found")) {
256
- if (errorMsg.toLowerCase().includes("artifact")) {
301
+ catch (e) {
302
+ const errorMsg = String(e);
303
+ if (errorMsg.toLowerCase().includes("not found")) {
304
+ if (errorMsg.toLowerCase().includes("artifact")) {
305
+ return {
306
+ error: true,
307
+ error_message: `Artifact '${artifactId}' not found in task '${taskId}'. Check the task's artifacts list for valid artifact IDs.`,
308
+ };
309
+ }
257
310
  return {
258
311
  error: true,
259
- error_message: `Artifact '${artifactId}' not found in task '${taskId}'. Check the task's artifacts list for valid artifact IDs.`,
312
+ error_message: `${errorMsg} Use get_agents to see available agents.`,
260
313
  };
261
314
  }
262
- return {
263
- error: true,
264
- error_message: `${errorMsg} Use get_agents to see available agents.`,
265
- };
315
+ return { error: true, error_message: `Failed to view data artifact: ${e}` };
266
316
  }
267
- return { error: true, error_message: `Failed to view data artifact: ${e}` };
268
- }
317
+ },
318
+ };
319
+ /** All tool definitions as an array. */
320
+ get toolDefinitions() {
321
+ return [
322
+ this.getAgents,
323
+ this.getAgent,
324
+ this.sendMessage,
325
+ this.getTask,
326
+ this.viewTextArtifact,
327
+ this.viewDataArtifact,
328
+ ];
269
329
  }
270
- // -- LLM conversion methods --
271
- /**
272
- * Convert an A2A Message to MessageForLLM.
273
- *
274
- * Combines all TextParts into a single TextPartForLLM.
275
- * FileParts are ignored; file handling is done at the artifact level.
276
- */
277
- buildMessageForLlm(message) {
330
+ // -- Private helpers --
331
+ /** Convert an A2A Message to MessageForLLM. */
332
+ async buildMessageForLlm(message) {
278
333
  const parts = [];
279
334
  // Combine all text parts
280
335
  const textSegments = [];
@@ -292,6 +347,17 @@ export class A2ATools {
292
347
  parts.push({ kind: "data", data: part.data });
293
348
  }
294
349
  }
350
+ // Handle file parts
351
+ let savedPaths = [];
352
+ if (this.session.fileStore !== null) {
353
+ savedPaths = await this.session.fileStore.getMessage(message.messageId);
354
+ }
355
+ const hasSavedPaths = savedPaths.length > 0;
356
+ for (const part of message.parts) {
357
+ if (part.kind === "file") {
358
+ parts.push(A2ATools.buildFilePartForLlm(part, hasSavedPaths ? savedPaths : null));
359
+ }
360
+ }
295
361
  return {
296
362
  contextId: message.contextId ?? null,
297
363
  kind: "message",
@@ -300,12 +366,11 @@ export class A2ATools {
300
366
  }
301
367
  /** Convert a Task to TaskForLLM with artifact minimization and file path queries. */
302
368
  async buildTaskForLlm(task) {
303
- // Query fileStore for saved file paths
304
369
  let savedFilePaths = null;
305
370
  if (this.session.fileStore !== null && task.artifacts) {
306
371
  savedFilePaths = {};
307
372
  for (const artifact of task.artifacts) {
308
- const paths = await this.session.fileStore.get(task.id, artifact.artifactId);
373
+ const paths = await this.session.fileStore.getArtifact(task.id, artifact.artifactId);
309
374
  if (paths.length > 0) {
310
375
  savedFilePaths[artifact.artifactId] = paths;
311
376
  }
@@ -320,10 +385,9 @@ export class A2ATools {
320
385
  dataTip: DATA_MINIMIZED_TIP,
321
386
  })
322
387
  : [];
323
- // Build status message
324
388
  let statusMessage = null;
325
389
  if (task.status.message) {
326
- statusMessage = this.buildMessageForLlm(task.status.message);
390
+ statusMessage = await this.buildMessageForLlm(task.status.message);
327
391
  }
328
392
  return {
329
393
  id: task.id,
@@ -336,18 +400,8 @@ export class A2ATools {
336
400
  artifacts: minimized,
337
401
  };
338
402
  }
339
- /**
340
- * Look up an artifact through the resolution chain.
341
- *
342
- * 1. Check the task store (local cache)
343
- * 2. Fetch fresh via session.getTask (remote retrieval)
344
- *
345
- * @returns The Artifact.
346
- *
347
- * @throws Error if artifact cannot be found.
348
- */
403
+ /** Look up an artifact through the resolution chain. */
349
404
  async getArtifact(agentId, taskId, artifactId) {
350
- // 1. Check task store (local cache)
351
405
  const cachedTask = await this.session.taskStore.load(taskId);
352
406
  if (cachedTask?.artifacts) {
353
407
  for (const artifact of cachedTask.artifacts) {
@@ -356,7 +410,6 @@ export class A2ATools {
356
410
  }
357
411
  }
358
412
  }
359
- // 2. Fetch fresh via session.getTask
360
413
  const task = await this.session.getTask(agentId, taskId);
361
414
  if (task.artifacts) {
362
415
  for (const artifact of task.artifacts) {
@@ -367,11 +420,6 @@ export class A2ATools {
367
420
  }
368
421
  throw new Error(`Artifact '${artifactId}' not found in task '${taskId}'. The artifact may have expired or the task_id may be incorrect.`);
369
422
  }
370
- /**
371
- * Extract text content from artifact parts.
372
- *
373
- * @throws Error if artifact does not contain text content.
374
- */
375
423
  static extractText(artifact) {
376
424
  const textParts = [];
377
425
  for (const part of artifact.parts) {
@@ -386,11 +434,6 @@ export class A2ATools {
386
434
  }
387
435
  return textParts.join("\n");
388
436
  }
389
- /**
390
- * Extract data content from artifact parts.
391
- *
392
- * @throws Error if artifact does not contain data content.
393
- */
394
437
  static extractData(artifact) {
395
438
  const dataParts = [];
396
439
  for (const part of artifact.parts) {
@@ -405,12 +448,6 @@ export class A2ATools {
405
448
  }
406
449
  return dataParts.length === 1 ? dataParts[0] : dataParts;
407
450
  }
408
- /**
409
- * Parse a rows string into the type expected by DataArtifacts.view.
410
- *
411
- * Accepts: "0" (single int), "0-10" (range string), "0,2,5" (comma-separated
412
- * list of ints), "all" (passthrough string), or null.
413
- */
414
451
  static parseRows(rows) {
415
452
  if (rows === null) {
416
453
  return null;
@@ -419,7 +456,6 @@ export class A2ATools {
419
456
  if (trimmedRows === "all") {
420
457
  return "all";
421
458
  }
422
- // Comma-separated list: "0,2,5"
423
459
  if (trimmedRows.includes(",")) {
424
460
  try {
425
461
  return trimmedRows.split(",").map((x) => {
@@ -434,23 +470,15 @@ export class A2ATools {
434
470
  throw new Error(`Invalid rows format: '${trimmedRows}'. Comma-separated values must be integers. Examples: '0', '0-10', '0,2,5', 'all'.`);
435
471
  }
436
472
  }
437
- // Range string: "0-10"
438
473
  if (trimmedRows.includes("-")) {
439
474
  return trimmedRows;
440
475
  }
441
- // Single integer: "0"
442
476
  const n = Number.parseInt(trimmedRows, 10);
443
477
  if (Number.isNaN(n)) {
444
478
  throw new Error(`Invalid rows format: '${trimmedRows}'. Examples: '0' (single row), '0-10' (range), '0,2,5' (specific rows), 'all'.`);
445
479
  }
446
480
  return n;
447
481
  }
448
- /**
449
- * Parse a columns string into the type expected by DataArtifacts.view.
450
- *
451
- * Accepts: "name" (single column), "name,age" (comma-separated list),
452
- * "all" (passthrough string), or null.
453
- */
454
482
  static parseColumns(columns) {
455
483
  if (columns === null) {
456
484
  return null;
@@ -459,12 +487,55 @@ export class A2ATools {
459
487
  if (trimmedColumns === "all") {
460
488
  return "all";
461
489
  }
462
- // Comma-separated list: "name,age"
463
490
  if (trimmedColumns.includes(",")) {
464
491
  return trimmedColumns.split(",").map((c) => c.trim());
465
492
  }
466
- // Single column name
467
493
  return trimmedColumns;
468
494
  }
495
+ static buildFilePartForLlm(part, savedPaths) {
496
+ if (part.kind !== "file") {
497
+ throw new Error("Expected file part");
498
+ }
499
+ const fileObj = part.file;
500
+ const name = fileObj.name ?? null;
501
+ const mimeType = fileObj.mimeType ?? null;
502
+ if ("bytes" in fileObj) {
503
+ if (savedPaths !== null) {
504
+ return {
505
+ kind: "file",
506
+ name,
507
+ mimeType,
508
+ uri: null,
509
+ bytes: { _saved_to: savedPaths },
510
+ };
511
+ }
512
+ return {
513
+ kind: "file",
514
+ name,
515
+ mimeType,
516
+ uri: null,
517
+ bytes: { _error: "No FileStore configured. Cannot access file bytes." },
518
+ };
519
+ }
520
+ if ("uri" in fileObj) {
521
+ if (savedPaths !== null) {
522
+ return {
523
+ kind: "file",
524
+ name,
525
+ mimeType,
526
+ uri: { _saved_to: savedPaths },
527
+ bytes: null,
528
+ };
529
+ }
530
+ return {
531
+ kind: "file",
532
+ name,
533
+ mimeType,
534
+ uri: fileObj.uri,
535
+ bytes: null,
536
+ };
537
+ }
538
+ return { kind: "file", name, mimeType, uri: null, bytes: null };
539
+ }
469
540
  }
470
541
  //# sourceMappingURL=a2a-tools.js.map