@a2a-js/sdk 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +422 -0
- package/build/src/a2a_response.d.ts +5 -0
- package/build/src/a2a_response.js +2 -0
- package/build/src/client/client.d.ts +118 -0
- package/build/src/client/client.js +409 -0
- package/build/src/index.d.ts +21 -0
- package/build/src/index.js +18 -0
- package/build/src/samples/agents/movie-agent/genkit.d.ts +2 -0
- package/build/src/samples/agents/movie-agent/genkit.js +11 -0
- package/build/src/samples/agents/movie-agent/index.d.ts +1 -0
- package/build/src/samples/agents/movie-agent/index.js +252 -0
- package/build/src/samples/agents/movie-agent/tmdb.d.ts +7 -0
- package/build/src/samples/agents/movie-agent/tmdb.js +32 -0
- package/build/src/samples/agents/movie-agent/tools.d.ts +15 -0
- package/build/src/samples/agents/movie-agent/tools.js +74 -0
- package/build/src/samples/cli.d.ts +2 -0
- package/build/src/samples/cli.js +289 -0
- package/build/src/server/a2a_express_app.d.ts +14 -0
- package/build/src/server/a2a_express_app.js +98 -0
- package/build/src/server/agent_execution/agent_executor.d.ts +18 -0
- package/build/src/server/agent_execution/agent_executor.js +2 -0
- package/build/src/server/agent_execution/request_context.d.ts +9 -0
- package/build/src/server/agent_execution/request_context.js +15 -0
- package/build/src/server/error.d.ts +23 -0
- package/build/src/server/error.js +57 -0
- package/build/src/server/events/execution_event_bus.d.ts +16 -0
- package/build/src/server/events/execution_event_bus.js +13 -0
- package/build/src/server/events/execution_event_bus_manager.d.ts +27 -0
- package/build/src/server/events/execution_event_bus_manager.js +36 -0
- package/build/src/server/events/execution_event_queue.d.ts +24 -0
- package/build/src/server/events/execution_event_queue.js +63 -0
- package/build/src/server/request_handler/a2a_request_handler.d.ts +11 -0
- package/build/src/server/request_handler/a2a_request_handler.js +2 -0
- package/build/src/server/request_handler/default_request_handler.d.ts +22 -0
- package/build/src/server/request_handler/default_request_handler.js +296 -0
- package/build/src/server/result_manager.d.ts +29 -0
- package/build/src/server/result_manager.js +149 -0
- package/build/src/server/store.d.ts +25 -0
- package/build/src/server/store.js +17 -0
- package/build/src/server/transports/jsonrpc_transport_handler.d.ts +15 -0
- package/build/src/server/transports/jsonrpc_transport_handler.js +114 -0
- package/build/src/server/utils.d.ts +22 -0
- package/build/src/server/utils.js +34 -0
- package/build/src/types-old.d.ts +832 -0
- package/build/src/types-old.js +20 -0
- package/build/src/types.d.ts +2046 -0
- package/build/src/types.js +8 -0
- package/package.json +52 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid'; // For generating unique IDs
|
|
3
|
+
import { InMemoryTaskStore, A2AExpressApp, DefaultRequestHandler } from "../../../index.js";
|
|
4
|
+
import { ai } from "./genkit.js";
|
|
5
|
+
import { searchMovies, searchPeople } from "./tools.js";
|
|
6
|
+
if (!process.env.GEMINI_API_KEY || !process.env.TMDB_API_KEY) {
|
|
7
|
+
console.error("GEMINI_API_KEY and TMDB_API_KEY environment variables are required");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
// Simple store for contexts
|
|
11
|
+
const contexts = new Map();
|
|
12
|
+
// Load the Genkit prompt
|
|
13
|
+
const movieAgentPrompt = ai.prompt('movie_agent');
|
|
14
|
+
/**
|
|
15
|
+
* MovieAgentExecutor implements the agent's core logic.
|
|
16
|
+
*/
|
|
17
|
+
class MovieAgentExecutor {
|
|
18
|
+
cancelledTasks = new Set();
|
|
19
|
+
cancelTask = async (taskId, eventBus) => {
|
|
20
|
+
this.cancelledTasks.add(taskId);
|
|
21
|
+
// The execute loop is responsible for publishing the final state
|
|
22
|
+
};
|
|
23
|
+
async execute(requestContext, eventBus) {
|
|
24
|
+
const userMessage = requestContext.userMessage;
|
|
25
|
+
const existingTask = requestContext.task;
|
|
26
|
+
// Determine IDs for the task and context
|
|
27
|
+
const taskId = requestContext.taskId;
|
|
28
|
+
const contextId = requestContext.contextId;
|
|
29
|
+
console.log(`[MovieAgentExecutor] Processing message ${userMessage.messageId} for task ${taskId} (context: ${contextId})`);
|
|
30
|
+
// 1. Publish initial Task event if it's a new task
|
|
31
|
+
if (!existingTask) {
|
|
32
|
+
const initialTask = {
|
|
33
|
+
kind: 'task',
|
|
34
|
+
id: taskId,
|
|
35
|
+
contextId: contextId,
|
|
36
|
+
status: {
|
|
37
|
+
state: 'submitted',
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
},
|
|
40
|
+
history: [userMessage], // Start history with the current user message
|
|
41
|
+
metadata: userMessage.metadata, // Carry over metadata from message if any
|
|
42
|
+
};
|
|
43
|
+
eventBus.publish(initialTask);
|
|
44
|
+
}
|
|
45
|
+
// 2. Publish "working" status update
|
|
46
|
+
const workingStatusUpdate = {
|
|
47
|
+
kind: 'status-update',
|
|
48
|
+
taskId: taskId,
|
|
49
|
+
contextId: contextId,
|
|
50
|
+
status: {
|
|
51
|
+
state: 'working',
|
|
52
|
+
message: {
|
|
53
|
+
kind: 'message',
|
|
54
|
+
role: 'agent',
|
|
55
|
+
messageId: uuidv4(),
|
|
56
|
+
parts: [{ kind: 'text', text: 'Processing your question, hang tight!' }],
|
|
57
|
+
taskId: taskId,
|
|
58
|
+
contextId: contextId,
|
|
59
|
+
},
|
|
60
|
+
timestamp: new Date().toISOString(),
|
|
61
|
+
},
|
|
62
|
+
final: false,
|
|
63
|
+
};
|
|
64
|
+
eventBus.publish(workingStatusUpdate);
|
|
65
|
+
// 3. Prepare messages for Genkit prompt
|
|
66
|
+
const historyForGenkit = contexts.get(contextId) || [];
|
|
67
|
+
if (!historyForGenkit.find(m => m.messageId === userMessage.messageId)) {
|
|
68
|
+
historyForGenkit.push(userMessage);
|
|
69
|
+
}
|
|
70
|
+
contexts.set(contextId, historyForGenkit);
|
|
71
|
+
const messages = historyForGenkit
|
|
72
|
+
.map((m) => ({
|
|
73
|
+
role: (m.role === 'agent' ? 'model' : 'user'),
|
|
74
|
+
content: m.parts
|
|
75
|
+
.filter((p) => p.kind === 'text' && !!p.text)
|
|
76
|
+
.map((p) => ({
|
|
77
|
+
text: p.text,
|
|
78
|
+
})),
|
|
79
|
+
}))
|
|
80
|
+
.filter((m) => m.content.length > 0);
|
|
81
|
+
if (messages.length === 0) {
|
|
82
|
+
console.warn(`[MovieAgentExecutor] No valid text messages found in history for task ${taskId}.`);
|
|
83
|
+
const failureUpdate = {
|
|
84
|
+
kind: 'status-update',
|
|
85
|
+
taskId: taskId,
|
|
86
|
+
contextId: contextId,
|
|
87
|
+
status: {
|
|
88
|
+
state: 'failed',
|
|
89
|
+
message: {
|
|
90
|
+
kind: 'message',
|
|
91
|
+
role: 'agent',
|
|
92
|
+
messageId: uuidv4(),
|
|
93
|
+
parts: [{ kind: 'text', text: 'No message found to process.' }],
|
|
94
|
+
taskId: taskId,
|
|
95
|
+
contextId: contextId,
|
|
96
|
+
},
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
},
|
|
99
|
+
final: true,
|
|
100
|
+
};
|
|
101
|
+
eventBus.publish(failureUpdate);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const goal = existingTask?.metadata?.goal || userMessage.metadata?.goal;
|
|
105
|
+
try {
|
|
106
|
+
// 4. Run the Genkit prompt
|
|
107
|
+
const response = await movieAgentPrompt({ goal: goal, now: new Date().toISOString() }, {
|
|
108
|
+
messages,
|
|
109
|
+
tools: [searchMovies, searchPeople],
|
|
110
|
+
});
|
|
111
|
+
// Check if the request has been cancelled
|
|
112
|
+
if (this.cancelledTasks.has(taskId)) {
|
|
113
|
+
console.log(`[MovieAgentExecutor] Request cancelled for task: ${taskId}`);
|
|
114
|
+
const cancelledUpdate = {
|
|
115
|
+
kind: 'status-update',
|
|
116
|
+
taskId: taskId,
|
|
117
|
+
contextId: contextId,
|
|
118
|
+
status: {
|
|
119
|
+
state: 'canceled',
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
},
|
|
122
|
+
final: true, // Cancellation is a final state
|
|
123
|
+
};
|
|
124
|
+
eventBus.publish(cancelledUpdate);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const responseText = response.text; // Access the text property using .text()
|
|
128
|
+
console.info(`[MovieAgentExecutor] Prompt response: ${responseText}`);
|
|
129
|
+
const lines = responseText.trim().split('\n');
|
|
130
|
+
const finalStateLine = lines.at(-1)?.trim().toUpperCase();
|
|
131
|
+
const agentReplyText = lines.slice(0, lines.length - 1).join('\n').trim();
|
|
132
|
+
let finalA2AState = "unknown";
|
|
133
|
+
if (finalStateLine === 'COMPLETED') {
|
|
134
|
+
finalA2AState = 'completed';
|
|
135
|
+
}
|
|
136
|
+
else if (finalStateLine === 'AWAITING_USER_INPUT') {
|
|
137
|
+
finalA2AState = 'input-required';
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.warn(`[MovieAgentExecutor] Unexpected final state line from prompt: ${finalStateLine}. Defaulting to 'completed'.`);
|
|
141
|
+
finalA2AState = 'completed'; // Default if LLM deviates
|
|
142
|
+
}
|
|
143
|
+
// 5. Publish final task status update
|
|
144
|
+
const agentMessage = {
|
|
145
|
+
kind: 'message',
|
|
146
|
+
role: 'agent',
|
|
147
|
+
messageId: uuidv4(),
|
|
148
|
+
parts: [{ kind: 'text', text: agentReplyText || "Completed." }], // Ensure some text
|
|
149
|
+
taskId: taskId,
|
|
150
|
+
contextId: contextId,
|
|
151
|
+
};
|
|
152
|
+
historyForGenkit.push(agentMessage);
|
|
153
|
+
contexts.set(contextId, historyForGenkit);
|
|
154
|
+
const finalUpdate = {
|
|
155
|
+
kind: 'status-update',
|
|
156
|
+
taskId: taskId,
|
|
157
|
+
contextId: contextId,
|
|
158
|
+
status: {
|
|
159
|
+
state: finalA2AState,
|
|
160
|
+
message: agentMessage,
|
|
161
|
+
timestamp: new Date().toISOString(),
|
|
162
|
+
},
|
|
163
|
+
final: true,
|
|
164
|
+
};
|
|
165
|
+
eventBus.publish(finalUpdate);
|
|
166
|
+
console.log(`[MovieAgentExecutor] Task ${taskId} finished with state: ${finalA2AState}`);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
console.error(`[MovieAgentExecutor] Error processing task ${taskId}:`, error);
|
|
170
|
+
const errorUpdate = {
|
|
171
|
+
kind: 'status-update',
|
|
172
|
+
taskId: taskId,
|
|
173
|
+
contextId: contextId,
|
|
174
|
+
status: {
|
|
175
|
+
state: 'failed',
|
|
176
|
+
message: {
|
|
177
|
+
kind: 'message',
|
|
178
|
+
role: 'agent',
|
|
179
|
+
messageId: uuidv4(),
|
|
180
|
+
parts: [{ kind: 'text', text: `Agent error: ${error.message}` }],
|
|
181
|
+
taskId: taskId,
|
|
182
|
+
contextId: contextId,
|
|
183
|
+
},
|
|
184
|
+
timestamp: new Date().toISOString(),
|
|
185
|
+
},
|
|
186
|
+
final: true,
|
|
187
|
+
};
|
|
188
|
+
eventBus.publish(errorUpdate);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// --- Server Setup ---
|
|
193
|
+
const movieAgentCard = {
|
|
194
|
+
name: 'Movie Agent',
|
|
195
|
+
description: 'An agent that can answer questions about movies and actors using TMDB.',
|
|
196
|
+
// Adjust the base URL and port as needed. /a2a is the default base in A2AExpressApp
|
|
197
|
+
url: 'http://localhost:41241/', // Example: if baseUrl in A2AExpressApp
|
|
198
|
+
provider: {
|
|
199
|
+
organization: 'A2A Samples',
|
|
200
|
+
url: 'https://example.com/a2a-samples' // Added provider URL
|
|
201
|
+
},
|
|
202
|
+
version: '0.0.2', // Incremented version
|
|
203
|
+
capabilities: {
|
|
204
|
+
streaming: true, // The new framework supports streaming
|
|
205
|
+
pushNotifications: false, // Assuming not implemented for this agent yet
|
|
206
|
+
stateTransitionHistory: true, // Agent uses history
|
|
207
|
+
},
|
|
208
|
+
// authentication: null, // Property 'authentication' does not exist on type 'AgentCard'.
|
|
209
|
+
securitySchemes: undefined, // Or define actual security schemes if any
|
|
210
|
+
security: undefined,
|
|
211
|
+
defaultInputModes: ['text'],
|
|
212
|
+
defaultOutputModes: ['text', 'task-status'], // task-status is a common output mode
|
|
213
|
+
skills: [
|
|
214
|
+
{
|
|
215
|
+
id: 'general_movie_chat',
|
|
216
|
+
name: 'General Movie Chat',
|
|
217
|
+
description: 'Answer general questions or chat about movies, actors, directors.',
|
|
218
|
+
tags: ['movies', 'actors', 'directors'],
|
|
219
|
+
examples: [
|
|
220
|
+
'Tell me about the plot of Inception.',
|
|
221
|
+
'Recommend a good sci-fi movie.',
|
|
222
|
+
'Who directed The Matrix?',
|
|
223
|
+
'What other movies has Scarlett Johansson been in?',
|
|
224
|
+
'Find action movies starring Keanu Reeves',
|
|
225
|
+
'Which came out first, Jurassic Park or Terminator 2?',
|
|
226
|
+
],
|
|
227
|
+
inputModes: ['text'], // Explicitly defining for skill
|
|
228
|
+
outputModes: ['text', 'task-status'] // Explicitly defining for skill
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
supportsAuthenticatedExtendedCard: false,
|
|
232
|
+
};
|
|
233
|
+
async function main() {
|
|
234
|
+
// 1. Create TaskStore
|
|
235
|
+
const taskStore = new InMemoryTaskStore();
|
|
236
|
+
// 2. Create AgentExecutor
|
|
237
|
+
const agentExecutor = new MovieAgentExecutor();
|
|
238
|
+
// 3. Create DefaultRequestHandler
|
|
239
|
+
const requestHandler = new DefaultRequestHandler(movieAgentCard, taskStore, agentExecutor);
|
|
240
|
+
// 4. Create and setup A2AExpressApp
|
|
241
|
+
const appBuilder = new A2AExpressApp(requestHandler);
|
|
242
|
+
const expressApp = appBuilder.setupRoutes(express());
|
|
243
|
+
// 5. Start the server
|
|
244
|
+
const PORT = process.env.PORT || 41241;
|
|
245
|
+
expressApp.listen(PORT, () => {
|
|
246
|
+
console.log(`[MovieAgent] Server using new framework started on http://localhost:${PORT}`);
|
|
247
|
+
console.log(`[MovieAgent] Agent Card: http://localhost:${PORT}/.well-known/agent.json`);
|
|
248
|
+
console.log('[MovieAgent] Press Ctrl+C to stop the server');
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
main().catch(console.error);
|
|
252
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility function to call the TMDB API
|
|
3
|
+
* @param endpoint The TMDB API endpoint (e.g., 'movie', 'person')
|
|
4
|
+
* @param query The search query
|
|
5
|
+
* @returns Promise that resolves to the API response data
|
|
6
|
+
*/
|
|
7
|
+
export declare function callTmdbApi(endpoint: string, query: string): Promise<any>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility function to call the TMDB API
|
|
3
|
+
* @param endpoint The TMDB API endpoint (e.g., 'movie', 'person')
|
|
4
|
+
* @param query The search query
|
|
5
|
+
* @returns Promise that resolves to the API response data
|
|
6
|
+
*/
|
|
7
|
+
export async function callTmdbApi(endpoint, query) {
|
|
8
|
+
// Validate API key
|
|
9
|
+
const apiKey = process.env.TMDB_API_KEY;
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
throw new Error("TMDB_API_KEY environment variable is not set");
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
// Make request to TMDB API
|
|
15
|
+
const url = new URL(`https://api.themoviedb.org/3/search/${endpoint}`);
|
|
16
|
+
url.searchParams.append("api_key", apiKey);
|
|
17
|
+
url.searchParams.append("query", query);
|
|
18
|
+
url.searchParams.append("include_adult", "false");
|
|
19
|
+
url.searchParams.append("language", "en-US");
|
|
20
|
+
url.searchParams.append("page", "1");
|
|
21
|
+
const response = await fetch(url.toString());
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`TMDB API error: ${response.status} ${response.statusText}`);
|
|
24
|
+
}
|
|
25
|
+
return await response.json();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error(`Error calling TMDB API (${endpoint}):`, error);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=tmdb.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "./genkit.js";
|
|
2
|
+
export declare const searchMovies: import("genkit").ToolAction<z.ZodObject<{
|
|
3
|
+
query: z.ZodString;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
query?: string;
|
|
6
|
+
}, {
|
|
7
|
+
query?: string;
|
|
8
|
+
}>, z.ZodTypeAny>;
|
|
9
|
+
export declare const searchPeople: import("genkit").ToolAction<z.ZodObject<{
|
|
10
|
+
query: z.ZodString;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
query?: string;
|
|
13
|
+
}, {
|
|
14
|
+
query?: string;
|
|
15
|
+
}>, z.ZodTypeAny>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ai, z } from "./genkit.js";
|
|
2
|
+
import { callTmdbApi } from "./tmdb.js";
|
|
3
|
+
export const searchMovies = ai.defineTool({
|
|
4
|
+
name: "searchMovies",
|
|
5
|
+
description: "search TMDB for movies by title",
|
|
6
|
+
inputSchema: z.object({
|
|
7
|
+
query: z.string(),
|
|
8
|
+
}),
|
|
9
|
+
}, async ({ query }) => {
|
|
10
|
+
console.log("[tmdb:searchMovies]", JSON.stringify(query));
|
|
11
|
+
try {
|
|
12
|
+
const data = await callTmdbApi("movie", query);
|
|
13
|
+
// Only modify image paths to be full URLs
|
|
14
|
+
const results = data.results.map((movie) => {
|
|
15
|
+
if (movie.poster_path) {
|
|
16
|
+
movie.poster_path = `https://image.tmdb.org/t/p/w500${movie.poster_path}`;
|
|
17
|
+
}
|
|
18
|
+
if (movie.backdrop_path) {
|
|
19
|
+
movie.backdrop_path = `https://image.tmdb.org/t/p/w500${movie.backdrop_path}`;
|
|
20
|
+
}
|
|
21
|
+
return movie;
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
...data,
|
|
25
|
+
results,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error("Error searching movies:", error);
|
|
30
|
+
// Re-throwing allows Genkit/the caller to handle it appropriately
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
export const searchPeople = ai.defineTool({
|
|
35
|
+
name: "searchPeople",
|
|
36
|
+
description: "search TMDB for people by name",
|
|
37
|
+
inputSchema: z.object({
|
|
38
|
+
query: z.string(),
|
|
39
|
+
}),
|
|
40
|
+
}, async ({ query }) => {
|
|
41
|
+
console.log("[tmdb:searchPeople]", JSON.stringify(query));
|
|
42
|
+
try {
|
|
43
|
+
const data = await callTmdbApi("person", query);
|
|
44
|
+
// Only modify image paths to be full URLs
|
|
45
|
+
const results = data.results.map((person) => {
|
|
46
|
+
if (person.profile_path) {
|
|
47
|
+
person.profile_path = `https://image.tmdb.org/t/p/w500${person.profile_path}`;
|
|
48
|
+
}
|
|
49
|
+
// Also modify poster paths in known_for works
|
|
50
|
+
if (person.known_for && Array.isArray(person.known_for)) {
|
|
51
|
+
person.known_for = person.known_for.map((work) => {
|
|
52
|
+
if (work.poster_path) {
|
|
53
|
+
work.poster_path = `https://image.tmdb.org/t/p/w500${work.poster_path}`;
|
|
54
|
+
}
|
|
55
|
+
if (work.backdrop_path) {
|
|
56
|
+
work.backdrop_path = `https://image.tmdb.org/t/p/w500${work.backdrop_path}`;
|
|
57
|
+
}
|
|
58
|
+
return work;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return person;
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
...data,
|
|
65
|
+
results,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error("Error searching people:", error);
|
|
70
|
+
// Re-throwing allows Genkit/the caller to handle it appropriately
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { // Added for explicit Part typing
|
|
5
|
+
A2AClient, } from "../index.js";
|
|
6
|
+
// --- ANSI Colors ---
|
|
7
|
+
const colors = {
|
|
8
|
+
reset: "\x1b[0m",
|
|
9
|
+
bright: "\x1b[1m",
|
|
10
|
+
dim: "\x1b[2m",
|
|
11
|
+
red: "\x1b[31m",
|
|
12
|
+
green: "\x1b[32m",
|
|
13
|
+
yellow: "\x1b[33m",
|
|
14
|
+
blue: "\x1b[34m",
|
|
15
|
+
magenta: "\x1b[35m",
|
|
16
|
+
cyan: "\x1b[36m",
|
|
17
|
+
gray: "\x1b[90m",
|
|
18
|
+
};
|
|
19
|
+
// --- Helper Functions ---
|
|
20
|
+
function colorize(color, text) {
|
|
21
|
+
return `${colors[color]}${text}${colors.reset}`;
|
|
22
|
+
}
|
|
23
|
+
function generateId() {
|
|
24
|
+
return crypto.randomUUID();
|
|
25
|
+
}
|
|
26
|
+
// --- State ---
|
|
27
|
+
let currentTaskId = undefined; // Initialize as undefined
|
|
28
|
+
let currentContextId = undefined; // Initialize as undefined
|
|
29
|
+
const serverUrl = process.argv[2] || "http://localhost:41241"; // Agent's base URL
|
|
30
|
+
const client = new A2AClient(serverUrl);
|
|
31
|
+
let agentName = "Agent"; // Default, try to get from agent card later
|
|
32
|
+
// --- Readline Setup ---
|
|
33
|
+
const rl = readline.createInterface({
|
|
34
|
+
input: process.stdin,
|
|
35
|
+
output: process.stdout,
|
|
36
|
+
prompt: colorize("cyan", "You: "),
|
|
37
|
+
});
|
|
38
|
+
// --- Response Handling ---
|
|
39
|
+
// Function now accepts the unwrapped event payload directly
|
|
40
|
+
function printAgentEvent(event) {
|
|
41
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
42
|
+
const prefix = colorize("magenta", `\n${agentName} [${timestamp}]:`);
|
|
43
|
+
// Check if it's a TaskStatusUpdateEvent
|
|
44
|
+
if (event.kind === "status-update") {
|
|
45
|
+
const update = event; // Cast for type safety
|
|
46
|
+
const state = update.status.state;
|
|
47
|
+
let stateEmoji = "❓";
|
|
48
|
+
let stateColor = "yellow";
|
|
49
|
+
switch (state) {
|
|
50
|
+
case "working":
|
|
51
|
+
stateEmoji = "⏳";
|
|
52
|
+
stateColor = "blue";
|
|
53
|
+
break;
|
|
54
|
+
case "input-required":
|
|
55
|
+
stateEmoji = "🤔";
|
|
56
|
+
stateColor = "yellow";
|
|
57
|
+
break;
|
|
58
|
+
case "completed":
|
|
59
|
+
stateEmoji = "✅";
|
|
60
|
+
stateColor = "green";
|
|
61
|
+
break;
|
|
62
|
+
case "canceled":
|
|
63
|
+
stateEmoji = "⏹️";
|
|
64
|
+
stateColor = "gray";
|
|
65
|
+
break;
|
|
66
|
+
case "failed":
|
|
67
|
+
stateEmoji = "❌";
|
|
68
|
+
stateColor = "red";
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
stateEmoji = "ℹ️"; // For other states like submitted, rejected etc.
|
|
72
|
+
stateColor = "dim";
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
console.log(`${prefix} ${stateEmoji} Status: ${colorize(stateColor, state)} (Task: ${update.taskId}, Context: ${update.contextId}) ${update.final ? colorize("bright", "[FINAL]") : ""}`);
|
|
76
|
+
if (update.status.message) {
|
|
77
|
+
printMessageContent(update.status.message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Check if it's a TaskArtifactUpdateEvent
|
|
81
|
+
else if (event.kind === "artifact-update") {
|
|
82
|
+
const update = event; // Cast for type safety
|
|
83
|
+
console.log(`${prefix} 📄 Artifact Received: ${update.artifact.name || "(unnamed)"} (ID: ${update.artifact.artifactId}, Task: ${update.taskId}, Context: ${update.contextId})`);
|
|
84
|
+
// Create a temporary message-like structure to reuse printMessageContent
|
|
85
|
+
printMessageContent({
|
|
86
|
+
messageId: generateId(), // Dummy messageId
|
|
87
|
+
kind: "message", // Dummy kind
|
|
88
|
+
role: "agent", // Assuming artifact parts are from agent
|
|
89
|
+
parts: update.artifact.parts,
|
|
90
|
+
taskId: update.taskId,
|
|
91
|
+
contextId: update.contextId,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// This case should ideally not be reached if called correctly
|
|
96
|
+
console.log(prefix, colorize("yellow", "Received unknown event type in printAgentEvent:"), event);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function printMessageContent(message) {
|
|
100
|
+
message.parts.forEach((part, index) => {
|
|
101
|
+
const partPrefix = colorize("red", ` Part ${index + 1}:`);
|
|
102
|
+
if (part.kind === "text") { // Check kind property
|
|
103
|
+
console.log(`${partPrefix} ${colorize("green", "📝 Text:")}`, part.text);
|
|
104
|
+
}
|
|
105
|
+
else if (part.kind === "file") { // Check kind property
|
|
106
|
+
const filePart = part;
|
|
107
|
+
console.log(`${partPrefix} ${colorize("blue", "📄 File:")} Name: ${filePart.file.name || "N/A"}, Type: ${filePart.file.mimeType || "N/A"}, Source: ${("bytes" in filePart.file) ? "Inline (bytes)" : filePart.file.uri}`);
|
|
108
|
+
}
|
|
109
|
+
else if (part.kind === "data") { // Check kind property
|
|
110
|
+
const dataPart = part;
|
|
111
|
+
console.log(`${partPrefix} ${colorize("yellow", "📊 Data:")}`, JSON.stringify(dataPart.data, null, 2));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.log(`${partPrefix} ${colorize("yellow", "Unsupported part kind:")}`, part);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// --- Agent Card Fetching ---
|
|
119
|
+
async function fetchAndDisplayAgentCard() {
|
|
120
|
+
// Use the client's getAgentCard method.
|
|
121
|
+
// The client was initialized with serverUrl, which is the agent's base URL.
|
|
122
|
+
console.log(colorize("dim", `Attempting to fetch agent card from agent at: ${serverUrl}`));
|
|
123
|
+
try {
|
|
124
|
+
// client.getAgentCard() uses the agentBaseUrl provided during client construction
|
|
125
|
+
const card = await client.getAgentCard();
|
|
126
|
+
agentName = card.name || "Agent"; // Update global agent name
|
|
127
|
+
console.log(colorize("green", `✓ Agent Card Found:`));
|
|
128
|
+
console.log(` Name: ${colorize("bright", agentName)}`);
|
|
129
|
+
if (card.description) {
|
|
130
|
+
console.log(` Description: ${card.description}`);
|
|
131
|
+
}
|
|
132
|
+
console.log(` Version: ${card.version || "N/A"}`);
|
|
133
|
+
if (card.capabilities?.streaming) {
|
|
134
|
+
console.log(` Streaming: ${colorize("green", "Supported")}`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
console.log(` Streaming: ${colorize("yellow", "Not Supported (or not specified)")}`);
|
|
138
|
+
}
|
|
139
|
+
// Update prompt prefix to use the fetched name
|
|
140
|
+
// The prompt is set dynamically before each rl.prompt() call in the main loop
|
|
141
|
+
// to reflect the current agentName if it changes (though unlikely after initial fetch).
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.log(colorize("yellow", `⚠️ Error fetching or parsing agent card`));
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// --- Main Loop ---
|
|
149
|
+
async function main() {
|
|
150
|
+
console.log(colorize("bright", `A2A Terminal Client`));
|
|
151
|
+
console.log(colorize("dim", `Agent Base URL: ${serverUrl}`));
|
|
152
|
+
await fetchAndDisplayAgentCard(); // Fetch the card before starting the loop
|
|
153
|
+
console.log(colorize("dim", `No active task or context initially. Use '/new' to start a fresh session or send a message.`));
|
|
154
|
+
console.log(colorize("green", `Enter messages, or use '/new' to start a new session. '/exit' to quit.`));
|
|
155
|
+
rl.setPrompt(colorize("cyan", `${agentName} > You: `)); // Set initial prompt
|
|
156
|
+
rl.prompt();
|
|
157
|
+
rl.on("line", async (line) => {
|
|
158
|
+
const input = line.trim();
|
|
159
|
+
rl.setPrompt(colorize("cyan", `${agentName} > You: `)); // Ensure prompt reflects current agentName
|
|
160
|
+
if (!input) {
|
|
161
|
+
rl.prompt();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (input.toLowerCase() === "/new") {
|
|
165
|
+
currentTaskId = undefined;
|
|
166
|
+
currentContextId = undefined; // Reset contextId on /new
|
|
167
|
+
console.log(colorize("bright", `✨ Starting new session. Task and Context IDs are cleared.`));
|
|
168
|
+
rl.prompt();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (input.toLowerCase() === "/exit") {
|
|
172
|
+
rl.close();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Construct params for sendMessageStream
|
|
176
|
+
const messageId = generateId(); // Generate a unique message ID
|
|
177
|
+
const messagePayload = {
|
|
178
|
+
messageId: messageId,
|
|
179
|
+
kind: "message", // Required by Message interface
|
|
180
|
+
role: "user",
|
|
181
|
+
parts: [
|
|
182
|
+
{
|
|
183
|
+
kind: "text", // Required by TextPart interface
|
|
184
|
+
text: input,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
// Conditionally add taskId to the message payload
|
|
189
|
+
if (currentTaskId) {
|
|
190
|
+
messagePayload.taskId = currentTaskId;
|
|
191
|
+
}
|
|
192
|
+
// Conditionally add contextId to the message payload
|
|
193
|
+
if (currentContextId) {
|
|
194
|
+
messagePayload.contextId = currentContextId;
|
|
195
|
+
}
|
|
196
|
+
const params = {
|
|
197
|
+
message: messagePayload,
|
|
198
|
+
// Optional: configuration for streaming, blocking, etc.
|
|
199
|
+
// configuration: {
|
|
200
|
+
// acceptedOutputModes: ['text/plain', 'application/json'], // Example
|
|
201
|
+
// blocking: false // Default for streaming is usually non-blocking
|
|
202
|
+
// }
|
|
203
|
+
};
|
|
204
|
+
try {
|
|
205
|
+
console.log(colorize("red", "Sending message..."));
|
|
206
|
+
// Use sendMessageStream
|
|
207
|
+
const stream = client.sendMessageStream(params);
|
|
208
|
+
// Iterate over the events from the stream
|
|
209
|
+
for await (const event of stream) {
|
|
210
|
+
const timestamp = new Date().toLocaleTimeString(); // Get fresh timestamp for each event
|
|
211
|
+
const prefix = colorize("magenta", `\n${agentName} [${timestamp}]:`);
|
|
212
|
+
if (event.kind === "status-update" || event.kind === "artifact-update") {
|
|
213
|
+
const typedEvent = event;
|
|
214
|
+
printAgentEvent(typedEvent);
|
|
215
|
+
// If the event is a TaskStatusUpdateEvent and it's final, reset currentTaskId
|
|
216
|
+
if (typedEvent.kind === "status-update" && typedEvent.final && typedEvent.status.state !== "input-required") {
|
|
217
|
+
console.log(colorize("yellow", ` Task ${typedEvent.taskId} is final. Clearing current task ID.`));
|
|
218
|
+
currentTaskId = undefined;
|
|
219
|
+
// Optionally, you might want to clear currentContextId as well if a task ending implies context ending.
|
|
220
|
+
// currentContextId = undefined;
|
|
221
|
+
// console.log(colorize("dim", ` Context ID also cleared as task is final.`));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else if (event.kind === "message") {
|
|
225
|
+
const msg = event;
|
|
226
|
+
console.log(`${prefix} ${colorize("green", "✉️ Message Stream Event:")}`);
|
|
227
|
+
printMessageContent(msg);
|
|
228
|
+
if (msg.taskId && msg.taskId !== currentTaskId) {
|
|
229
|
+
console.log(colorize("dim", ` Task ID context updated to ${msg.taskId} based on message event.`));
|
|
230
|
+
currentTaskId = msg.taskId;
|
|
231
|
+
}
|
|
232
|
+
if (msg.contextId && msg.contextId !== currentContextId) {
|
|
233
|
+
console.log(colorize("dim", ` Context ID updated to ${msg.contextId} based on message event.`));
|
|
234
|
+
currentContextId = msg.contextId;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else if (event.kind === "task") {
|
|
238
|
+
const task = event;
|
|
239
|
+
console.log(`${prefix} ${colorize("blue", "ℹ️ Task Stream Event:")} ID: ${task.id}, Context: ${task.contextId}, Status: ${task.status.state}`);
|
|
240
|
+
if (task.id !== currentTaskId) {
|
|
241
|
+
console.log(colorize("dim", ` Task ID updated from ${currentTaskId || 'N/A'} to ${task.id}`));
|
|
242
|
+
currentTaskId = task.id;
|
|
243
|
+
}
|
|
244
|
+
if (task.contextId && task.contextId !== currentContextId) {
|
|
245
|
+
console.log(colorize("dim", ` Context ID updated from ${currentContextId || 'N/A'} to ${task.contextId}`));
|
|
246
|
+
currentContextId = task.contextId;
|
|
247
|
+
}
|
|
248
|
+
if (task.status.message) {
|
|
249
|
+
console.log(colorize("gray", " Task includes message:"));
|
|
250
|
+
printMessageContent(task.status.message);
|
|
251
|
+
}
|
|
252
|
+
if (task.artifacts && task.artifacts.length > 0) {
|
|
253
|
+
console.log(colorize("gray", ` Task includes ${task.artifacts.length} artifact(s).`));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
console.log(prefix, colorize("yellow", "Received unknown event structure from stream:"), event);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
console.log(colorize("dim", `--- End of response stream for this input ---`));
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
264
|
+
const prefix = colorize("red", `\n${agentName} [${timestamp}] ERROR:`);
|
|
265
|
+
console.error(prefix, `Error communicating with agent:`, error.message || error);
|
|
266
|
+
if (error.code) {
|
|
267
|
+
console.error(colorize("gray", ` Code: ${error.code}`));
|
|
268
|
+
}
|
|
269
|
+
if (error.data) {
|
|
270
|
+
console.error(colorize("gray", ` Data: ${JSON.stringify(error.data)}`));
|
|
271
|
+
}
|
|
272
|
+
if (!(error.code || error.data) && error.stack) {
|
|
273
|
+
console.error(colorize("gray", error.stack.split('\n').slice(1, 3).join('\n')));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
rl.prompt();
|
|
278
|
+
}
|
|
279
|
+
}).on("close", () => {
|
|
280
|
+
console.log(colorize("yellow", "\nExiting A2A Terminal Client. Goodbye!"));
|
|
281
|
+
process.exit(0);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
// --- Start ---
|
|
285
|
+
main().catch(err => {
|
|
286
|
+
console.error(colorize("red", "Unhandled error in main:"), err);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
});
|
|
289
|
+
//# sourceMappingURL=cli.js.map
|