@runtypelabs/a2a-aisdk-example 0.0.1 → 0.2.3
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 +21 -0
- package/README.md +357 -0
- package/dist/chunk-OLB7ZZNY.js +1153 -0
- package/dist/chunk-OLB7ZZNY.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +215 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +333 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/vercel/index.d.ts +145 -0
- package/dist/vercel/index.js +926 -0
- package/dist/vercel/index.js.map +1 -0
- package/package.json +83 -4
- package/index.js +0 -1
|
@@ -0,0 +1,1153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/types.ts
|
|
4
|
+
var JSON_RPC_ERRORS = {
|
|
5
|
+
PARSE_ERROR: -32700,
|
|
6
|
+
INVALID_REQUEST: -32600,
|
|
7
|
+
METHOD_NOT_FOUND: -32601,
|
|
8
|
+
INVALID_PARAMS: -32602,
|
|
9
|
+
INTERNAL_ERROR: -32603,
|
|
10
|
+
// Custom A2A errors
|
|
11
|
+
TASK_NOT_FOUND: -32001,
|
|
12
|
+
SKILL_NOT_FOUND: -32002,
|
|
13
|
+
UNAUTHORIZED: -32003,
|
|
14
|
+
RATE_LIMITED: -32004,
|
|
15
|
+
TASK_CANCELED: -32005
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/time-executor.ts
|
|
19
|
+
import {
|
|
20
|
+
format,
|
|
21
|
+
addDays,
|
|
22
|
+
addWeeks,
|
|
23
|
+
addMonths,
|
|
24
|
+
addBusinessDays,
|
|
25
|
+
differenceInDays,
|
|
26
|
+
differenceInHours,
|
|
27
|
+
differenceInMinutes,
|
|
28
|
+
isWeekend,
|
|
29
|
+
isBefore,
|
|
30
|
+
parseISO
|
|
31
|
+
} from "date-fns";
|
|
32
|
+
import { formatInTimeZone } from "date-fns-tz";
|
|
33
|
+
function createResponse(result, source) {
|
|
34
|
+
return {
|
|
35
|
+
result,
|
|
36
|
+
computed: { method: "deterministic", source },
|
|
37
|
+
usage: "Use this value directly. Do not recalculate."
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function extractInput(message) {
|
|
41
|
+
for (const part of message.parts) {
|
|
42
|
+
if (part.type === "data" && part.data) {
|
|
43
|
+
return part.data;
|
|
44
|
+
}
|
|
45
|
+
if (part.type === "text" && part.text) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(part.text);
|
|
48
|
+
} catch {
|
|
49
|
+
return { expression: part.text };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
async function executeTimeSkill(context) {
|
|
56
|
+
const input = extractInput(context.message);
|
|
57
|
+
let response;
|
|
58
|
+
switch (context.skill.id) {
|
|
59
|
+
case "time/now": {
|
|
60
|
+
const tz = input.timezone || "UTC";
|
|
61
|
+
const now = /* @__PURE__ */ new Date();
|
|
62
|
+
response = createResponse(
|
|
63
|
+
{
|
|
64
|
+
timestamp: now.toISOString(),
|
|
65
|
+
timezone: tz,
|
|
66
|
+
formatted: formatInTimeZone(now, tz, "yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
67
|
+
human: formatInTimeZone(now, tz, "EEEE, MMMM d, yyyy h:mm a zzz"),
|
|
68
|
+
day: formatInTimeZone(now, tz, "EEEE"),
|
|
69
|
+
offset: formatInTimeZone(now, tz, "xxx")
|
|
70
|
+
},
|
|
71
|
+
"system_clock"
|
|
72
|
+
);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case "time/day_of_week": {
|
|
76
|
+
const dateStr = input.date;
|
|
77
|
+
if (!dateStr) throw new Error("Missing required field: date");
|
|
78
|
+
const date = parseISO(dateStr);
|
|
79
|
+
if (isNaN(date.getTime())) throw new Error(`Invalid date: ${dateStr}`);
|
|
80
|
+
response = createResponse(
|
|
81
|
+
{
|
|
82
|
+
date: dateStr,
|
|
83
|
+
day: format(date, "EEEE"),
|
|
84
|
+
// @snake-case-ok: A2A protocol response field
|
|
85
|
+
day_index: date.getDay(),
|
|
86
|
+
// @snake-case-ok: A2A protocol response field
|
|
87
|
+
is_weekend: isWeekend(date)
|
|
88
|
+
},
|
|
89
|
+
"date_arithmetic"
|
|
90
|
+
);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "time/add": {
|
|
94
|
+
const baseStr = input.date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
95
|
+
const base = parseISO(baseStr);
|
|
96
|
+
if (isNaN(base.getTime())) throw new Error(`Invalid date: ${baseStr}`);
|
|
97
|
+
let result = base;
|
|
98
|
+
if (input.days) result = addDays(result, input.days);
|
|
99
|
+
if (input.weeks) result = addWeeks(result, input.weeks);
|
|
100
|
+
if (input.months) result = addMonths(result, input.months);
|
|
101
|
+
response = createResponse(
|
|
102
|
+
{
|
|
103
|
+
original: baseStr,
|
|
104
|
+
computed: format(result, "yyyy-MM-dd"),
|
|
105
|
+
day: format(result, "EEEE"),
|
|
106
|
+
operation: { days: input.days, weeks: input.weeks, months: input.months }
|
|
107
|
+
},
|
|
108
|
+
"date_arithmetic"
|
|
109
|
+
);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "time/business_days": {
|
|
113
|
+
const baseStr = input.date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
114
|
+
const days = input.days;
|
|
115
|
+
if (typeof days !== "number") throw new Error("Missing required field: days");
|
|
116
|
+
const base = parseISO(baseStr);
|
|
117
|
+
if (isNaN(base.getTime())) throw new Error(`Invalid date: ${baseStr}`);
|
|
118
|
+
const result = addBusinessDays(base, days);
|
|
119
|
+
response = createResponse(
|
|
120
|
+
{
|
|
121
|
+
original: baseStr,
|
|
122
|
+
computed: format(result, "yyyy-MM-dd"),
|
|
123
|
+
day: format(result, "EEEE"),
|
|
124
|
+
// @snake-case-ok: A2A protocol response field
|
|
125
|
+
business_days_added: days
|
|
126
|
+
},
|
|
127
|
+
"date_arithmetic"
|
|
128
|
+
);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "time/diff": {
|
|
132
|
+
const from = parseISO(input.from);
|
|
133
|
+
const to = parseISO(input.to);
|
|
134
|
+
if (isNaN(from.getTime())) throw new Error(`Invalid from date: ${input.from}`);
|
|
135
|
+
if (isNaN(to.getTime())) throw new Error(`Invalid to date: ${input.to}`);
|
|
136
|
+
const days = differenceInDays(to, from);
|
|
137
|
+
const hours = differenceInHours(to, from);
|
|
138
|
+
const minutes = differenceInMinutes(to, from);
|
|
139
|
+
response = createResponse(
|
|
140
|
+
{
|
|
141
|
+
from: input.from,
|
|
142
|
+
to: input.to,
|
|
143
|
+
difference: { days, hours, minutes },
|
|
144
|
+
human: `${Math.abs(days)} days`
|
|
145
|
+
},
|
|
146
|
+
"date_arithmetic"
|
|
147
|
+
);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case "time/is_past": {
|
|
151
|
+
const ts = input.timestamp;
|
|
152
|
+
if (!ts) throw new Error("Missing required field: timestamp");
|
|
153
|
+
const check = parseISO(ts);
|
|
154
|
+
if (isNaN(check.getTime())) throw new Error(`Invalid timestamp: ${ts}`);
|
|
155
|
+
const now = /* @__PURE__ */ new Date();
|
|
156
|
+
response = createResponse(
|
|
157
|
+
{
|
|
158
|
+
timestamp: ts,
|
|
159
|
+
// @snake-case-ok: A2A protocol response field
|
|
160
|
+
is_past: isBefore(check, now),
|
|
161
|
+
// @snake-case-ok: A2A protocol response field
|
|
162
|
+
is_future: isBefore(now, check),
|
|
163
|
+
// @snake-case-ok: A2A protocol response field
|
|
164
|
+
checked_at: now.toISOString()
|
|
165
|
+
},
|
|
166
|
+
"system_clock"
|
|
167
|
+
);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case "time/convert": {
|
|
171
|
+
const ts = input.timestamp;
|
|
172
|
+
const toTz = input.to;
|
|
173
|
+
if (!ts) throw new Error("Missing required field: timestamp");
|
|
174
|
+
if (!toTz) throw new Error("Missing required field: to (timezone)");
|
|
175
|
+
const date = parseISO(ts);
|
|
176
|
+
if (isNaN(date.getTime())) throw new Error(`Invalid timestamp: ${ts}`);
|
|
177
|
+
response = createResponse(
|
|
178
|
+
{
|
|
179
|
+
original: ts,
|
|
180
|
+
converted: formatInTimeZone(date, toTz, "yyyy-MM-dd'T'HH:mm:ssXXX"),
|
|
181
|
+
timezone: toTz,
|
|
182
|
+
human: formatInTimeZone(date, toTz, "EEEE, MMMM d, yyyy h:mm a zzz")
|
|
183
|
+
},
|
|
184
|
+
"date_arithmetic"
|
|
185
|
+
);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case "time/parse": {
|
|
189
|
+
const expr = input.expression?.toLowerCase();
|
|
190
|
+
const tz = input.timezone || "UTC";
|
|
191
|
+
const ref = input.reference ? parseISO(input.reference) : /* @__PURE__ */ new Date();
|
|
192
|
+
if (!expr) throw new Error("Missing required field: expression");
|
|
193
|
+
let result = null;
|
|
194
|
+
if (expr === "today") {
|
|
195
|
+
result = ref;
|
|
196
|
+
} else if (expr === "tomorrow") {
|
|
197
|
+
result = addDays(ref, 1);
|
|
198
|
+
} else if (expr === "yesterday") {
|
|
199
|
+
result = addDays(ref, -1);
|
|
200
|
+
} else if (expr.startsWith("next ")) {
|
|
201
|
+
const dayName = expr.replace("next ", "").split(" ")[0];
|
|
202
|
+
const days = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
203
|
+
const targetDay = days.indexOf(dayName);
|
|
204
|
+
if (targetDay >= 0) {
|
|
205
|
+
let daysToAdd = targetDay - ref.getDay();
|
|
206
|
+
if (daysToAdd <= 0) daysToAdd += 7;
|
|
207
|
+
result = addDays(ref, daysToAdd);
|
|
208
|
+
}
|
|
209
|
+
} else if (expr.includes("in ") && expr.includes(" day")) {
|
|
210
|
+
const match = expr.match(/in (\d+) days?/);
|
|
211
|
+
if (match) result = addDays(ref, parseInt(match[1]));
|
|
212
|
+
} else if (expr.includes("in ") && expr.includes(" week")) {
|
|
213
|
+
const match = expr.match(/in (\d+) weeks?/);
|
|
214
|
+
if (match) result = addWeeks(ref, parseInt(match[1]));
|
|
215
|
+
}
|
|
216
|
+
if (!result) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Could not parse expression: "${expr}". Supported: today, tomorrow, yesterday, next [day], in N days/weeks`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
response = createResponse(
|
|
222
|
+
{
|
|
223
|
+
expression: input.expression,
|
|
224
|
+
resolved: format(result, "yyyy-MM-dd"),
|
|
225
|
+
day: format(result, "EEEE"),
|
|
226
|
+
timezone: tz,
|
|
227
|
+
// @snake-case-ok: A2A protocol response field
|
|
228
|
+
reference_used: ref.toISOString()
|
|
229
|
+
},
|
|
230
|
+
"date_arithmetic"
|
|
231
|
+
);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
default:
|
|
235
|
+
throw new Error(`Unknown time skill: ${context.skill.id}`);
|
|
236
|
+
}
|
|
237
|
+
const text = JSON.stringify(response, null, 2);
|
|
238
|
+
return {
|
|
239
|
+
text,
|
|
240
|
+
artifacts: [
|
|
241
|
+
{
|
|
242
|
+
name: "response",
|
|
243
|
+
parts: [{ type: "text", text }]
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/executor.ts
|
|
250
|
+
import { streamText, generateText, tool, jsonSchema, stepCountIs } from "ai";
|
|
251
|
+
import { createGateway } from "@ai-sdk/gateway";
|
|
252
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
253
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
254
|
+
var DEFAULT_SYSTEM_PROMPTS = {
|
|
255
|
+
chat: `You are a helpful AI assistant. Respond concisely and accurately to the user's questions.`,
|
|
256
|
+
analyze: `You are an analytical AI assistant. Analyze the provided input and give detailed insights.`,
|
|
257
|
+
summarize: `You are a summarization assistant. Provide clear, concise summaries of the provided content.`,
|
|
258
|
+
code: `You are a coding assistant. Help with code-related questions, provide examples, and explain concepts clearly.`
|
|
259
|
+
};
|
|
260
|
+
var CHAT_SKILL_ID = "chat";
|
|
261
|
+
var TOOL_STOP_CONDITION = stepCountIs(5);
|
|
262
|
+
var TOOL_TAG = "tool";
|
|
263
|
+
var LLM_TAG = "llm";
|
|
264
|
+
function extractTextFromMessage(message) {
|
|
265
|
+
return message.parts.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
|
|
266
|
+
}
|
|
267
|
+
function createLLMProvider(config) {
|
|
268
|
+
const gatewayKey = process.env.AI_GATEWAY_API_KEY;
|
|
269
|
+
if (gatewayKey) {
|
|
270
|
+
const gateway = createGateway({ apiKey: gatewayKey });
|
|
271
|
+
const modelId = config.gatewayModel || `${config.provider}/${config.model}`;
|
|
272
|
+
return gateway(modelId);
|
|
273
|
+
}
|
|
274
|
+
const apiKey = config.apiKey || process.env[`${config.provider.toUpperCase()}_API_KEY`];
|
|
275
|
+
if (!apiKey) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`API key not provided. Set AI_GATEWAY_API_KEY for Vercel AI Gateway, or ${config.provider.toUpperCase()}_API_KEY for direct provider access.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
switch (config.provider) {
|
|
281
|
+
case "openai":
|
|
282
|
+
return createOpenAI({ apiKey })(config.model);
|
|
283
|
+
case "anthropic":
|
|
284
|
+
return createAnthropic({ apiKey })(config.model);
|
|
285
|
+
default:
|
|
286
|
+
throw new Error(`Unsupported LLM provider: ${config.provider}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function getSystemPrompt(skill) {
|
|
290
|
+
if (skill.systemPrompt) {
|
|
291
|
+
return skill.systemPrompt;
|
|
292
|
+
}
|
|
293
|
+
return DEFAULT_SYSTEM_PROMPTS[skill.id] || DEFAULT_SYSTEM_PROMPTS.chat;
|
|
294
|
+
}
|
|
295
|
+
function sanitizeToolName(skillId) {
|
|
296
|
+
let name = skillId.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase();
|
|
297
|
+
if (!name) {
|
|
298
|
+
name = "tool";
|
|
299
|
+
}
|
|
300
|
+
if (/^[0-9]/.test(name)) {
|
|
301
|
+
name = `tool_${name}`;
|
|
302
|
+
}
|
|
303
|
+
return name;
|
|
304
|
+
}
|
|
305
|
+
function makeUniqueToolName(skillId, usedNames) {
|
|
306
|
+
const baseName = sanitizeToolName(skillId);
|
|
307
|
+
if (!usedNames.has(baseName)) {
|
|
308
|
+
usedNames.add(baseName);
|
|
309
|
+
return baseName;
|
|
310
|
+
}
|
|
311
|
+
let counter = 1;
|
|
312
|
+
let candidate = `${baseName}_${counter}`;
|
|
313
|
+
while (usedNames.has(candidate)) {
|
|
314
|
+
counter += 1;
|
|
315
|
+
candidate = `${baseName}_${counter}`;
|
|
316
|
+
}
|
|
317
|
+
usedNames.add(candidate);
|
|
318
|
+
return candidate;
|
|
319
|
+
}
|
|
320
|
+
function toToolInput(params) {
|
|
321
|
+
if (params && typeof params === "object" && !Array.isArray(params)) {
|
|
322
|
+
return params;
|
|
323
|
+
}
|
|
324
|
+
return { input: params };
|
|
325
|
+
}
|
|
326
|
+
function parseToolResult(text) {
|
|
327
|
+
try {
|
|
328
|
+
return JSON.parse(text);
|
|
329
|
+
} catch {
|
|
330
|
+
return { text };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
async function executeCatalogToolSkill(parentContext, skill, params) {
|
|
334
|
+
if (skill.id.startsWith("time/")) {
|
|
335
|
+
const toolContext = {
|
|
336
|
+
taskId: `${parentContext.taskId}_tool_${skill.id}`,
|
|
337
|
+
contextId: parentContext.contextId,
|
|
338
|
+
skill,
|
|
339
|
+
message: {
|
|
340
|
+
role: "user",
|
|
341
|
+
parts: [{ type: "data", data: toToolInput(params) }]
|
|
342
|
+
},
|
|
343
|
+
metadata: parentContext.metadata
|
|
344
|
+
};
|
|
345
|
+
const result = await executeTimeSkill(toolContext);
|
|
346
|
+
return parseToolResult(result.text);
|
|
347
|
+
}
|
|
348
|
+
throw new Error(`Unsupported tool skill: ${skill.id}`);
|
|
349
|
+
}
|
|
350
|
+
function buildChatTools(context, skillsCatalog) {
|
|
351
|
+
if (context.skill.id !== CHAT_SKILL_ID || !skillsCatalog || skillsCatalog.length === 0) {
|
|
352
|
+
return {};
|
|
353
|
+
}
|
|
354
|
+
const tools = {};
|
|
355
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
356
|
+
for (const skill of skillsCatalog) {
|
|
357
|
+
if (skill.id === context.skill.id) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const tags = skill.tags ?? [];
|
|
361
|
+
if (!tags.includes(TOOL_TAG) || tags.includes(LLM_TAG)) {
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (!skill.id.startsWith("time/")) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const toolName = makeUniqueToolName(skill.id, usedNames);
|
|
368
|
+
const schema = skill.inputSchema ?? {
|
|
369
|
+
type: "object",
|
|
370
|
+
properties: {},
|
|
371
|
+
additionalProperties: true
|
|
372
|
+
};
|
|
373
|
+
tools[toolName] = tool({
|
|
374
|
+
description: skill.description || `Execute ${skill.name}`,
|
|
375
|
+
inputSchema: jsonSchema(schema),
|
|
376
|
+
execute: async (params) => executeCatalogToolSkill(context, skill, params)
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return tools;
|
|
380
|
+
}
|
|
381
|
+
async function executeTask(context, llmConfig, skillsCatalog) {
|
|
382
|
+
const model = createLLMProvider(llmConfig);
|
|
383
|
+
const userMessage = extractTextFromMessage(context.message);
|
|
384
|
+
const systemPrompt = getSystemPrompt(context.skill);
|
|
385
|
+
const chatTools = buildChatTools(context, skillsCatalog);
|
|
386
|
+
const hasChatTools = Object.keys(chatTools).length > 0;
|
|
387
|
+
const result = await generateText({
|
|
388
|
+
model,
|
|
389
|
+
system: systemPrompt,
|
|
390
|
+
prompt: userMessage,
|
|
391
|
+
temperature: llmConfig.temperature ?? 0.7,
|
|
392
|
+
maxOutputTokens: llmConfig.maxOutputTokens ?? 4096,
|
|
393
|
+
...hasChatTools ? { tools: chatTools, stopWhen: TOOL_STOP_CONDITION } : {}
|
|
394
|
+
});
|
|
395
|
+
return {
|
|
396
|
+
text: result.text,
|
|
397
|
+
artifacts: [
|
|
398
|
+
{
|
|
399
|
+
name: "response",
|
|
400
|
+
parts: [{ type: "text", text: result.text }]
|
|
401
|
+
}
|
|
402
|
+
]
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
async function executeTaskStreaming(context, llmConfig, callbacks, skillsCatalog) {
|
|
406
|
+
const model = createLLMProvider(llmConfig);
|
|
407
|
+
const userMessage = extractTextFromMessage(context.message);
|
|
408
|
+
const systemPrompt = getSystemPrompt(context.skill);
|
|
409
|
+
const chatTools = buildChatTools(context, skillsCatalog);
|
|
410
|
+
const hasChatTools = Object.keys(chatTools).length > 0;
|
|
411
|
+
const result = streamText({
|
|
412
|
+
model,
|
|
413
|
+
system: systemPrompt,
|
|
414
|
+
prompt: userMessage,
|
|
415
|
+
temperature: llmConfig.temperature ?? 0.7,
|
|
416
|
+
maxOutputTokens: llmConfig.maxOutputTokens ?? 4096,
|
|
417
|
+
...hasChatTools ? { tools: chatTools, stopWhen: TOOL_STOP_CONDITION } : {},
|
|
418
|
+
onError: async ({ error }) => {
|
|
419
|
+
await callbacks.onError(error instanceof Error ? error : new Error(String(error)));
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
for await (const chunk of result.textStream) {
|
|
423
|
+
await callbacks.onChunk(chunk);
|
|
424
|
+
}
|
|
425
|
+
await callbacks.onComplete();
|
|
426
|
+
}
|
|
427
|
+
async function executeEcho(context) {
|
|
428
|
+
const userMessage = extractTextFromMessage(context.message);
|
|
429
|
+
return {
|
|
430
|
+
text: `Echo from ${context.skill.name}: ${userMessage}`,
|
|
431
|
+
artifacts: [
|
|
432
|
+
{
|
|
433
|
+
name: "response",
|
|
434
|
+
parts: [{ type: "text", text: `Echo from ${context.skill.name}: ${userMessage}` }]
|
|
435
|
+
}
|
|
436
|
+
]
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
async function executeEchoStreaming(context, callbacks) {
|
|
440
|
+
const userMessage = extractTextFromMessage(context.message);
|
|
441
|
+
const response = `Echo from ${context.skill.name}: ${userMessage}`;
|
|
442
|
+
try {
|
|
443
|
+
const words = response.split(" ");
|
|
444
|
+
for (let i = 0; i < words.length; i++) {
|
|
445
|
+
const chunk = (i === 0 ? "" : " ") + words[i];
|
|
446
|
+
await callbacks.onChunk(chunk);
|
|
447
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
448
|
+
}
|
|
449
|
+
await callbacks.onComplete();
|
|
450
|
+
} catch (error) {
|
|
451
|
+
await callbacks.onError(error instanceof Error ? error : new Error(String(error)));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/default-skills.ts
|
|
456
|
+
var DEFAULT_SKILLS = [
|
|
457
|
+
// Time tools (deterministic — no LLM)
|
|
458
|
+
{
|
|
459
|
+
id: "time/now",
|
|
460
|
+
name: "Current Time",
|
|
461
|
+
description: "Returns current UTC time with optional timezone conversion",
|
|
462
|
+
inputSchema: {
|
|
463
|
+
type: "object",
|
|
464
|
+
properties: {
|
|
465
|
+
timezone: { type: "string", description: "IANA timezone name (e.g. America/New_York)" }
|
|
466
|
+
},
|
|
467
|
+
additionalProperties: false
|
|
468
|
+
},
|
|
469
|
+
tags: ["deterministic", "time", "tool"]
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
id: "time/parse",
|
|
473
|
+
name: "Parse Date Expression",
|
|
474
|
+
description: 'Converts "next Tuesday at 3pm" to ISO timestamp',
|
|
475
|
+
inputSchema: {
|
|
476
|
+
type: "object",
|
|
477
|
+
properties: {
|
|
478
|
+
expression: { type: "string", description: "Natural language date expression" },
|
|
479
|
+
timezone: { type: "string", description: "IANA timezone name (default: UTC)" },
|
|
480
|
+
reference: { type: "string", description: "Reference ISO timestamp", format: "date-time" }
|
|
481
|
+
},
|
|
482
|
+
required: ["expression"],
|
|
483
|
+
additionalProperties: false
|
|
484
|
+
},
|
|
485
|
+
tags: ["deterministic", "time", "tool"]
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
id: "time/convert",
|
|
489
|
+
name: "Convert Timezone",
|
|
490
|
+
description: "Converts a timestamp between timezones",
|
|
491
|
+
inputSchema: {
|
|
492
|
+
type: "object",
|
|
493
|
+
properties: {
|
|
494
|
+
timestamp: { type: "string", description: "ISO timestamp to convert", format: "date-time" },
|
|
495
|
+
to: { type: "string", description: "Target IANA timezone name" }
|
|
496
|
+
},
|
|
497
|
+
required: ["timestamp", "to"],
|
|
498
|
+
additionalProperties: false
|
|
499
|
+
},
|
|
500
|
+
tags: ["deterministic", "time", "tool"]
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
id: "time/add",
|
|
504
|
+
name: "Date Arithmetic",
|
|
505
|
+
description: "Add days, weeks, or months to a date",
|
|
506
|
+
inputSchema: {
|
|
507
|
+
type: "object",
|
|
508
|
+
properties: {
|
|
509
|
+
date: { type: "string", description: "Base ISO date (defaults to today)" },
|
|
510
|
+
days: { type: "number", description: "Days to add (can be negative)" },
|
|
511
|
+
weeks: { type: "number", description: "Weeks to add (can be negative)" },
|
|
512
|
+
months: { type: "number", description: "Months to add (can be negative)" }
|
|
513
|
+
},
|
|
514
|
+
additionalProperties: false
|
|
515
|
+
},
|
|
516
|
+
tags: ["deterministic", "time", "tool"]
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
id: "time/diff",
|
|
520
|
+
name: "Date Difference",
|
|
521
|
+
description: "Calculate duration between two dates",
|
|
522
|
+
inputSchema: {
|
|
523
|
+
type: "object",
|
|
524
|
+
properties: {
|
|
525
|
+
from: { type: "string", description: "Start ISO timestamp", format: "date-time" },
|
|
526
|
+
to: { type: "string", description: "End ISO timestamp", format: "date-time" }
|
|
527
|
+
},
|
|
528
|
+
required: ["from", "to"],
|
|
529
|
+
additionalProperties: false
|
|
530
|
+
},
|
|
531
|
+
tags: ["deterministic", "time", "tool"]
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
id: "time/day_of_week",
|
|
535
|
+
name: "Day of Week",
|
|
536
|
+
description: "Returns the day of the week for a given date",
|
|
537
|
+
inputSchema: {
|
|
538
|
+
type: "object",
|
|
539
|
+
properties: {
|
|
540
|
+
date: { type: "string", description: "ISO date string" }
|
|
541
|
+
},
|
|
542
|
+
required: ["date"],
|
|
543
|
+
additionalProperties: false
|
|
544
|
+
},
|
|
545
|
+
tags: ["deterministic", "time", "tool"]
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
id: "time/is_past",
|
|
549
|
+
name: "Is Past",
|
|
550
|
+
description: "Checks whether a timestamp is in the past",
|
|
551
|
+
inputSchema: {
|
|
552
|
+
type: "object",
|
|
553
|
+
properties: {
|
|
554
|
+
timestamp: { type: "string", description: "ISO timestamp to check", format: "date-time" }
|
|
555
|
+
},
|
|
556
|
+
required: ["timestamp"],
|
|
557
|
+
additionalProperties: false
|
|
558
|
+
},
|
|
559
|
+
tags: ["deterministic", "time", "tool"]
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
id: "time/business_days",
|
|
563
|
+
name: "Business Days",
|
|
564
|
+
description: "Add or subtract business days from a date",
|
|
565
|
+
inputSchema: {
|
|
566
|
+
type: "object",
|
|
567
|
+
properties: {
|
|
568
|
+
date: { type: "string", description: "Base ISO date (defaults to today)" },
|
|
569
|
+
days: { type: "number", description: "Business days to add/subtract" }
|
|
570
|
+
},
|
|
571
|
+
required: ["days"],
|
|
572
|
+
additionalProperties: false
|
|
573
|
+
},
|
|
574
|
+
tags: ["deterministic", "time", "tool"]
|
|
575
|
+
},
|
|
576
|
+
// LLM-powered skills
|
|
577
|
+
{
|
|
578
|
+
id: "chat",
|
|
579
|
+
name: "Chat",
|
|
580
|
+
description: "General conversational AI assistant",
|
|
581
|
+
systemPrompt: "You are a helpful AI assistant. Respond concisely and accurately.",
|
|
582
|
+
tags: ["llm"]
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
id: "echo",
|
|
586
|
+
name: "Echo",
|
|
587
|
+
description: "Echoes back the input (for testing)",
|
|
588
|
+
tags: ["testing"]
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
id: "analyze",
|
|
592
|
+
name: "Analyze",
|
|
593
|
+
description: "Analyzes provided content and gives insights",
|
|
594
|
+
systemPrompt: "You are an analytical AI. Analyze the input and provide detailed insights.",
|
|
595
|
+
tags: ["llm"]
|
|
596
|
+
}
|
|
597
|
+
];
|
|
598
|
+
var DEFAULT_LLM_CONFIG = {
|
|
599
|
+
provider: "openai",
|
|
600
|
+
model: "gpt-5-nano",
|
|
601
|
+
gatewayModel: "openai/gpt-5-nano",
|
|
602
|
+
temperature: 0.7,
|
|
603
|
+
maxOutputTokens: 4096
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// src/server.ts
|
|
607
|
+
import express from "express";
|
|
608
|
+
import { v4 as uuidv4 } from "uuid";
|
|
609
|
+
var A2A_PROTOCOL_VERSION = "0.3";
|
|
610
|
+
var tasks = /* @__PURE__ */ new Map();
|
|
611
|
+
function generateTaskId() {
|
|
612
|
+
return `task_${Date.now()}_${uuidv4().substring(0, 8)}`;
|
|
613
|
+
}
|
|
614
|
+
function jsonRpcSuccess(id, result) {
|
|
615
|
+
return { jsonrpc: "2.0", id, result };
|
|
616
|
+
}
|
|
617
|
+
function jsonRpcError(id, code, message, data) {
|
|
618
|
+
return { jsonrpc: "2.0", id, error: { code, message, data } };
|
|
619
|
+
}
|
|
620
|
+
function createA2AServer(options) {
|
|
621
|
+
const { config, llmConfig = DEFAULT_LLM_CONFIG, echoMode = false } = options;
|
|
622
|
+
const app = express();
|
|
623
|
+
app.use(express.json());
|
|
624
|
+
app.use((_req, res, next) => {
|
|
625
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
626
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
627
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key");
|
|
628
|
+
next();
|
|
629
|
+
});
|
|
630
|
+
app.options("*", (_req, res) => {
|
|
631
|
+
res.sendStatus(204);
|
|
632
|
+
});
|
|
633
|
+
const port = config.port || 9999;
|
|
634
|
+
const host = config.host || "localhost";
|
|
635
|
+
const skills = config.skills || DEFAULT_SKILLS;
|
|
636
|
+
const agentCard = {
|
|
637
|
+
name: config.name,
|
|
638
|
+
description: config.description || `A2A Agent: ${config.name}`,
|
|
639
|
+
url: `http://${host}:${port}/a2a`,
|
|
640
|
+
version: config.version || "1.0.0",
|
|
641
|
+
protocolVersion: A2A_PROTOCOL_VERSION,
|
|
642
|
+
defaultInputModes: config.defaultInputModes || ["text", "data"],
|
|
643
|
+
defaultOutputModes: config.defaultOutputModes || ["text", "data"],
|
|
644
|
+
provider: config.provider || {
|
|
645
|
+
organization: "Runtype",
|
|
646
|
+
url: "https://runtype.com"
|
|
647
|
+
},
|
|
648
|
+
capabilities: {
|
|
649
|
+
streaming: true,
|
|
650
|
+
pushNotifications: false,
|
|
651
|
+
statefulness: "task"
|
|
652
|
+
},
|
|
653
|
+
skills: skills.map((s) => ({
|
|
654
|
+
id: s.id,
|
|
655
|
+
name: s.name,
|
|
656
|
+
description: s.description,
|
|
657
|
+
inputModes: ["text", "data"],
|
|
658
|
+
outputModes: ["text", "data"],
|
|
659
|
+
tags: s.tags ?? [],
|
|
660
|
+
inputSchema: s.inputSchema
|
|
661
|
+
})),
|
|
662
|
+
authentication: {
|
|
663
|
+
type: "none"
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
app.get("/.well-known/agent.json", (_req, res) => {
|
|
667
|
+
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
668
|
+
res.json(agentCard);
|
|
669
|
+
});
|
|
670
|
+
app.get("/health", (_req, res) => {
|
|
671
|
+
res.json({ status: "healthy", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
672
|
+
});
|
|
673
|
+
app.post("/a2a", async (req, res) => {
|
|
674
|
+
const body = req.body;
|
|
675
|
+
if (body.jsonrpc !== "2.0" || !body.method) {
|
|
676
|
+
res.json(jsonRpcError(body?.id, JSON_RPC_ERRORS.INVALID_REQUEST, "Invalid JSON-RPC request"));
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const { id, method, params } = body;
|
|
680
|
+
console.log(`[A2A] ${method}`, params ? JSON.stringify(params).substring(0, 100) : "");
|
|
681
|
+
try {
|
|
682
|
+
switch (method) {
|
|
683
|
+
case "tasks/send":
|
|
684
|
+
await handleTasksSend(req, res, id, params, skills, llmConfig, echoMode);
|
|
685
|
+
break;
|
|
686
|
+
case "tasks/sendSubscribe":
|
|
687
|
+
await handleTasksSendSubscribe(req, res, id, params, skills, llmConfig, echoMode);
|
|
688
|
+
break;
|
|
689
|
+
case "tasks/get":
|
|
690
|
+
handleTasksGet(res, id, params);
|
|
691
|
+
break;
|
|
692
|
+
case "tasks/cancel":
|
|
693
|
+
handleTasksCancel(res, id, params);
|
|
694
|
+
break;
|
|
695
|
+
case "ping":
|
|
696
|
+
res.json(jsonRpcSuccess(id, { pong: true }));
|
|
697
|
+
break;
|
|
698
|
+
default:
|
|
699
|
+
res.json(jsonRpcError(id, JSON_RPC_ERRORS.METHOD_NOT_FOUND, `Method not found: ${method}`));
|
|
700
|
+
}
|
|
701
|
+
} catch (error) {
|
|
702
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
703
|
+
console.error(`[A2A] Error in ${method}:`, errorMessage);
|
|
704
|
+
res.json(jsonRpcError(id, JSON_RPC_ERRORS.INTERNAL_ERROR, errorMessage));
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
let server = null;
|
|
708
|
+
return {
|
|
709
|
+
app,
|
|
710
|
+
start: () => new Promise((resolve) => {
|
|
711
|
+
server = app.listen(port, () => {
|
|
712
|
+
console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
713
|
+
console.log(`A2A Agent Server: ${config.name}`);
|
|
714
|
+
console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
715
|
+
console.log("");
|
|
716
|
+
console.log(` Agent Card: http://${host}:${port}/.well-known/agent.json`);
|
|
717
|
+
console.log(` A2A Endpoint: http://${host}:${port}/a2a`);
|
|
718
|
+
console.log(` Health Check: http://${host}:${port}/health`);
|
|
719
|
+
console.log("");
|
|
720
|
+
console.log(" Skills:");
|
|
721
|
+
for (const skill of skills) {
|
|
722
|
+
console.log(` - ${skill.id}: ${skill.description}`);
|
|
723
|
+
}
|
|
724
|
+
console.log("");
|
|
725
|
+
console.log(` Mode: ${echoMode ? "Echo (testing)" : `LLM (${llmConfig.provider}/${llmConfig.model})`}`);
|
|
726
|
+
console.log("");
|
|
727
|
+
console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
728
|
+
console.log("");
|
|
729
|
+
resolve();
|
|
730
|
+
});
|
|
731
|
+
}),
|
|
732
|
+
stop: () => new Promise((resolve, reject) => {
|
|
733
|
+
if (server) {
|
|
734
|
+
server.close((err) => {
|
|
735
|
+
if (err) reject(err);
|
|
736
|
+
else resolve();
|
|
737
|
+
});
|
|
738
|
+
} else {
|
|
739
|
+
resolve();
|
|
740
|
+
}
|
|
741
|
+
})
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
async function handleTasksSend(_req, res, id, params, skills, llmConfig, echoMode) {
|
|
745
|
+
const { skill: skillId, message, contextId, metadata } = params;
|
|
746
|
+
const skill = skills.find((s) => s.id === skillId);
|
|
747
|
+
if (!skill) {
|
|
748
|
+
res.json(
|
|
749
|
+
jsonRpcError(id, JSON_RPC_ERRORS.SKILL_NOT_FOUND, `Skill not found: ${skillId}`, {
|
|
750
|
+
availableSkills: skills.map((s) => s.id)
|
|
751
|
+
})
|
|
752
|
+
);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const taskId = generateTaskId();
|
|
756
|
+
const task = {
|
|
757
|
+
id: taskId,
|
|
758
|
+
contextId,
|
|
759
|
+
status: "submitted",
|
|
760
|
+
skillId,
|
|
761
|
+
requestMessage: message,
|
|
762
|
+
artifacts: [],
|
|
763
|
+
metadata,
|
|
764
|
+
history: [{ status: "submitted", message: "Task created", timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
|
|
765
|
+
};
|
|
766
|
+
tasks.set(taskId, task);
|
|
767
|
+
task.status = "working";
|
|
768
|
+
task.history.push({ status: "working", message: "Execution started", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
769
|
+
try {
|
|
770
|
+
const context = {
|
|
771
|
+
taskId,
|
|
772
|
+
contextId,
|
|
773
|
+
skill,
|
|
774
|
+
message,
|
|
775
|
+
metadata
|
|
776
|
+
};
|
|
777
|
+
let result;
|
|
778
|
+
if (skillId.startsWith("time/")) {
|
|
779
|
+
result = await executeTimeSkill(context);
|
|
780
|
+
} else if (echoMode || skillId === "echo") {
|
|
781
|
+
result = await executeEcho(context);
|
|
782
|
+
} else {
|
|
783
|
+
result = await executeTask(context, llmConfig, skills);
|
|
784
|
+
}
|
|
785
|
+
task.status = "completed";
|
|
786
|
+
task.artifacts = result.artifacts;
|
|
787
|
+
task.history.push({ status: "completed", message: "Task completed", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
788
|
+
res.json(
|
|
789
|
+
jsonRpcSuccess(id, {
|
|
790
|
+
task: {
|
|
791
|
+
id: taskId,
|
|
792
|
+
contextId,
|
|
793
|
+
status: "completed",
|
|
794
|
+
artifacts: task.artifacts,
|
|
795
|
+
metadata
|
|
796
|
+
}
|
|
797
|
+
})
|
|
798
|
+
);
|
|
799
|
+
} catch (error) {
|
|
800
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
801
|
+
task.status = "failed";
|
|
802
|
+
task.error = { message: errorMessage };
|
|
803
|
+
task.history.push({ status: "failed", message: errorMessage, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
804
|
+
res.json(jsonRpcError(id, JSON_RPC_ERRORS.INTERNAL_ERROR, `Task execution failed: ${errorMessage}`));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
async function handleTasksSendSubscribe(_req, res, id, params, skills, llmConfig, echoMode) {
|
|
808
|
+
const { skill: skillId, message, contextId, metadata } = params;
|
|
809
|
+
const skill = skills.find((s) => s.id === skillId);
|
|
810
|
+
if (!skill) {
|
|
811
|
+
res.json(
|
|
812
|
+
jsonRpcError(id, JSON_RPC_ERRORS.SKILL_NOT_FOUND, `Skill not found: ${skillId}`, {
|
|
813
|
+
availableSkills: skills.map((s) => s.id)
|
|
814
|
+
})
|
|
815
|
+
);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const taskId = generateTaskId();
|
|
819
|
+
const task = {
|
|
820
|
+
id: taskId,
|
|
821
|
+
contextId,
|
|
822
|
+
status: "submitted",
|
|
823
|
+
skillId,
|
|
824
|
+
requestMessage: message,
|
|
825
|
+
artifacts: [],
|
|
826
|
+
metadata,
|
|
827
|
+
history: [{ status: "submitted", message: "Task created", timestamp: (/* @__PURE__ */ new Date()).toISOString() }]
|
|
828
|
+
};
|
|
829
|
+
tasks.set(taskId, task);
|
|
830
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
831
|
+
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
832
|
+
res.setHeader("Connection", "keep-alive");
|
|
833
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
834
|
+
const sendEvent = (event, data) => {
|
|
835
|
+
res.write(`event: ${event}
|
|
836
|
+
data: ${JSON.stringify(data)}
|
|
837
|
+
|
|
838
|
+
`);
|
|
839
|
+
};
|
|
840
|
+
sendEvent("task/status", { taskId, contextId, status: "submitted" });
|
|
841
|
+
task.status = "working";
|
|
842
|
+
task.history.push({ status: "working", message: "Execution started", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
843
|
+
sendEvent("task/status", { taskId, contextId, status: "working" });
|
|
844
|
+
try {
|
|
845
|
+
const context = {
|
|
846
|
+
taskId,
|
|
847
|
+
contextId,
|
|
848
|
+
skill,
|
|
849
|
+
message,
|
|
850
|
+
metadata
|
|
851
|
+
};
|
|
852
|
+
let artifactIndex = 0;
|
|
853
|
+
let fullText = "";
|
|
854
|
+
const callbacks = {
|
|
855
|
+
onChunk: async (text) => {
|
|
856
|
+
fullText += text;
|
|
857
|
+
sendEvent("task/artifact", {
|
|
858
|
+
taskId,
|
|
859
|
+
artifact: {
|
|
860
|
+
name: "response",
|
|
861
|
+
parts: [{ type: "text", text }],
|
|
862
|
+
index: artifactIndex,
|
|
863
|
+
append: artifactIndex > 0,
|
|
864
|
+
lastChunk: false
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
artifactIndex++;
|
|
868
|
+
},
|
|
869
|
+
onComplete: async () => {
|
|
870
|
+
sendEvent("task/artifact", {
|
|
871
|
+
taskId,
|
|
872
|
+
artifact: {
|
|
873
|
+
name: "response",
|
|
874
|
+
parts: [{ type: "text", text: "" }],
|
|
875
|
+
index: artifactIndex,
|
|
876
|
+
append: true,
|
|
877
|
+
lastChunk: true
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
task.status = "completed";
|
|
881
|
+
task.artifacts = [{ name: "response", parts: [{ type: "text", text: fullText }] }];
|
|
882
|
+
task.history.push({ status: "completed", message: "Task completed", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
883
|
+
sendEvent("task/status", { taskId, contextId, status: "completed", final: true });
|
|
884
|
+
res.end();
|
|
885
|
+
},
|
|
886
|
+
onError: async (error) => {
|
|
887
|
+
task.status = "failed";
|
|
888
|
+
task.error = { message: error.message };
|
|
889
|
+
task.history.push({ status: "failed", message: error.message, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
890
|
+
sendEvent("task/error", { taskId, error: { code: -32603, message: error.message } });
|
|
891
|
+
sendEvent("task/status", { taskId, contextId, status: "failed", message: error.message, final: true });
|
|
892
|
+
res.end();
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
if (skillId.startsWith("time/")) {
|
|
896
|
+
const timeResult = await executeTimeSkill(context);
|
|
897
|
+
await callbacks.onChunk(timeResult.text);
|
|
898
|
+
await callbacks.onComplete();
|
|
899
|
+
} else if (echoMode || skillId === "echo") {
|
|
900
|
+
await executeEchoStreaming(context, callbacks);
|
|
901
|
+
} else {
|
|
902
|
+
await executeTaskStreaming(context, llmConfig, callbacks, skills);
|
|
903
|
+
}
|
|
904
|
+
} catch (error) {
|
|
905
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
906
|
+
task.status = "failed";
|
|
907
|
+
task.error = { message: errorMessage };
|
|
908
|
+
task.history.push({ status: "failed", message: errorMessage, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
909
|
+
sendEvent("task/error", { taskId, error: { code: -32603, message: errorMessage } });
|
|
910
|
+
sendEvent("task/status", { taskId, contextId, status: "failed", message: errorMessage, final: true });
|
|
911
|
+
res.end();
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
function handleTasksGet(res, id, params) {
|
|
915
|
+
const { taskId, includeHistory } = params;
|
|
916
|
+
const task = tasks.get(taskId);
|
|
917
|
+
if (!task) {
|
|
918
|
+
res.json(jsonRpcError(id, JSON_RPC_ERRORS.TASK_NOT_FOUND, `Task not found: ${taskId}`));
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const result = {
|
|
922
|
+
task: {
|
|
923
|
+
id: task.id,
|
|
924
|
+
contextId: task.contextId,
|
|
925
|
+
status: task.status,
|
|
926
|
+
artifacts: task.artifacts,
|
|
927
|
+
metadata: task.metadata
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
if (task.error) {
|
|
931
|
+
result.task.error = task.error;
|
|
932
|
+
}
|
|
933
|
+
if (includeHistory) {
|
|
934
|
+
result.task.history = task.history;
|
|
935
|
+
}
|
|
936
|
+
res.json(jsonRpcSuccess(id, result));
|
|
937
|
+
}
|
|
938
|
+
function handleTasksCancel(res, id, params) {
|
|
939
|
+
const { taskId, reason } = params;
|
|
940
|
+
const task = tasks.get(taskId);
|
|
941
|
+
if (!task) {
|
|
942
|
+
res.json(jsonRpcError(id, JSON_RPC_ERRORS.TASK_NOT_FOUND, `Task not found: ${taskId}`));
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (["completed", "failed", "canceled"].includes(task.status)) {
|
|
946
|
+
res.json(jsonRpcError(id, JSON_RPC_ERRORS.TASK_CANCELED, `Task cannot be canceled: already ${task.status}`));
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
task.status = "canceled";
|
|
950
|
+
task.history.push({
|
|
951
|
+
status: "canceled",
|
|
952
|
+
message: reason || "Canceled by request",
|
|
953
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
954
|
+
});
|
|
955
|
+
res.json(jsonRpcSuccess(id, { task: { id: taskId, status: "canceled" } }));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/client.ts
|
|
959
|
+
var A2AClient = class {
|
|
960
|
+
baseUrl;
|
|
961
|
+
rpcUrl;
|
|
962
|
+
agentCardUrl;
|
|
963
|
+
apiKey;
|
|
964
|
+
headers;
|
|
965
|
+
constructor(options) {
|
|
966
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
967
|
+
const isA2aBase = this.baseUrl.endsWith("/a2a");
|
|
968
|
+
this.rpcUrl = isA2aBase ? this.baseUrl : `${this.baseUrl}/a2a`;
|
|
969
|
+
this.agentCardUrl = `${this.baseUrl}/.well-known/agent.json`;
|
|
970
|
+
this.apiKey = options.apiKey;
|
|
971
|
+
this.headers = options.headers || {};
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Get the agent card for discovery
|
|
975
|
+
*/
|
|
976
|
+
async getAgentCard() {
|
|
977
|
+
const response = await fetch(this.agentCardUrl, {
|
|
978
|
+
headers: this.getHeaders()
|
|
979
|
+
});
|
|
980
|
+
if (!response.ok) {
|
|
981
|
+
throw new Error(`Failed to fetch agent card: ${response.status} ${response.statusText}`);
|
|
982
|
+
}
|
|
983
|
+
return response.json();
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Send a task and wait for completion (synchronous)
|
|
987
|
+
*/
|
|
988
|
+
async sendTask(params) {
|
|
989
|
+
const request = {
|
|
990
|
+
jsonrpc: "2.0",
|
|
991
|
+
id: `req_${Date.now()}`,
|
|
992
|
+
method: "tasks/send",
|
|
993
|
+
params
|
|
994
|
+
};
|
|
995
|
+
const response = await this.sendJsonRpc(request);
|
|
996
|
+
if (response.error) {
|
|
997
|
+
throw new Error(`A2A error: ${response.error.message} (code: ${response.error.code})`);
|
|
998
|
+
}
|
|
999
|
+
return response.result.task;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Send a task with streaming (SSE)
|
|
1003
|
+
*/
|
|
1004
|
+
async sendTaskStreaming(params, callbacks) {
|
|
1005
|
+
const request = {
|
|
1006
|
+
jsonrpc: "2.0",
|
|
1007
|
+
id: `req_${Date.now()}`,
|
|
1008
|
+
method: "tasks/sendSubscribe",
|
|
1009
|
+
params
|
|
1010
|
+
};
|
|
1011
|
+
const response = await fetch(`${this.baseUrl}/a2a`, {
|
|
1012
|
+
method: "POST",
|
|
1013
|
+
headers: {
|
|
1014
|
+
"Content-Type": "application/json",
|
|
1015
|
+
...this.getHeaders()
|
|
1016
|
+
},
|
|
1017
|
+
body: JSON.stringify(request)
|
|
1018
|
+
});
|
|
1019
|
+
if (!response.ok) {
|
|
1020
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1021
|
+
}
|
|
1022
|
+
const reader = response.body?.getReader();
|
|
1023
|
+
if (!reader) {
|
|
1024
|
+
throw new Error("No response body");
|
|
1025
|
+
}
|
|
1026
|
+
const decoder = new TextDecoder();
|
|
1027
|
+
let buffer = "";
|
|
1028
|
+
while (true) {
|
|
1029
|
+
const { done, value } = await reader.read();
|
|
1030
|
+
if (done) break;
|
|
1031
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1032
|
+
const lines = buffer.split("\n");
|
|
1033
|
+
buffer = lines.pop() || "";
|
|
1034
|
+
let currentEvent = "";
|
|
1035
|
+
for (const line of lines) {
|
|
1036
|
+
if (line.startsWith("event: ")) {
|
|
1037
|
+
currentEvent = line.substring(7);
|
|
1038
|
+
} else if (line.startsWith("data: ")) {
|
|
1039
|
+
const data = JSON.parse(line.substring(6));
|
|
1040
|
+
switch (currentEvent) {
|
|
1041
|
+
case "task/status":
|
|
1042
|
+
callbacks.onStatus?.(data.status, data);
|
|
1043
|
+
break;
|
|
1044
|
+
case "task/artifact":
|
|
1045
|
+
callbacks.onArtifact?.(data.artifact);
|
|
1046
|
+
if (data.artifact?.parts) {
|
|
1047
|
+
for (const part of data.artifact.parts) {
|
|
1048
|
+
if (part.type === "text" && part.text) {
|
|
1049
|
+
callbacks.onChunk?.(part.text);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
break;
|
|
1054
|
+
case "task/error":
|
|
1055
|
+
callbacks.onError?.(data.error);
|
|
1056
|
+
break;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Get task status
|
|
1064
|
+
*/
|
|
1065
|
+
async getTask(taskId, includeHistory = false) {
|
|
1066
|
+
const request = {
|
|
1067
|
+
jsonrpc: "2.0",
|
|
1068
|
+
id: `req_${Date.now()}`,
|
|
1069
|
+
method: "tasks/get",
|
|
1070
|
+
params: { taskId, includeHistory }
|
|
1071
|
+
};
|
|
1072
|
+
const response = await this.sendJsonRpc(request);
|
|
1073
|
+
if (response.error) {
|
|
1074
|
+
throw new Error(`A2A error: ${response.error.message} (code: ${response.error.code})`);
|
|
1075
|
+
}
|
|
1076
|
+
return response.result.task;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Cancel a task
|
|
1080
|
+
*/
|
|
1081
|
+
async cancelTask(taskId, reason) {
|
|
1082
|
+
const request = {
|
|
1083
|
+
jsonrpc: "2.0",
|
|
1084
|
+
id: `req_${Date.now()}`,
|
|
1085
|
+
method: "tasks/cancel",
|
|
1086
|
+
params: { taskId, reason }
|
|
1087
|
+
};
|
|
1088
|
+
const response = await this.sendJsonRpc(request);
|
|
1089
|
+
if (response.error) {
|
|
1090
|
+
throw new Error(`A2A error: ${response.error.message} (code: ${response.error.code})`);
|
|
1091
|
+
}
|
|
1092
|
+
return response.result.task;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Send a JSON-RPC request
|
|
1096
|
+
*/
|
|
1097
|
+
async sendJsonRpc(request) {
|
|
1098
|
+
const response = await fetch(this.rpcUrl, {
|
|
1099
|
+
method: "POST",
|
|
1100
|
+
headers: {
|
|
1101
|
+
"Content-Type": "application/json",
|
|
1102
|
+
...this.getHeaders()
|
|
1103
|
+
},
|
|
1104
|
+
body: JSON.stringify(request)
|
|
1105
|
+
});
|
|
1106
|
+
if (!response.ok) {
|
|
1107
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1108
|
+
}
|
|
1109
|
+
return response.json();
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Get headers for requests
|
|
1113
|
+
*/
|
|
1114
|
+
getHeaders() {
|
|
1115
|
+
const headers = { ...this.headers };
|
|
1116
|
+
if (this.apiKey) {
|
|
1117
|
+
if (this.apiKey.startsWith("a2a_")) {
|
|
1118
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1119
|
+
} else {
|
|
1120
|
+
headers["X-API-Key"] = this.apiKey;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return headers;
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
function createRuntypeA2AClient(options) {
|
|
1127
|
+
const { productId, surfaceId, apiKey, environment = "production" } = options;
|
|
1128
|
+
const baseUrls = {
|
|
1129
|
+
production: "https://api.runtype.com",
|
|
1130
|
+
staging: "https://api.runtype-staging.com",
|
|
1131
|
+
local: "http://localhost:8787"
|
|
1132
|
+
};
|
|
1133
|
+
const baseUrl = `${baseUrls[environment]}/v1/products/${productId}/surfaces/${surfaceId}/a2a`;
|
|
1134
|
+
return new A2AClient({
|
|
1135
|
+
baseUrl,
|
|
1136
|
+
apiKey
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
export {
|
|
1141
|
+
JSON_RPC_ERRORS,
|
|
1142
|
+
executeTimeSkill,
|
|
1143
|
+
executeTask,
|
|
1144
|
+
executeTaskStreaming,
|
|
1145
|
+
executeEcho,
|
|
1146
|
+
executeEchoStreaming,
|
|
1147
|
+
DEFAULT_SKILLS,
|
|
1148
|
+
DEFAULT_LLM_CONFIG,
|
|
1149
|
+
createA2AServer,
|
|
1150
|
+
A2AClient,
|
|
1151
|
+
createRuntypeA2AClient
|
|
1152
|
+
};
|
|
1153
|
+
//# sourceMappingURL=chunk-OLB7ZZNY.js.map
|