proofscan 0.10.61 → 0.11.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.
- package/README.ja.md +1 -0
- package/README.md +2 -0
- package/dist/a2a/agent-card.d.ts +40 -0
- package/dist/a2a/agent-card.d.ts.map +1 -0
- package/dist/a2a/agent-card.js +227 -0
- package/dist/a2a/agent-card.js.map +1 -0
- package/dist/a2a/client.d.ts +169 -0
- package/dist/a2a/client.d.ts.map +1 -0
- package/dist/a2a/client.js +854 -0
- package/dist/a2a/client.js.map +1 -0
- package/dist/a2a/config.d.ts +35 -0
- package/dist/a2a/config.d.ts.map +1 -0
- package/dist/a2a/config.js +474 -0
- package/dist/a2a/config.js.map +1 -0
- package/dist/a2a/index.d.ts +11 -0
- package/dist/a2a/index.d.ts.map +1 -0
- package/dist/a2a/index.js +11 -0
- package/dist/a2a/index.js.map +1 -0
- package/dist/a2a/normalizer.d.ts +66 -0
- package/dist/a2a/normalizer.d.ts.map +1 -0
- package/dist/a2a/normalizer.js +146 -0
- package/dist/a2a/normalizer.js.map +1 -0
- package/dist/a2a/session-manager.d.ts +81 -0
- package/dist/a2a/session-manager.d.ts.map +1 -0
- package/dist/a2a/session-manager.js +176 -0
- package/dist/a2a/session-manager.js.map +1 -0
- package/dist/a2a/types.d.ts +249 -0
- package/dist/a2a/types.d.ts.map +1 -0
- package/dist/a2a/types.js +8 -0
- package/dist/a2a/types.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/agent.d.ts +12 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +339 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +12 -10
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/connectors.js +2 -2
- package/dist/commands/connectors.js.map +1 -1
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/plans.js +1 -1
- package/dist/commands/plans.js.map +1 -1
- package/dist/commands/record.js +5 -4
- package/dist/commands/record.js.map +1 -1
- package/dist/commands/rpc.d.ts.map +1 -1
- package/dist/commands/rpc.js +220 -3
- package/dist/commands/rpc.js.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +8 -10
- package/dist/commands/scan.js.map +1 -1
- package/dist/commands/secrets.d.ts.map +1 -1
- package/dist/commands/secrets.js +11 -10
- package/dist/commands/secrets.js.map +1 -1
- package/dist/commands/sessions.js +2 -2
- package/dist/commands/sessions.js.map +1 -1
- package/dist/commands/summary.d.ts.map +1 -1
- package/dist/commands/summary.js +4 -2
- package/dist/commands/summary.js.map +1 -1
- package/dist/commands/task.d.ts +14 -0
- package/dist/commands/task.d.ts.map +1 -0
- package/dist/commands/task.js +520 -0
- package/dist/commands/task.js.map +1 -0
- package/dist/db/agent-cache-store.d.ts +57 -0
- package/dist/db/agent-cache-store.d.ts.map +1 -0
- package/dist/db/agent-cache-store.js +99 -0
- package/dist/db/agent-cache-store.js.map +1 -0
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +86 -1
- package/dist/db/connection.js.map +1 -1
- package/dist/db/events-store.d.ts +321 -7
- package/dist/db/events-store.d.ts.map +1 -1
- package/dist/db/events-store.js +659 -31
- package/dist/db/events-store.js.map +1 -1
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +2 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/proofs-store.d.ts +8 -1
- package/dist/db/proofs-store.d.ts.map +1 -1
- package/dist/db/proofs-store.js +18 -8
- package/dist/db/proofs-store.js.map +1 -1
- package/dist/db/schema.d.ts +27 -3
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +201 -5
- package/dist/db/schema.js.map +1 -1
- package/dist/db/targets-store.d.ts +79 -0
- package/dist/db/targets-store.d.ts.map +1 -0
- package/dist/db/targets-store.js +150 -0
- package/dist/db/targets-store.js.map +1 -0
- package/dist/db/tool-analysis.d.ts +15 -3
- package/dist/db/tool-analysis.d.ts.map +1 -1
- package/dist/db/tool-analysis.js +35 -17
- package/dist/db/tool-analysis.js.map +1 -1
- package/dist/db/types.d.ts +86 -2
- package/dist/db/types.d.ts.map +1 -1
- package/dist/db/types.js +1 -1
- package/dist/filter/fields.d.ts.map +1 -1
- package/dist/filter/fields.js +22 -0
- package/dist/filter/fields.js.map +1 -1
- package/dist/filter/parser.js +2 -2
- package/dist/filter/parser.js.map +1 -1
- package/dist/filter/types.d.ts +1 -1
- package/dist/filter/types.d.ts.map +1 -1
- package/dist/html/analytics.test.ts +682 -0
- package/dist/html/analytics.ts +499 -0
- package/dist/html/browser.ts +39 -0
- package/dist/html/index.ts +97 -0
- package/dist/html/rpc-inspector.test.ts +529 -0
- package/dist/html/rpc-inspector.ts +1700 -0
- package/dist/html/templates.js +4 -4
- package/dist/html/templates.js.map +1 -1
- package/dist/html/templates.test.ts +861 -0
- package/dist/html/templates.ts +3163 -0
- package/dist/html/trace-viewer.html +624 -0
- package/dist/html/types.d.ts +3 -3
- package/dist/html/types.d.ts.map +1 -1
- package/dist/html/types.ts +491 -0
- package/dist/html/utils.ts +107 -0
- package/dist/monitor/data/connectors.d.ts.map +1 -1
- package/dist/monitor/data/connectors.js +113 -8
- package/dist/monitor/data/connectors.js.map +1 -1
- package/dist/monitor/data/popl.js +2 -2
- package/dist/monitor/data/popl.js.map +1 -1
- package/dist/monitor/routes/api.js +2 -2
- package/dist/monitor/routes/api.js.map +1 -1
- package/dist/monitor/routes/connectors.js +15 -15
- package/dist/monitor/routes/connectors.js.map +1 -1
- package/dist/monitor/routes/popl.js +5 -5
- package/dist/monitor/routes/popl.js.map +1 -1
- package/dist/monitor/templates/components.js +2 -2
- package/dist/monitor/templates/components.js.map +1 -1
- package/dist/monitor/templates/popl.js +4 -4
- package/dist/monitor/templates/popl.js.map +1 -1
- package/dist/monitor/types.d.ts +2 -2
- package/dist/monitor/types.d.ts.map +1 -1
- package/dist/proxy/bridge-utils.d.ts +41 -0
- package/dist/proxy/bridge-utils.d.ts.map +1 -0
- package/dist/proxy/bridge-utils.js +60 -0
- package/dist/proxy/bridge-utils.js.map +1 -0
- package/dist/proxy/ipc-client.d.ts.map +1 -1
- package/dist/proxy/ipc-client.js +1 -2
- package/dist/proxy/ipc-client.js.map +1 -1
- package/dist/proxy/ipc-server.d.ts.map +1 -1
- package/dist/proxy/ipc-server.js +4 -2
- package/dist/proxy/ipc-server.js.map +1 -1
- package/dist/proxy/mcp-server.d.ts +31 -0
- package/dist/proxy/mcp-server.d.ts.map +1 -1
- package/dist/proxy/mcp-server.js +393 -4
- package/dist/proxy/mcp-server.js.map +1 -1
- package/dist/proxy/types.d.ts +95 -0
- package/dist/proxy/types.d.ts.map +1 -1
- package/dist/secrets/management.d.ts +2 -2
- package/dist/secrets/management.d.ts.map +1 -1
- package/dist/secrets/management.js +7 -7
- package/dist/secrets/management.js.map +1 -1
- package/dist/shell/completer.d.ts.map +1 -1
- package/dist/shell/completer.js +16 -0
- package/dist/shell/completer.js.map +1 -1
- package/dist/shell/context-applicator.d.ts.map +1 -1
- package/dist/shell/context-applicator.js +32 -0
- package/dist/shell/context-applicator.js.map +1 -1
- package/dist/shell/filter-mappers.d.ts +5 -1
- package/dist/shell/filter-mappers.d.ts.map +1 -1
- package/dist/shell/filter-mappers.js +12 -0
- package/dist/shell/filter-mappers.js.map +1 -1
- package/dist/shell/find-command.js +13 -13
- package/dist/shell/find-command.js.map +1 -1
- package/dist/shell/inscribe-commands.js +5 -5
- package/dist/shell/inscribe-commands.js.map +1 -1
- package/dist/shell/pager/less-pager.d.ts +1 -1
- package/dist/shell/pager/less-pager.d.ts.map +1 -1
- package/dist/shell/pager/less-pager.js +5 -2
- package/dist/shell/pager/less-pager.js.map +1 -1
- package/dist/shell/pager/more-pager.d.ts +1 -1
- package/dist/shell/pager/more-pager.d.ts.map +1 -1
- package/dist/shell/pager/more-pager.js +3 -2
- package/dist/shell/pager/more-pager.js.map +1 -1
- package/dist/shell/pager/renderer.d.ts.map +1 -1
- package/dist/shell/pager/renderer.js +66 -15
- package/dist/shell/pager/renderer.js.map +1 -1
- package/dist/shell/pager/types.d.ts +5 -2
- package/dist/shell/pager/types.d.ts.map +1 -1
- package/dist/shell/pager/utils.d.ts +5 -2
- package/dist/shell/pager/utils.d.ts.map +1 -1
- package/dist/shell/pager/utils.js +14 -17
- package/dist/shell/pager/utils.js.map +1 -1
- package/dist/shell/pipeline-types.d.ts +12 -4
- package/dist/shell/pipeline-types.d.ts.map +1 -1
- package/dist/shell/ref-commands.js +7 -7
- package/dist/shell/ref-commands.js.map +1 -1
- package/dist/shell/ref-resolver.d.ts +15 -15
- package/dist/shell/ref-resolver.d.ts.map +1 -1
- package/dist/shell/ref-resolver.js +34 -20
- package/dist/shell/ref-resolver.js.map +1 -1
- package/dist/shell/repl.d.ts +25 -0
- package/dist/shell/repl.d.ts.map +1 -1
- package/dist/shell/repl.js +285 -51
- package/dist/shell/repl.js.map +1 -1
- package/dist/shell/router-commands.d.ts +30 -0
- package/dist/shell/router-commands.d.ts.map +1 -1
- package/dist/shell/router-commands.js +1011 -62
- package/dist/shell/router-commands.js.map +1 -1
- package/dist/shell/selector.d.ts +1 -1
- package/dist/shell/selector.d.ts.map +1 -1
- package/dist/shell/selector.js +1 -1
- package/dist/shell/selector.js.map +1 -1
- package/dist/shell/types.d.ts.map +1 -1
- package/dist/shell/types.js +3 -1
- package/dist/shell/types.js.map +1 -1
- package/dist/shell/where-command.d.ts.map +1 -1
- package/dist/shell/where-command.js +19 -3
- package/dist/shell/where-command.js.map +1 -1
- package/dist/utils/output.d.ts.map +1 -1
- package/dist/utils/output.js +7 -1
- package/dist/utils/output.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Client
|
|
3
|
+
*
|
|
4
|
+
* Client for sending messages to A2A agents via JSON-RPC 2.0.
|
|
5
|
+
* Implements message/send, tasks/get, tasks/list, and tasks/cancel operations.
|
|
6
|
+
*
|
|
7
|
+
* Phase 4 - Client Implementation
|
|
8
|
+
* Phase 2 - Task Management (getTask, listTasks, cancelTask)
|
|
9
|
+
* Phase 2.4 - Task DB event recording
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
import { isPrivateUrl } from './agent-card.js';
|
|
13
|
+
// Maximum response size (1MB) to prevent DoS
|
|
14
|
+
const MAX_RESPONSE_SIZE = 1024 * 1024;
|
|
15
|
+
// ===== A2A Client =====
|
|
16
|
+
/**
|
|
17
|
+
* A2A Client for sending messages to agents
|
|
18
|
+
*/
|
|
19
|
+
export class A2AClient {
|
|
20
|
+
baseUrl;
|
|
21
|
+
defaultHeaders;
|
|
22
|
+
agentCard;
|
|
23
|
+
allowLocal;
|
|
24
|
+
eventsStore; // Phase 2.4: Optional events store for task event recording
|
|
25
|
+
constructor(agentCard, options) {
|
|
26
|
+
this.agentCard = agentCard;
|
|
27
|
+
this.baseUrl = agentCard.url.replace(/\/$/, '');
|
|
28
|
+
this.allowLocal = options?.allowLocal ?? false;
|
|
29
|
+
this.eventsStore = options?.eventsStore; // Phase 2.4
|
|
30
|
+
// SSRF protection: Block private URLs in constructor
|
|
31
|
+
if (isPrivateUrl(this.baseUrl) && !this.allowLocal) {
|
|
32
|
+
throw new Error('Private or local URLs are not allowed');
|
|
33
|
+
}
|
|
34
|
+
this.defaultHeaders = {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
'Accept': 'application/json',
|
|
37
|
+
...options?.headers,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get EventsStore instance (Phase 2.4)
|
|
42
|
+
* @private
|
|
43
|
+
*/
|
|
44
|
+
getEventsStore() {
|
|
45
|
+
return this.eventsStore;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Send a message to the agent
|
|
49
|
+
* POST /message/send (JSON-RPC 2.0)
|
|
50
|
+
*/
|
|
51
|
+
async sendMessage(message, options = {}) {
|
|
52
|
+
const { timeout = 30000, headers = {}, blocking = false } = options;
|
|
53
|
+
// Convert string message to A2AMessage with unique messageId
|
|
54
|
+
const messageId = randomUUID();
|
|
55
|
+
const a2aMessage = typeof message === 'string'
|
|
56
|
+
? { role: 'user', parts: [{ text: message }], messageId }
|
|
57
|
+
: { ...message, messageId: message.messageId ?? messageId };
|
|
58
|
+
// Build JSON-RPC request with unique ID
|
|
59
|
+
const requestId = randomUUID();
|
|
60
|
+
const request = {
|
|
61
|
+
jsonrpc: '2.0',
|
|
62
|
+
id: requestId,
|
|
63
|
+
method: 'message/send',
|
|
64
|
+
params: {
|
|
65
|
+
message: a2aMessage,
|
|
66
|
+
configuration: blocking ? { blocking: true } : undefined,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
// Set up abort controller for timeout
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
72
|
+
try {
|
|
73
|
+
// SSRF protection (defense-in-depth): Double-check URL even though constructor validates
|
|
74
|
+
if (isPrivateUrl(this.baseUrl) && !this.allowLocal) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: 'Private or local URLs are not allowed',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const response = await fetch(this.baseUrl, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { ...this.defaultHeaders, ...headers },
|
|
83
|
+
body: JSON.stringify(request),
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
});
|
|
86
|
+
// Validate Content-Type
|
|
87
|
+
const contentType = response.headers.get('content-type');
|
|
88
|
+
if (!contentType?.includes('application/json')) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
statusCode: response.status,
|
|
92
|
+
error: `Expected JSON response, got ${contentType || 'unknown'}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Check Content-Length for size limit
|
|
96
|
+
const contentLength = response.headers.get('content-length');
|
|
97
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
statusCode: response.status,
|
|
101
|
+
error: `Response too large: ${contentLength} bytes (max ${MAX_RESPONSE_SIZE})`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const responseText = await response.text();
|
|
105
|
+
// Validate actual response size
|
|
106
|
+
if (responseText.length > MAX_RESPONSE_SIZE) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
statusCode: response.status,
|
|
110
|
+
error: `Response too large: ${responseText.length} bytes (max ${MAX_RESPONSE_SIZE})`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
let responseData;
|
|
114
|
+
try {
|
|
115
|
+
responseData = JSON.parse(responseText);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
statusCode: response.status,
|
|
121
|
+
error: `Invalid JSON response: ${responseText.slice(0, 200)}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Handle JSON-RPC error
|
|
125
|
+
if (responseData.error) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
statusCode: response.status,
|
|
129
|
+
error: `${responseData.error.code}: ${responseData.error.message}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Parse result - can be Task or Message
|
|
133
|
+
if (!responseData.result) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
statusCode: response.status,
|
|
137
|
+
error: 'No result in response',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const result = responseData.result;
|
|
141
|
+
// Check if result is a Task (has 'status' field)
|
|
142
|
+
if ('status' in result) {
|
|
143
|
+
const task = this.parseTask(result);
|
|
144
|
+
return { ok: true, task };
|
|
145
|
+
}
|
|
146
|
+
// Check if result is a Message (has 'role' field)
|
|
147
|
+
if ('role' in result) {
|
|
148
|
+
const msg = this.parseMessage(result);
|
|
149
|
+
return { ok: true, message: msg };
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
statusCode: response.status,
|
|
154
|
+
error: `Unknown response type: ${JSON.stringify(result).slice(0, 200)}`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error instanceof Error) {
|
|
159
|
+
if (error.name === 'AbortError') {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
error: `Request timeout after ${timeout}ms`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
error: error.message,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
error: String(error),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
clearTimeout(timeoutId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get task by ID (Phase 2 - A2A Protocol Compliant)
|
|
181
|
+
* POST /a2a (JSON-RPC 2.0)
|
|
182
|
+
*
|
|
183
|
+
* @param taskId - The task ID to retrieve
|
|
184
|
+
* @param options - Optional: historyLength, headers, timeout
|
|
185
|
+
* @returns Promise<GetTaskResult> with task or error
|
|
186
|
+
*/
|
|
187
|
+
async getTask(taskId, options = {}) {
|
|
188
|
+
const { historyLength, headers = {}, timeout = 30000 } = options;
|
|
189
|
+
const requestId = randomUUID();
|
|
190
|
+
const request = {
|
|
191
|
+
jsonrpc: '2.0',
|
|
192
|
+
id: requestId,
|
|
193
|
+
method: 'tasks/get',
|
|
194
|
+
params: {
|
|
195
|
+
id: taskId,
|
|
196
|
+
...(historyLength !== undefined && { historyLength }),
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
201
|
+
try {
|
|
202
|
+
const response = await fetch(this.baseUrl, {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: { ...this.defaultHeaders, ...headers },
|
|
205
|
+
body: JSON.stringify(request),
|
|
206
|
+
signal: controller.signal,
|
|
207
|
+
});
|
|
208
|
+
const contentType = response.headers.get('content-type');
|
|
209
|
+
if (!contentType?.includes('application/json')) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
statusCode: response.status,
|
|
213
|
+
error: `Expected JSON response, got ${contentType || 'unknown'}`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const responseText = await response.text();
|
|
217
|
+
if (responseText.length > MAX_RESPONSE_SIZE) {
|
|
218
|
+
return {
|
|
219
|
+
ok: false,
|
|
220
|
+
statusCode: response.status,
|
|
221
|
+
error: `Response too large: ${responseText.length} bytes (max ${MAX_RESPONSE_SIZE})`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
let responseData;
|
|
225
|
+
try {
|
|
226
|
+
responseData = JSON.parse(responseText);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
statusCode: response.status,
|
|
232
|
+
error: `Invalid JSON response: ${responseText.slice(0, 200)}`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (responseData.error) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
statusCode: response.status,
|
|
239
|
+
error: `${responseData.error.code}: ${responseData.error.message}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (!responseData.result) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
statusCode: response.status,
|
|
246
|
+
error: 'No result in response',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const task = this.parseTask(responseData.result);
|
|
250
|
+
return { ok: true, task };
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (error instanceof Error) {
|
|
254
|
+
if (error.name === 'AbortError') {
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
error: `Request timeout after ${timeout}ms`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
error: error.message,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
error: String(error),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
clearTimeout(timeoutId);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* List tasks with optional filters (Phase 2)
|
|
276
|
+
* POST /a2a (JSON-RPC 2.0)
|
|
277
|
+
*
|
|
278
|
+
* @param params - Optional: contextId, status, pageSize, pageToken, includeArtifacts
|
|
279
|
+
* @returns Promise<ListTasksResult> with tasks list or error
|
|
280
|
+
*/
|
|
281
|
+
async listTasks(params) {
|
|
282
|
+
const requestId = randomUUID();
|
|
283
|
+
const request = {
|
|
284
|
+
jsonrpc: '2.0',
|
|
285
|
+
id: requestId,
|
|
286
|
+
method: 'tasks/list',
|
|
287
|
+
params: (params || {}),
|
|
288
|
+
};
|
|
289
|
+
try {
|
|
290
|
+
const response = await fetch(this.baseUrl, {
|
|
291
|
+
method: 'POST',
|
|
292
|
+
headers: this.defaultHeaders,
|
|
293
|
+
body: JSON.stringify(request),
|
|
294
|
+
});
|
|
295
|
+
const contentType = response.headers.get('content-type');
|
|
296
|
+
if (!contentType?.includes('application/json')) {
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
statusCode: response.status,
|
|
300
|
+
error: `Expected JSON response, got ${contentType || 'unknown'}`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const responseText = await response.text();
|
|
304
|
+
if (responseText.length > MAX_RESPONSE_SIZE) {
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
statusCode: response.status,
|
|
308
|
+
error: `Response too large: ${responseText.length} bytes (max ${MAX_RESPONSE_SIZE})`,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
let responseData;
|
|
312
|
+
try {
|
|
313
|
+
responseData = JSON.parse(responseText);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return {
|
|
317
|
+
ok: false,
|
|
318
|
+
statusCode: response.status,
|
|
319
|
+
error: `Invalid JSON response: ${responseText.slice(0, 200)}`,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (responseData.error) {
|
|
323
|
+
return {
|
|
324
|
+
ok: false,
|
|
325
|
+
statusCode: response.status,
|
|
326
|
+
error: `${responseData.error.code}: ${responseData.error.message}`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (!responseData.result) {
|
|
330
|
+
return {
|
|
331
|
+
ok: false,
|
|
332
|
+
statusCode: response.status,
|
|
333
|
+
error: 'No result in response',
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const result = responseData.result;
|
|
337
|
+
const tasks = [];
|
|
338
|
+
if (Array.isArray(result.tasks)) {
|
|
339
|
+
for (const taskData of result.tasks) {
|
|
340
|
+
tasks.push(this.parseTask(taskData));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const listResponse = {
|
|
344
|
+
tasks,
|
|
345
|
+
nextPageToken: result.nextPageToken ? String(result.nextPageToken) : '',
|
|
346
|
+
pageSize: typeof result.pageSize === 'number' ? result.pageSize : 50,
|
|
347
|
+
totalSize: typeof result.totalSize === 'number' ? result.totalSize : undefined,
|
|
348
|
+
};
|
|
349
|
+
return { ok: true, response: listResponse };
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
if (error instanceof Error) {
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
error: error.message,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
error: String(error),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Cancel a task (Phase 2 - A2A Protocol Compliant)
|
|
366
|
+
* POST /a2a (JSON-RPC 2.0)
|
|
367
|
+
*
|
|
368
|
+
* @param taskId - The task ID to cancel
|
|
369
|
+
* @returns Promise<CancelTaskResult> with canceled task or error
|
|
370
|
+
*/
|
|
371
|
+
async cancelTask(taskId) {
|
|
372
|
+
const requestId = randomUUID();
|
|
373
|
+
const request = {
|
|
374
|
+
jsonrpc: '2.0',
|
|
375
|
+
id: requestId,
|
|
376
|
+
method: 'tasks/cancel',
|
|
377
|
+
params: {
|
|
378
|
+
id: taskId,
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
try {
|
|
382
|
+
const response = await fetch(this.baseUrl, {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
headers: this.defaultHeaders,
|
|
385
|
+
body: JSON.stringify(request),
|
|
386
|
+
});
|
|
387
|
+
const contentType = response.headers.get('content-type');
|
|
388
|
+
if (!contentType?.includes('application/json')) {
|
|
389
|
+
return {
|
|
390
|
+
ok: false,
|
|
391
|
+
statusCode: response.status,
|
|
392
|
+
error: `Expected JSON response, got ${contentType || 'unknown'}`,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const responseText = await response.text();
|
|
396
|
+
if (responseText.length > MAX_RESPONSE_SIZE) {
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
statusCode: response.status,
|
|
400
|
+
error: `Response too large: ${responseText.length} bytes (max ${MAX_RESPONSE_SIZE})`,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
let responseData;
|
|
404
|
+
try {
|
|
405
|
+
responseData = JSON.parse(responseText);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
statusCode: response.status,
|
|
411
|
+
error: `Invalid JSON response: ${responseText.slice(0, 200)}`,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (responseData.error) {
|
|
415
|
+
return {
|
|
416
|
+
ok: false,
|
|
417
|
+
statusCode: response.status,
|
|
418
|
+
error: `${responseData.error.code}: ${responseData.error.message}`,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
// Parse the canceled task from response
|
|
422
|
+
if (responseData.result) {
|
|
423
|
+
const task = this.parseTask(responseData.result);
|
|
424
|
+
return { ok: true, task };
|
|
425
|
+
}
|
|
426
|
+
return { ok: true };
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
if (error instanceof Error) {
|
|
430
|
+
return {
|
|
431
|
+
ok: false,
|
|
432
|
+
error: error.message,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
ok: false,
|
|
437
|
+
error: String(error),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Stream message to agent
|
|
443
|
+
* POST /message/stream (JSON-RPC 2.0 + SSE response)
|
|
444
|
+
*/
|
|
445
|
+
async streamMessage(message, options = {}) {
|
|
446
|
+
const { timeout = 60000, headers = {}, onStatus, onArtifact, onMessage, onTask, onError, signal, } = options;
|
|
447
|
+
// Build JSON-RPC request
|
|
448
|
+
const a2aMessage = typeof message === 'string'
|
|
449
|
+
? { role: 'user', parts: [{ text: message }] }
|
|
450
|
+
: message;
|
|
451
|
+
const requestId = randomUUID();
|
|
452
|
+
const request = {
|
|
453
|
+
jsonrpc: '2.0',
|
|
454
|
+
id: requestId,
|
|
455
|
+
method: 'message/stream',
|
|
456
|
+
params: { message: a2aMessage },
|
|
457
|
+
};
|
|
458
|
+
// Abort controller for timeout
|
|
459
|
+
const controller = new AbortController();
|
|
460
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
461
|
+
// Combine with external signal if provided
|
|
462
|
+
if (signal) {
|
|
463
|
+
signal.addEventListener('abort', () => controller.abort());
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
// SSRF protection (defense-in-depth)
|
|
467
|
+
if (isPrivateUrl(this.baseUrl) && !this.allowLocal) {
|
|
468
|
+
return {
|
|
469
|
+
ok: false,
|
|
470
|
+
error: 'Private or local URLs are not allowed',
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const response = await fetch(`${this.baseUrl}/message/stream`, {
|
|
474
|
+
method: 'POST',
|
|
475
|
+
headers: { ...this.defaultHeaders, ...headers },
|
|
476
|
+
body: JSON.stringify(request),
|
|
477
|
+
signal: controller.signal,
|
|
478
|
+
});
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
return {
|
|
481
|
+
ok: false,
|
|
482
|
+
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const contentType = response.headers.get('content-type');
|
|
486
|
+
if (!contentType?.includes('text/event-stream')) {
|
|
487
|
+
return {
|
|
488
|
+
ok: false,
|
|
489
|
+
error: `Expected SSE, got ${contentType}`,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
// Process SSE stream
|
|
493
|
+
const reader = response.body?.getReader();
|
|
494
|
+
if (!reader) {
|
|
495
|
+
return { ok: false, error: 'No response body' };
|
|
496
|
+
}
|
|
497
|
+
const decoder = new TextDecoder();
|
|
498
|
+
let buffer = '';
|
|
499
|
+
let taskId;
|
|
500
|
+
while (true) {
|
|
501
|
+
const { done, value } = await reader.read();
|
|
502
|
+
if (done)
|
|
503
|
+
break;
|
|
504
|
+
buffer += decoder.decode(value, { stream: true });
|
|
505
|
+
const lines = buffer.split('\n');
|
|
506
|
+
buffer = lines.pop() || ''; // Keep incomplete line
|
|
507
|
+
for (const line of lines) {
|
|
508
|
+
if (line.startsWith('data: ')) {
|
|
509
|
+
const data = line.slice(6);
|
|
510
|
+
if (data === '[DONE]')
|
|
511
|
+
continue;
|
|
512
|
+
try {
|
|
513
|
+
const json = JSON.parse(data);
|
|
514
|
+
const event = this.parseStreamEvent(json);
|
|
515
|
+
if (event) {
|
|
516
|
+
switch (event.type) {
|
|
517
|
+
case 'status':
|
|
518
|
+
taskId = event.event.taskId;
|
|
519
|
+
onStatus?.(event.event);
|
|
520
|
+
if (event.event.final) {
|
|
521
|
+
return { ok: true, taskId };
|
|
522
|
+
}
|
|
523
|
+
break;
|
|
524
|
+
case 'artifact':
|
|
525
|
+
onArtifact?.(event.event);
|
|
526
|
+
break;
|
|
527
|
+
case 'message':
|
|
528
|
+
onMessage?.(event.message);
|
|
529
|
+
break;
|
|
530
|
+
case 'task':
|
|
531
|
+
taskId = event.task.id;
|
|
532
|
+
onTask?.(event.task);
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch (e) {
|
|
538
|
+
onError?.(`Parse error: ${e}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return { ok: true, taskId };
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
547
|
+
return { ok: false, error: `Timeout after ${timeout}ms` };
|
|
548
|
+
}
|
|
549
|
+
return { ok: false, error: String(error) };
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
clearTimeout(timeoutId);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Parse Task from JSON-RPC response
|
|
557
|
+
*/
|
|
558
|
+
parseTask(data) {
|
|
559
|
+
const task = {
|
|
560
|
+
id: String(data.id || ''),
|
|
561
|
+
status: this.parseTaskStatus(data.status),
|
|
562
|
+
messages: [],
|
|
563
|
+
artifacts: undefined,
|
|
564
|
+
createdAt: data.createdAt ? String(data.createdAt) : undefined,
|
|
565
|
+
updatedAt: data.updatedAt ? String(data.updatedAt) : undefined,
|
|
566
|
+
contextId: data.contextId ? String(data.contextId) : undefined,
|
|
567
|
+
};
|
|
568
|
+
// Parse messages (A2A uses 'history' field, some agents use 'messages')
|
|
569
|
+
const messageList = Array.isArray(data.history) ? data.history :
|
|
570
|
+
Array.isArray(data.messages) ? data.messages : [];
|
|
571
|
+
if (messageList.length > 0) {
|
|
572
|
+
task.messages = messageList.map(msg => this.parseMessage(msg));
|
|
573
|
+
}
|
|
574
|
+
// Parse artifacts
|
|
575
|
+
if (Array.isArray(data.artifacts)) {
|
|
576
|
+
task.artifacts = data.artifacts.map(art => ({
|
|
577
|
+
name: art.name ? String(art.name) : undefined,
|
|
578
|
+
description: art.description ? String(art.description) : undefined,
|
|
579
|
+
parts: Array.isArray(art.parts)
|
|
580
|
+
? art.parts.map((part) => this.parsePart(part))
|
|
581
|
+
: [],
|
|
582
|
+
}));
|
|
583
|
+
}
|
|
584
|
+
return task;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Parse Message from JSON-RPC response
|
|
588
|
+
*/
|
|
589
|
+
parseMessage(data) {
|
|
590
|
+
if (!data || typeof data !== 'object') {
|
|
591
|
+
return { role: 'user', parts: [] };
|
|
592
|
+
}
|
|
593
|
+
const obj = data;
|
|
594
|
+
const message = {
|
|
595
|
+
// A2A protocol uses 'agent' for assistant responses
|
|
596
|
+
role: (obj.role === 'assistant' || obj.role === 'agent') ? 'assistant' : 'user',
|
|
597
|
+
parts: [],
|
|
598
|
+
metadata: obj.metadata,
|
|
599
|
+
contextId: obj.contextId ? String(obj.contextId) : undefined,
|
|
600
|
+
referenceTaskIds: Array.isArray(obj.referenceTaskIds)
|
|
601
|
+
? obj.referenceTaskIds.map(String)
|
|
602
|
+
: undefined,
|
|
603
|
+
};
|
|
604
|
+
if (Array.isArray(obj.parts)) {
|
|
605
|
+
message.parts = obj.parts.map(part => this.parsePart(part));
|
|
606
|
+
}
|
|
607
|
+
return message;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Parse Message Part from JSON-RPC response
|
|
611
|
+
*/
|
|
612
|
+
parsePart(data) {
|
|
613
|
+
if (!data || typeof data !== 'object') {
|
|
614
|
+
return { text: '' };
|
|
615
|
+
}
|
|
616
|
+
const obj = data;
|
|
617
|
+
// TextPart
|
|
618
|
+
if ('text' in obj && typeof obj.text === 'string') {
|
|
619
|
+
return { text: obj.text };
|
|
620
|
+
}
|
|
621
|
+
// DataPart (file data)
|
|
622
|
+
if ('data' in obj && 'mimeType' in obj) {
|
|
623
|
+
return {
|
|
624
|
+
data: String(obj.data),
|
|
625
|
+
mimeType: String(obj.mimeType),
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
// Fallback: if 'text' exists but not a string, convert
|
|
629
|
+
if ('text' in obj) {
|
|
630
|
+
return { text: String(obj.text) };
|
|
631
|
+
}
|
|
632
|
+
return { text: '' };
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Parse task status string
|
|
636
|
+
*/
|
|
637
|
+
parseTaskStatus(status) {
|
|
638
|
+
if (typeof status !== 'string') {
|
|
639
|
+
return 'pending';
|
|
640
|
+
}
|
|
641
|
+
const validStatuses = [
|
|
642
|
+
'pending',
|
|
643
|
+
'working',
|
|
644
|
+
'input_required',
|
|
645
|
+
'completed',
|
|
646
|
+
'failed',
|
|
647
|
+
'canceled',
|
|
648
|
+
'rejected',
|
|
649
|
+
];
|
|
650
|
+
if (validStatuses.includes(status)) {
|
|
651
|
+
return status;
|
|
652
|
+
}
|
|
653
|
+
return 'pending';
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Parse SSE event data
|
|
657
|
+
*/
|
|
658
|
+
parseStreamEvent(data) {
|
|
659
|
+
if (!data || typeof data !== 'object')
|
|
660
|
+
return null;
|
|
661
|
+
const obj = data;
|
|
662
|
+
// JSON-RPC response wrapper
|
|
663
|
+
const result = obj.result;
|
|
664
|
+
if (!result)
|
|
665
|
+
return null;
|
|
666
|
+
// Check event type
|
|
667
|
+
if ('status' in result && 'taskId' in result) {
|
|
668
|
+
return {
|
|
669
|
+
type: 'status',
|
|
670
|
+
event: this.parseStatusEvent(result),
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
if ('artifact' in result && 'taskId' in result) {
|
|
674
|
+
return {
|
|
675
|
+
type: 'artifact',
|
|
676
|
+
event: this.parseArtifactEvent(result),
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
if ('id' in result && 'status' in result && 'messages' in result) {
|
|
680
|
+
return {
|
|
681
|
+
type: 'task',
|
|
682
|
+
task: this.parseTask(result),
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if ('role' in result && 'parts' in result) {
|
|
686
|
+
return {
|
|
687
|
+
type: 'message',
|
|
688
|
+
message: this.parseMessage(result),
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Parse status event
|
|
695
|
+
*/
|
|
696
|
+
parseStatusEvent(data) {
|
|
697
|
+
return {
|
|
698
|
+
taskId: String(data.taskId || ''),
|
|
699
|
+
contextId: data.contextId ? String(data.contextId) : undefined,
|
|
700
|
+
status: this.parseTaskStatus(data.status),
|
|
701
|
+
message: data.message ? this.parseMessage(data.message) : undefined,
|
|
702
|
+
final: Boolean(data.final),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Parse artifact event
|
|
707
|
+
*/
|
|
708
|
+
parseArtifactEvent(data) {
|
|
709
|
+
const artifact = data.artifact || {};
|
|
710
|
+
return {
|
|
711
|
+
taskId: String(data.taskId || ''),
|
|
712
|
+
contextId: data.contextId ? String(data.contextId) : undefined,
|
|
713
|
+
artifact: {
|
|
714
|
+
name: artifact.name ? String(artifact.name) : undefined,
|
|
715
|
+
description: artifact.description ? String(artifact.description) : undefined,
|
|
716
|
+
parts: Array.isArray(artifact.parts)
|
|
717
|
+
? artifact.parts.map((p) => this.parsePart(p))
|
|
718
|
+
: [],
|
|
719
|
+
index: typeof artifact.index === 'number' ? artifact.index : undefined,
|
|
720
|
+
append: Boolean(artifact.append),
|
|
721
|
+
lastChunk: Boolean(artifact.lastChunk),
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// ===== Helper Functions =====
|
|
727
|
+
import { TargetsStore } from '../db/targets-store.js';
|
|
728
|
+
import { AgentCacheStore } from '../db/agent-cache-store.js';
|
|
729
|
+
import { fetchAgentCard } from './agent-card.js';
|
|
730
|
+
/**
|
|
731
|
+
* Create A2A client from agent ID
|
|
732
|
+
* Fetches Agent Card from cache or remote endpoint
|
|
733
|
+
*/
|
|
734
|
+
export async function createA2AClient(configDir, agentId) {
|
|
735
|
+
const targetsStore = new TargetsStore(configDir);
|
|
736
|
+
const cacheStore = new AgentCacheStore(configDir);
|
|
737
|
+
// Find agent by ID or prefix
|
|
738
|
+
const agents = targetsStore.list({ type: 'agent' });
|
|
739
|
+
const agent = agents.find(a => a.id === agentId || a.id.startsWith(agentId));
|
|
740
|
+
if (!agent) {
|
|
741
|
+
return { ok: false, error: `Agent '${agentId}' not found` };
|
|
742
|
+
}
|
|
743
|
+
if (!agent.enabled) {
|
|
744
|
+
return { ok: false, error: `Agent '${agentId}' is disabled` };
|
|
745
|
+
}
|
|
746
|
+
// Get agent config
|
|
747
|
+
const config = agent.config;
|
|
748
|
+
if (!config.url) {
|
|
749
|
+
return { ok: false, error: `Agent '${agentId}' has no URL configured` };
|
|
750
|
+
}
|
|
751
|
+
// Check cache
|
|
752
|
+
const cached = cacheStore.get(agent.id);
|
|
753
|
+
const now = new Date();
|
|
754
|
+
let agentCard = null;
|
|
755
|
+
if (cached?.agentCard && cached.expiresAt && new Date(cached.expiresAt) > now) {
|
|
756
|
+
// Use cached card
|
|
757
|
+
agentCard = cached.agentCard;
|
|
758
|
+
}
|
|
759
|
+
else if (cached?.agentCard && !cached.expiresAt) {
|
|
760
|
+
// Use cached card (no expiration)
|
|
761
|
+
agentCard = cached.agentCard;
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
// Fetch fresh agent card
|
|
765
|
+
const fetchResult = await fetchAgentCard(config.url, { allowLocal: config.allow_local ?? false });
|
|
766
|
+
if (!fetchResult.ok || !fetchResult.agentCard) {
|
|
767
|
+
return {
|
|
768
|
+
ok: false,
|
|
769
|
+
error: `Failed to fetch agent card: ${fetchResult.error}`,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
agentCard = fetchResult.agentCard;
|
|
773
|
+
// Update cache
|
|
774
|
+
const ttl = config.ttl_seconds || 3600;
|
|
775
|
+
const expiresAt = new Date(now.getTime() + ttl * 1000).toISOString();
|
|
776
|
+
cacheStore.set({
|
|
777
|
+
targetId: agent.id,
|
|
778
|
+
agentCard,
|
|
779
|
+
agentCardHash: fetchResult.hash,
|
|
780
|
+
fetchedAt: now.toISOString(),
|
|
781
|
+
expiresAt,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
// Create client with allowLocal from config
|
|
785
|
+
const client = new A2AClient(agentCard, { allowLocal: config.allow_local ?? false });
|
|
786
|
+
return { ok: true, client, agentCard };
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Probe agent capabilities (Phase 2.5)
|
|
790
|
+
*
|
|
791
|
+
* Detects agent capabilities by:
|
|
792
|
+
* 1. Checking Agent Card capabilities field (if present)
|
|
793
|
+
* 2. Probing tasks/list endpoint to detect task support
|
|
794
|
+
*
|
|
795
|
+
* Note: Streaming capability is only detected from Agent Card, not probed,
|
|
796
|
+
* because SSE probing is more complex and unreliable.
|
|
797
|
+
*
|
|
798
|
+
* @param agentCard - The agent's Agent Card
|
|
799
|
+
* @param _allowLocal - Reserved for future use (URL validation)
|
|
800
|
+
* @returns Promise<AgentCapabilities> with detected capabilities
|
|
801
|
+
*/
|
|
802
|
+
export async function probeCapabilities(agentCard, _allowLocal = false) {
|
|
803
|
+
// Default capabilities (all false)
|
|
804
|
+
const capabilities = {
|
|
805
|
+
tasks: false,
|
|
806
|
+
streaming: false,
|
|
807
|
+
};
|
|
808
|
+
// First, check Agent Card capabilities field (if present)
|
|
809
|
+
// This is the authoritative source if provided by the agent
|
|
810
|
+
if (agentCard.capabilities) {
|
|
811
|
+
if (agentCard.capabilities.streaming !== undefined) {
|
|
812
|
+
capabilities.streaming = agentCard.capabilities.streaming;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
// Probe tasks/list endpoint
|
|
816
|
+
try {
|
|
817
|
+
const response = await fetch(agentCard.url, {
|
|
818
|
+
method: 'POST',
|
|
819
|
+
headers: {
|
|
820
|
+
'Content-Type': 'application/json',
|
|
821
|
+
},
|
|
822
|
+
body: JSON.stringify({
|
|
823
|
+
jsonrpc: '2.0',
|
|
824
|
+
id: 'probe-tasks',
|
|
825
|
+
method: 'tasks/list',
|
|
826
|
+
params: {},
|
|
827
|
+
}),
|
|
828
|
+
signal: AbortSignal.timeout(5000), // 5 second timeout for probe
|
|
829
|
+
});
|
|
830
|
+
// Check for successful JSON-RPC response
|
|
831
|
+
// A 200 with { "error": { "code": -32601, ... } } means method not found
|
|
832
|
+
if (response.ok) {
|
|
833
|
+
try {
|
|
834
|
+
const data = await response.json();
|
|
835
|
+
// Only set tasks=true if we get a result (not a JSON-RPC error)
|
|
836
|
+
if (data.result !== undefined && !data.error) {
|
|
837
|
+
capabilities.tasks = true;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
// If we can't parse JSON, assume tasks not supported
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
// On network error or timeout, assume tasks not supported (keep false)
|
|
847
|
+
}
|
|
848
|
+
// Note: We don't probe message/stream here because:
|
|
849
|
+
// 1. It's more complex (SSE)
|
|
850
|
+
// 2. The Agent Card capabilities field is the preferred source
|
|
851
|
+
// 3. Most A2A agents that support tasks also support streaming
|
|
852
|
+
return capabilities;
|
|
853
|
+
}
|
|
854
|
+
//# sourceMappingURL=client.js.map
|