framework-agents 0.0.0-stage → 0.1.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/README.md +66 -2
- package/dist/agent-25ZNQBEB.js +6 -0
- package/dist/agent-ZWGIDKF6.js +7 -0
- package/dist/agent-ZWGIDKF6.js.map +1 -0
- package/dist/chunk-77OZE2QD.js +1811 -0
- package/dist/chunk-77OZE2QD.js.map +1 -0
- package/dist/chunk-LUGUQQLR.js +1033 -0
- package/dist/cli.js +1209 -0
- package/dist/index.d.ts +2842 -0
- package/dist/index.js +3951 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
Agent,
|
|
4
|
+
AgentError,
|
|
5
|
+
LocalFileStorage,
|
|
6
|
+
SessionManager,
|
|
7
|
+
filesystemMiddleware,
|
|
8
|
+
hasImages,
|
|
9
|
+
imagesOf,
|
|
10
|
+
todoListMiddleware,
|
|
11
|
+
wireTextOf
|
|
12
|
+
} from "./chunk-LUGUQQLR.js";
|
|
13
|
+
|
|
14
|
+
// src/serve/cli.ts
|
|
15
|
+
import { resolve as resolve3 } from "path";
|
|
16
|
+
|
|
17
|
+
// src/serve/index.ts
|
|
18
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
19
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
20
|
+
import { config as dotenvConfig } from "dotenv";
|
|
21
|
+
import { z as z4 } from "zod";
|
|
22
|
+
|
|
23
|
+
// src/config/index.ts
|
|
24
|
+
import { existsSync } from "fs";
|
|
25
|
+
import { dirname, join, resolve } from "path";
|
|
26
|
+
import { config } from "dotenv";
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
function loadEnv() {
|
|
29
|
+
if (process.env.FRAMEWORK_ENV_PATH) {
|
|
30
|
+
config({ path: process.env.FRAMEWORK_ENV_PATH });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
config();
|
|
34
|
+
let dir = resolve(process.cwd());
|
|
35
|
+
for (let i = 0; i < 3; i++) {
|
|
36
|
+
dir = dirname(dir);
|
|
37
|
+
const candidate = join(dir, ".env");
|
|
38
|
+
if (existsSync(candidate)) {
|
|
39
|
+
config({ path: candidate });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
loadEnv();
|
|
45
|
+
var ConfigSchema = z.object({
|
|
46
|
+
TOGETHER_API_KEY: z.string().default(""),
|
|
47
|
+
TOGETHER_BASE_URL: z.string().default("https://api.together.xyz/v1"),
|
|
48
|
+
/** Small chat default — one model, no tiers. */
|
|
49
|
+
TOGETHER_MODEL: z.string().default("Qwen/Qwen3.5-9B"),
|
|
50
|
+
OPENAI_BASE_URL: z.string().default("https://api.openai.com/v1"),
|
|
51
|
+
OPENAI_API_KEY: z.string().default(""),
|
|
52
|
+
OPENAI_MODEL: z.string().default("gpt-4o-mini"),
|
|
53
|
+
ANTHROPIC_API_KEY: z.string().default(""),
|
|
54
|
+
ANTHROPIC_BASE_URL: z.string().default("https://api.anthropic.com"),
|
|
55
|
+
ANTHROPIC_MODEL: z.string().default("claude-sonnet-4-6"),
|
|
56
|
+
OLLAMA_BASE_URL: z.string().default("http://localhost:11434/v1"),
|
|
57
|
+
OLLAMA_MODEL: z.string().default("qwen2.5"),
|
|
58
|
+
E2E_MODEL: z.string().default("")
|
|
59
|
+
});
|
|
60
|
+
var cached;
|
|
61
|
+
function loadConfig(env = process.env) {
|
|
62
|
+
if (cached) return cached;
|
|
63
|
+
const parsed = ConfigSchema.parse({
|
|
64
|
+
TOGETHER_API_KEY: env.TOGETHER_API_KEY ?? "",
|
|
65
|
+
TOGETHER_BASE_URL: env.TOGETHER_BASE_URL,
|
|
66
|
+
TOGETHER_MODEL: env.TOGETHER_MODEL,
|
|
67
|
+
OPENAI_BASE_URL: env.OPENAI_BASE_URL,
|
|
68
|
+
OPENAI_API_KEY: env.OPENAI_API_KEY ?? "",
|
|
69
|
+
OPENAI_MODEL: env.OPENAI_MODEL,
|
|
70
|
+
ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY ?? "",
|
|
71
|
+
ANTHROPIC_BASE_URL: env.ANTHROPIC_BASE_URL,
|
|
72
|
+
ANTHROPIC_MODEL: env.ANTHROPIC_MODEL,
|
|
73
|
+
OLLAMA_BASE_URL: env.OLLAMA_BASE_URL,
|
|
74
|
+
OLLAMA_MODEL: env.OLLAMA_MODEL,
|
|
75
|
+
E2E_MODEL: env.E2E_MODEL ?? ""
|
|
76
|
+
});
|
|
77
|
+
const useTogether = parsed.TOGETHER_API_KEY.length > 0;
|
|
78
|
+
const model = useTogether ? parsed.TOGETHER_MODEL : parsed.OPENAI_MODEL;
|
|
79
|
+
cached = {
|
|
80
|
+
...parsed,
|
|
81
|
+
active: useTogether ? { baseUrl: parsed.TOGETHER_BASE_URL, apiKey: parsed.TOGETHER_API_KEY, model, source: "together" } : { baseUrl: parsed.OPENAI_BASE_URL, apiKey: parsed.OPENAI_API_KEY, model, source: "openai" }
|
|
82
|
+
};
|
|
83
|
+
return cached;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/models/capabilities.ts
|
|
87
|
+
var VISION_CAPABLE_PATTERNS = [
|
|
88
|
+
/qwen2.*vl/i,
|
|
89
|
+
/qwen.*vl/i,
|
|
90
|
+
/llama-3\.2-11b-vision/i,
|
|
91
|
+
/llama-3\.2-90b-vision/i,
|
|
92
|
+
/llava/i,
|
|
93
|
+
/minicpm-v/i,
|
|
94
|
+
/gpt-4o/i,
|
|
95
|
+
/gpt-4\.1/i,
|
|
96
|
+
/claude.*sonnet/i,
|
|
97
|
+
/claude.*opus/i,
|
|
98
|
+
/gemini/i
|
|
99
|
+
];
|
|
100
|
+
function supportsVision(modelId) {
|
|
101
|
+
return VISION_CAPABLE_PATTERNS.some((re) => re.test(modelId));
|
|
102
|
+
}
|
|
103
|
+
function assertCanHandle(modelId, needsVision) {
|
|
104
|
+
if (needsVision && !supportsVision(modelId)) {
|
|
105
|
+
throw new AgentError(
|
|
106
|
+
"MODEL_CAPABILITY",
|
|
107
|
+
`Model "${modelId}" cannot take image input. Switch TOGETHER_MODEL to a vision-capable model (e.g. Qwen/Qwen2.5-VL-72B-Instruct) or drop the images.`,
|
|
108
|
+
false
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/models/openai-compatible.ts
|
|
114
|
+
function reasoningOf(usage) {
|
|
115
|
+
return usage.reasoning_tokens ?? usage.completion_tokens_details?.reasoning_tokens;
|
|
116
|
+
}
|
|
117
|
+
function thinkingOf(m) {
|
|
118
|
+
const trace = m.reasoning ?? m.reasoning_content;
|
|
119
|
+
return trace ? trace : void 0;
|
|
120
|
+
}
|
|
121
|
+
function toWireRole(role) {
|
|
122
|
+
if (role === "tool") return "tool";
|
|
123
|
+
return role;
|
|
124
|
+
}
|
|
125
|
+
function toWireContent(m) {
|
|
126
|
+
if (typeof m.content === "string" && !hasImages(m.content)) return m.content;
|
|
127
|
+
const text = wireTextOf(m.content);
|
|
128
|
+
const imgs = imagesOf(m.content);
|
|
129
|
+
if (!imgs.length) return text;
|
|
130
|
+
return [
|
|
131
|
+
...text ? [{ type: "text", text }] : [],
|
|
132
|
+
...imgs.map((img) => ({
|
|
133
|
+
type: "image_url",
|
|
134
|
+
image_url: { url: img.data ? `data:${img.mimeType ?? "image/jpeg"};base64,${img.data}` : img.url }
|
|
135
|
+
}))
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
function toWireMessage(m) {
|
|
139
|
+
const base = { role: toWireRole(m.role), content: toWireContent(m) };
|
|
140
|
+
if (m.toolCallId) base.tool_call_id = m.toolCallId;
|
|
141
|
+
if (typeof m.content !== "string") {
|
|
142
|
+
const calls = m.content.filter(
|
|
143
|
+
(b) => b.type === "tool_call"
|
|
144
|
+
);
|
|
145
|
+
if (calls.length) {
|
|
146
|
+
base.tool_calls = calls.map((c) => ({
|
|
147
|
+
id: c.id,
|
|
148
|
+
type: "function",
|
|
149
|
+
function: { name: c.name, arguments: JSON.stringify(c.args ?? {}) }
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return base;
|
|
154
|
+
}
|
|
155
|
+
var OpenAICompatibleProvider = class {
|
|
156
|
+
id;
|
|
157
|
+
capabilities = { structuredOutput: true, streaming: true };
|
|
158
|
+
opts;
|
|
159
|
+
constructor(opts) {
|
|
160
|
+
if (!opts.apiKey) throw new Error("OpenAICompatibleProvider: apiKey is required (.env TOGETHER_API_KEY / OPENAI_API_KEY)");
|
|
161
|
+
this.opts = { temperature: 0.2, ...opts };
|
|
162
|
+
this.id = `openai-compatible:${opts.model}`;
|
|
163
|
+
}
|
|
164
|
+
async complete(options) {
|
|
165
|
+
const { fetchFn = fetch, baseUrl, apiKey, model, temperature, maxTokens, stream } = this.opts;
|
|
166
|
+
const needsVision = options.messages.some((m) => hasImages(m.content));
|
|
167
|
+
assertCanHandle(model, needsVision);
|
|
168
|
+
const url = `${baseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
169
|
+
const body = {
|
|
170
|
+
model,
|
|
171
|
+
temperature,
|
|
172
|
+
...maxTokens ? { max_tokens: maxTokens } : {},
|
|
173
|
+
// stream_options makes OpenAI-compatible servers attach usage to the final
|
|
174
|
+
// chunk; without it streaming runs report {0,0} — budgets/cost would be fiction.
|
|
175
|
+
...stream ? { stream: true, stream_options: { include_usage: true } } : {},
|
|
176
|
+
...options.responseFormat ? {
|
|
177
|
+
response_format: {
|
|
178
|
+
type: "json_schema",
|
|
179
|
+
json_schema: {
|
|
180
|
+
name: options.responseFormat.name,
|
|
181
|
+
strict: options.responseFormat.strict ?? true,
|
|
182
|
+
schema: options.responseFormat.schema
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} : {},
|
|
186
|
+
messages: options.messages.map(toWireMessage),
|
|
187
|
+
...options.tools.length ? {
|
|
188
|
+
tools: options.tools.map((t) => ({
|
|
189
|
+
type: "function",
|
|
190
|
+
function: {
|
|
191
|
+
name: t.name,
|
|
192
|
+
description: t.description,
|
|
193
|
+
// Without parameters the API advertises a zero-arg tool.
|
|
194
|
+
...t.parametersSchema ? { parameters: t.parametersSchema } : {}
|
|
195
|
+
}
|
|
196
|
+
}))
|
|
197
|
+
} : {}
|
|
198
|
+
};
|
|
199
|
+
let res;
|
|
200
|
+
try {
|
|
201
|
+
res = await fetchFn(url, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
204
|
+
body: JSON.stringify(body),
|
|
205
|
+
...options.signal ? { signal: options.signal } : {}
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
throw new AgentError("MODEL_ERROR", `Model request failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
209
|
+
}
|
|
210
|
+
if (!res.ok) {
|
|
211
|
+
const text = await res.text().catch(() => "");
|
|
212
|
+
const retryable = res.status === 429 || res.status >= 500;
|
|
213
|
+
throw new AgentError("MODEL_ERROR", `Model HTTP ${res.status}: ${text.slice(0, 300)}`, retryable);
|
|
214
|
+
}
|
|
215
|
+
return stream ? this.readStream(res, options) : this.readJson(res, options);
|
|
216
|
+
}
|
|
217
|
+
async readJson(res, options) {
|
|
218
|
+
const json = await res.json();
|
|
219
|
+
const choice = json.choices[0];
|
|
220
|
+
const msg = choice?.message ?? {};
|
|
221
|
+
const thinking = thinkingOf(msg);
|
|
222
|
+
if (thinking) options.onThinking?.(thinking);
|
|
223
|
+
if (msg.content) options.onToken?.(msg.content);
|
|
224
|
+
const finishReason = choice?.finish_reason ?? void 0;
|
|
225
|
+
if (!msg.content && !(msg.tool_calls ?? []).length && thinking && finishReason === "stop") {
|
|
226
|
+
throw new AgentError(
|
|
227
|
+
"MODEL_ERROR",
|
|
228
|
+
"Model stopped with finish_reason=stop having put the entire answer in the reasoning channel (empty content, no tool calls, thinking present). The reasoning/content split lost the reply \u2014 retry or raise maxTokens.",
|
|
229
|
+
true
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (!msg.content && !(msg.tool_calls ?? []).length && finishReason === "length") {
|
|
233
|
+
throw new AgentError(
|
|
234
|
+
"MODEL_ERROR",
|
|
235
|
+
`Model spent its entire max_tokens budget (${json.usage?.completion_tokens ?? "?"} tokens, finish_reason=length) thinking without producing content. Raise maxTokens.`,
|
|
236
|
+
false
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const reasoningTokens = json.usage ? reasoningOf(json.usage) : void 0;
|
|
240
|
+
return {
|
|
241
|
+
text: msg.content ?? "",
|
|
242
|
+
...thinking ? { thinking } : {},
|
|
243
|
+
toolCalls: parseToolCalls(msg.tool_calls ?? []),
|
|
244
|
+
...finishReason ? { finishReason } : {},
|
|
245
|
+
usage: {
|
|
246
|
+
inputTokens: json.usage?.prompt_tokens ?? 0,
|
|
247
|
+
outputTokens: json.usage?.completion_tokens ?? 0,
|
|
248
|
+
...reasoningTokens != null ? { reasoningTokens } : {}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/** SSE: `data: {...}` lines; delta.content -> onToken, delta.reasoning_content -> onThinking. */
|
|
253
|
+
async readStream(res, options) {
|
|
254
|
+
if (!res.body) throw new Error("Streaming response has no body");
|
|
255
|
+
const reader = res.body.getReader();
|
|
256
|
+
const decoder = new TextDecoder();
|
|
257
|
+
let text = "";
|
|
258
|
+
let thinking = "";
|
|
259
|
+
let inputTokens = 0;
|
|
260
|
+
let outputTokens = 0;
|
|
261
|
+
let reasoningTokens;
|
|
262
|
+
let finishReason;
|
|
263
|
+
const toolArgBuffers = /* @__PURE__ */ new Map();
|
|
264
|
+
let buffer = "";
|
|
265
|
+
const flush = (chunk) => {
|
|
266
|
+
buffer += chunk;
|
|
267
|
+
const lines = buffer.split("\n");
|
|
268
|
+
buffer = lines.pop() ?? "";
|
|
269
|
+
for (const line of lines) {
|
|
270
|
+
const t = line.trim();
|
|
271
|
+
if (!t.startsWith("data:")) continue;
|
|
272
|
+
const payload = t.slice(5).trim();
|
|
273
|
+
if (payload === "[DONE]") continue;
|
|
274
|
+
let evt;
|
|
275
|
+
try {
|
|
276
|
+
evt = JSON.parse(payload);
|
|
277
|
+
} catch {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (evt.choices?.[0]?.finish_reason) finishReason = evt.choices[0].finish_reason ?? void 0;
|
|
281
|
+
if (evt.usage) {
|
|
282
|
+
if (evt.usage.prompt_tokens != null) inputTokens = evt.usage.prompt_tokens;
|
|
283
|
+
if (evt.usage.completion_tokens != null) outputTokens = evt.usage.completion_tokens;
|
|
284
|
+
const rt = reasoningOf(evt.usage);
|
|
285
|
+
if (rt != null) reasoningTokens = rt;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const delta = evt.choices?.[0]?.delta;
|
|
289
|
+
if (!delta) continue;
|
|
290
|
+
const think = thinkingOf(delta);
|
|
291
|
+
if (think) {
|
|
292
|
+
thinking += think;
|
|
293
|
+
options.onThinking?.(think);
|
|
294
|
+
}
|
|
295
|
+
if (delta.content) {
|
|
296
|
+
text += delta.content;
|
|
297
|
+
options.onToken?.(delta.content);
|
|
298
|
+
}
|
|
299
|
+
for (const [i, tc] of (delta.tool_calls ?? []).entries()) {
|
|
300
|
+
const key = tc.index ?? i;
|
|
301
|
+
const slot = toolArgBuffers.get(key) ?? { id: "", name: "", args: "" };
|
|
302
|
+
if (tc.id) slot.id = tc.id;
|
|
303
|
+
if (tc.function?.name) slot.name = tc.function.name;
|
|
304
|
+
if (tc.function?.arguments) slot.args += tc.function.arguments;
|
|
305
|
+
toolArgBuffers.set(key, slot);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
for (; ; ) {
|
|
310
|
+
const { done, value } = await reader.read();
|
|
311
|
+
if (done) break;
|
|
312
|
+
flush(decoder.decode(value, { stream: true }));
|
|
313
|
+
}
|
|
314
|
+
flush(decoder.decode());
|
|
315
|
+
if (!text && !toolArgBuffers.size && thinking && finishReason === "stop") {
|
|
316
|
+
throw new AgentError(
|
|
317
|
+
"MODEL_ERROR",
|
|
318
|
+
"Stream ended at finish_reason=stop having put the entire answer in the reasoning channel (no content, no tool calls, thinking present). The reasoning/content split lost the reply \u2014 retry or raise maxTokens.",
|
|
319
|
+
true
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
if (!text && !toolArgBuffers.size && finishReason === "length") {
|
|
323
|
+
throw new AgentError(
|
|
324
|
+
"MODEL_ERROR",
|
|
325
|
+
`Stream ended at finish_reason=length with no content (${outputTokens} tokens burned). Raise maxTokens.`,
|
|
326
|
+
false
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
text,
|
|
331
|
+
...thinking ? { thinking } : {},
|
|
332
|
+
...finishReason ? { finishReason } : {},
|
|
333
|
+
toolCalls: [...toolArgBuffers.values()].filter((t) => t.name).map((t) => ({ id: t.id || `call_${++callSeq}`, name: t.name, args: safeJson(t.args) })),
|
|
334
|
+
usage: {
|
|
335
|
+
inputTokens,
|
|
336
|
+
outputTokens,
|
|
337
|
+
...reasoningTokens != null ? { reasoningTokens } : {}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
var callSeq = 0;
|
|
343
|
+
function parseToolCalls(calls) {
|
|
344
|
+
return calls.map((c) => ({ id: c.id ?? `call_${++callSeq}`, name: c.function.name, args: safeJson(c.function.arguments || "{}") }));
|
|
345
|
+
}
|
|
346
|
+
function safeJson(raw) {
|
|
347
|
+
try {
|
|
348
|
+
return JSON.parse(raw || "{}");
|
|
349
|
+
} catch {
|
|
350
|
+
return {};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/models/anthropic.ts
|
|
355
|
+
function normalizeStopReason(stopReason) {
|
|
356
|
+
if (!stopReason) return void 0;
|
|
357
|
+
return stopReason === "max_tokens" ? "length" : stopReason;
|
|
358
|
+
}
|
|
359
|
+
function assertNotEmptyTruncation(text, toolCalls, finishReason, tokenCount) {
|
|
360
|
+
if (!text && !toolCalls.length && finishReason === "length") {
|
|
361
|
+
throw new AgentError(
|
|
362
|
+
"MODEL_ERROR",
|
|
363
|
+
`Model stopped at max_tokens (${tokenCount} tokens) without producing content. Raise maxTokens.`,
|
|
364
|
+
false
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function assertNotEmptyReasoningSplit(text, toolCalls, thinking, stopReason) {
|
|
369
|
+
if (!text && !toolCalls.length && thinking && stopReason === "stop") {
|
|
370
|
+
throw new AgentError(
|
|
371
|
+
"MODEL_ERROR",
|
|
372
|
+
"Model stopped with stop_reason=stop having put the entire answer in the reasoning channel (empty content, no tool calls, thinking present). The reasoning/content split lost the reply \u2014 retry or raise maxTokens.",
|
|
373
|
+
true
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function toAnthropicContent(m) {
|
|
378
|
+
if (typeof m.content === "string") return m.content;
|
|
379
|
+
const parts = [];
|
|
380
|
+
const text = wireTextOf(m.content);
|
|
381
|
+
if (text) parts.push({ type: "text", text });
|
|
382
|
+
for (const img of imagesOf(m.content)) {
|
|
383
|
+
parts.push({
|
|
384
|
+
type: "image",
|
|
385
|
+
source: img.data ? { type: "base64", media_type: img.mimeType ?? "image/jpeg", data: img.data } : { type: "url", url: img.url }
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
for (const b of m.content) {
|
|
389
|
+
if (b.type === "tool_call") parts.push({ type: "tool_use", id: b.id, name: b.name, input: b.args ?? {} });
|
|
390
|
+
}
|
|
391
|
+
return parts;
|
|
392
|
+
}
|
|
393
|
+
var AnthropicProvider = class {
|
|
394
|
+
id;
|
|
395
|
+
capabilities = { structuredOutput: false, streaming: true };
|
|
396
|
+
opts;
|
|
397
|
+
constructor(opts) {
|
|
398
|
+
if (!opts.apiKey) throw new Error("AnthropicProvider: apiKey is required (.env ANTHROPIC_API_KEY)");
|
|
399
|
+
this.opts = { baseUrl: "https://api.anthropic.com", maxTokens: 1024, ...opts };
|
|
400
|
+
if (this.opts.thinkingBudget && this.opts.maxTokens != null && this.opts.maxTokens <= this.opts.thinkingBudget) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`AnthropicProvider: maxTokens (${this.opts.maxTokens}) must exceed thinkingBudget (${this.opts.thinkingBudget}); raise maxTokens`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
this.id = `anthropic:${opts.model}`;
|
|
406
|
+
}
|
|
407
|
+
async complete(options) {
|
|
408
|
+
const { fetchFn = fetch, apiKey, model, baseUrl, maxTokens, temperature, thinkingBudget, stream } = this.opts;
|
|
409
|
+
const needsVision = options.messages.some((m) => hasImages(m.content));
|
|
410
|
+
assertCanHandle(model, needsVision);
|
|
411
|
+
const system = options.messages.filter((m) => m.role === "system").map((m) => wireTextOf(m.content)).join("\n\n");
|
|
412
|
+
const messages = [];
|
|
413
|
+
for (const m of options.messages.filter((x) => x.role !== "system")) {
|
|
414
|
+
if (m.role === "tool") {
|
|
415
|
+
const result = { type: "tool_result", tool_use_id: m.toolCallId, content: toAnthropicContent(m) };
|
|
416
|
+
const last = messages[messages.length - 1];
|
|
417
|
+
if (last && last.role === "user" && Array.isArray(last.content) && last.content.some((b) => b.type === "tool_result")) {
|
|
418
|
+
last.content.push(result);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
messages.push({ role: "user", content: [result] });
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
messages.push({ role: m.role === "assistant" ? "assistant" : "user", content: toAnthropicContent(m) });
|
|
425
|
+
}
|
|
426
|
+
const body = {
|
|
427
|
+
model,
|
|
428
|
+
max_tokens: maxTokens,
|
|
429
|
+
...system ? { system } : {},
|
|
430
|
+
...temperature != null ? { temperature } : {},
|
|
431
|
+
...thinkingBudget ? { thinking: { type: "enabled", budget_tokens: thinkingBudget } } : {},
|
|
432
|
+
...stream ? { stream: true } : {},
|
|
433
|
+
messages,
|
|
434
|
+
...options.tools.length ? {
|
|
435
|
+
tools: options.tools.map((t) => ({
|
|
436
|
+
name: t.name,
|
|
437
|
+
description: t.description,
|
|
438
|
+
input_schema: t.parametersSchema ?? { type: "object" }
|
|
439
|
+
}))
|
|
440
|
+
} : {}
|
|
441
|
+
};
|
|
442
|
+
let res;
|
|
443
|
+
try {
|
|
444
|
+
res = await fetchFn(`${baseUrl.replace(/\/$/, "")}/v1/messages`, {
|
|
445
|
+
method: "POST",
|
|
446
|
+
headers: {
|
|
447
|
+
"content-type": "application/json",
|
|
448
|
+
"x-api-key": apiKey,
|
|
449
|
+
"anthropic-version": "2023-06-01",
|
|
450
|
+
...stream ? { accept: "text/event-stream" } : {}
|
|
451
|
+
},
|
|
452
|
+
body: JSON.stringify(body),
|
|
453
|
+
...options.signal ? { signal: options.signal } : {}
|
|
454
|
+
});
|
|
455
|
+
} catch (err) {
|
|
456
|
+
throw new AgentError("MODEL_ERROR", `Model request failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
457
|
+
}
|
|
458
|
+
if (!res.ok) {
|
|
459
|
+
const text = await res.text().catch(() => "");
|
|
460
|
+
const retryable = res.status === 429 || res.status >= 500;
|
|
461
|
+
throw new AgentError("MODEL_ERROR", `Model HTTP ${res.status}: ${text.slice(0, 300)}`, retryable);
|
|
462
|
+
}
|
|
463
|
+
return stream ? this.readStream(res, options) : this.readJson(res, options);
|
|
464
|
+
}
|
|
465
|
+
toCompletion(blocks, usage, options) {
|
|
466
|
+
let text = "";
|
|
467
|
+
let thinking = "";
|
|
468
|
+
const toolCalls = [];
|
|
469
|
+
for (const b of blocks) {
|
|
470
|
+
if (b.type === "text" && b.text) {
|
|
471
|
+
text += b.text;
|
|
472
|
+
options.onToken?.(b.text);
|
|
473
|
+
} else if (b.type === "thinking" && b.thinking) {
|
|
474
|
+
thinking += b.thinking;
|
|
475
|
+
options.onThinking?.(b.thinking);
|
|
476
|
+
} else if (b.type === "tool_use" && b.name) {
|
|
477
|
+
toolCalls.push({ id: b.id ?? `call_${toolCalls.length}`, name: b.name, args: b.input ?? {} });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return { text, ...thinking ? { thinking } : {}, toolCalls, usage };
|
|
481
|
+
}
|
|
482
|
+
async readJson(res, options) {
|
|
483
|
+
const json = await res.json();
|
|
484
|
+
const out = this.toCompletion(json.content ?? [], {
|
|
485
|
+
inputTokens: json.usage?.input_tokens ?? 0,
|
|
486
|
+
outputTokens: json.usage?.output_tokens ?? 0
|
|
487
|
+
}, options);
|
|
488
|
+
const finishReason = normalizeStopReason(json.stop_reason);
|
|
489
|
+
assertNotEmptyTruncation(out.text, out.toolCalls, finishReason, json.usage?.output_tokens ?? 0);
|
|
490
|
+
assertNotEmptyReasoningSplit(out.text, out.toolCalls, out.thinking ?? "", finishReason);
|
|
491
|
+
return finishReason ? { ...out, finishReason } : out;
|
|
492
|
+
}
|
|
493
|
+
async readStream(res, options) {
|
|
494
|
+
if (!res.body) throw new Error("Streaming response has no body");
|
|
495
|
+
const reader = res.body.getReader();
|
|
496
|
+
const decoder = new TextDecoder();
|
|
497
|
+
let buffer = "";
|
|
498
|
+
let event = "";
|
|
499
|
+
let inputTokens = 0;
|
|
500
|
+
let outputTokens = 0;
|
|
501
|
+
let stopReason;
|
|
502
|
+
const blocks = [];
|
|
503
|
+
let current;
|
|
504
|
+
let jsonInput = "";
|
|
505
|
+
const flush = (chunk) => {
|
|
506
|
+
buffer += chunk;
|
|
507
|
+
const lines = buffer.split("\n");
|
|
508
|
+
buffer = lines.pop() ?? "";
|
|
509
|
+
for (const line of lines) {
|
|
510
|
+
const t = line.trim();
|
|
511
|
+
if (!t) continue;
|
|
512
|
+
if (t.startsWith("event:")) {
|
|
513
|
+
event = t.slice(6).trim();
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (!t.startsWith("data:")) continue;
|
|
517
|
+
const payload = t.slice(5).trim();
|
|
518
|
+
if (!payload) continue;
|
|
519
|
+
let data;
|
|
520
|
+
try {
|
|
521
|
+
data = JSON.parse(payload);
|
|
522
|
+
} catch {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (event === "message_start") {
|
|
526
|
+
const mu = (data.message ?? {}).usage;
|
|
527
|
+
if (mu?.input_tokens != null) inputTokens = mu.input_tokens;
|
|
528
|
+
} else if (event === "message_delta") {
|
|
529
|
+
const du = data.usage ?? {};
|
|
530
|
+
if (du.output_tokens != null) outputTokens = du.output_tokens;
|
|
531
|
+
const sr = data.delta ?? {};
|
|
532
|
+
if (sr.stop_reason) stopReason = normalizeStopReason(sr.stop_reason);
|
|
533
|
+
} else if (event === "content_block_start") {
|
|
534
|
+
const block = data.content_block ?? {};
|
|
535
|
+
current = { type: block.type, id: block.id, name: block.name };
|
|
536
|
+
jsonInput = "";
|
|
537
|
+
} else if (event === "content_block_delta") {
|
|
538
|
+
const delta = data.delta ?? {};
|
|
539
|
+
if (delta.text) {
|
|
540
|
+
current = current ?? { type: "text" };
|
|
541
|
+
current.text = (current.text ?? "") + delta.text;
|
|
542
|
+
} else if (delta.thinking) {
|
|
543
|
+
current = current ?? { type: "thinking" };
|
|
544
|
+
current.thinking = (current.thinking ?? "") + delta.thinking;
|
|
545
|
+
} else if (delta.partial_json) {
|
|
546
|
+
jsonInput += delta.partial_json;
|
|
547
|
+
}
|
|
548
|
+
} else if (event === "content_block_stop") {
|
|
549
|
+
if (current) {
|
|
550
|
+
if (current.type === "tool_use") {
|
|
551
|
+
try {
|
|
552
|
+
current.input = JSON.parse(jsonInput || "{}");
|
|
553
|
+
} catch {
|
|
554
|
+
current.input = {};
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
blocks.push(current);
|
|
558
|
+
current = void 0;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
for (; ; ) {
|
|
564
|
+
const { done, value } = await reader.read();
|
|
565
|
+
if (done) break;
|
|
566
|
+
flush(decoder.decode(value, { stream: true }));
|
|
567
|
+
}
|
|
568
|
+
flush(decoder.decode());
|
|
569
|
+
if (current) blocks.push(current);
|
|
570
|
+
const out = this.toCompletion(blocks, { inputTokens, outputTokens }, options);
|
|
571
|
+
assertNotEmptyTruncation(out.text, out.toolCalls, stopReason, outputTokens);
|
|
572
|
+
assertNotEmptyReasoningSplit(out.text, out.toolCalls, out.thinking ?? "", stopReason);
|
|
573
|
+
return stopReason ? { ...out, finishReason: stopReason } : out;
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// src/models/ollama.ts
|
|
578
|
+
var OllamaProvider = class extends OpenAICompatibleProvider {
|
|
579
|
+
id;
|
|
580
|
+
capabilities = { structuredOutput: false, streaming: true };
|
|
581
|
+
constructor(opts) {
|
|
582
|
+
super({
|
|
583
|
+
baseUrl: opts.baseUrl ?? "http://localhost:11434/v1",
|
|
584
|
+
apiKey: "ollama",
|
|
585
|
+
model: opts.model,
|
|
586
|
+
...opts.temperature != null ? { temperature: opts.temperature } : {},
|
|
587
|
+
...opts.maxTokens != null ? { maxTokens: opts.maxTokens } : {},
|
|
588
|
+
...opts.stream != null ? { stream: opts.stream } : {},
|
|
589
|
+
...opts.fetchFn ? { fetchFn: opts.fetchFn } : {}
|
|
590
|
+
});
|
|
591
|
+
this.id = `ollama:${opts.model}`;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// src/models/factory.ts
|
|
596
|
+
function initChatModel(spec, opts = {}) {
|
|
597
|
+
const cfg = loadConfig();
|
|
598
|
+
const extra = {
|
|
599
|
+
...opts.temperature != null ? { temperature: opts.temperature } : {},
|
|
600
|
+
...opts.maxTokens != null ? { maxTokens: opts.maxTokens } : {},
|
|
601
|
+
...opts.stream != null ? { stream: opts.stream } : {}
|
|
602
|
+
};
|
|
603
|
+
const colon = spec.indexOf(":");
|
|
604
|
+
const provider = colon > 0 ? spec.slice(0, colon) : "together";
|
|
605
|
+
const model = colon > 0 ? spec.slice(colon + 1) : spec;
|
|
606
|
+
switch (provider) {
|
|
607
|
+
case "together": {
|
|
608
|
+
const apiKey = opts.apiKey ?? cfg.TOGETHER_API_KEY;
|
|
609
|
+
if (!apiKey) throw new Error("initChatModel: set TOGETHER_API_KEY in .env (or pass apiKey)");
|
|
610
|
+
return new OpenAICompatibleProvider({ baseUrl: opts.baseUrl ?? cfg.TOGETHER_BASE_URL, apiKey, model, ...extra });
|
|
611
|
+
}
|
|
612
|
+
case "openai": {
|
|
613
|
+
const apiKey = opts.apiKey ?? cfg.OPENAI_API_KEY;
|
|
614
|
+
if (!apiKey) throw new Error("initChatModel: set OPENAI_API_KEY in .env (or pass apiKey)");
|
|
615
|
+
return new OpenAICompatibleProvider({ baseUrl: opts.baseUrl ?? cfg.OPENAI_BASE_URL, apiKey, model, ...extra });
|
|
616
|
+
}
|
|
617
|
+
case "anthropic": {
|
|
618
|
+
const apiKey = opts.apiKey ?? cfg.ANTHROPIC_API_KEY;
|
|
619
|
+
if (!apiKey) throw new Error("initChatModel: set ANTHROPIC_API_KEY in .env (or pass apiKey)");
|
|
620
|
+
return new AnthropicProvider({
|
|
621
|
+
apiKey,
|
|
622
|
+
model,
|
|
623
|
+
...opts.baseUrl ? { baseUrl: opts.baseUrl } : { baseUrl: cfg.ANTHROPIC_BASE_URL },
|
|
624
|
+
...opts.thinkingBudget != null ? { thinkingBudget: opts.thinkingBudget } : {},
|
|
625
|
+
...extra
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
case "ollama": {
|
|
629
|
+
return new OllamaProvider({ model, ...opts.baseUrl ? { baseUrl: opts.baseUrl } : { baseUrl: cfg.OLLAMA_BASE_URL }, ...extra });
|
|
630
|
+
}
|
|
631
|
+
case "custom": {
|
|
632
|
+
if (!opts.baseUrl || !opts.apiKey) throw new Error("initChatModel: custom: needs baseUrl + apiKey");
|
|
633
|
+
return new OpenAICompatibleProvider({ baseUrl: opts.baseUrl, apiKey: opts.apiKey, model, ...extra });
|
|
634
|
+
}
|
|
635
|
+
default:
|
|
636
|
+
throw new Error(`initChatModel: unknown provider "${provider}" (use together:|openai:|anthropic:|ollama:|custom:)`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/a2a/index.ts
|
|
641
|
+
import { createServer } from "http";
|
|
642
|
+
import { z as z2 } from "zod";
|
|
643
|
+
function textOfParts(parts) {
|
|
644
|
+
if (!Array.isArray(parts)) return "";
|
|
645
|
+
return parts.map((p) => {
|
|
646
|
+
if (typeof p === "string") return p;
|
|
647
|
+
if (p && typeof p === "object") {
|
|
648
|
+
const o = p;
|
|
649
|
+
if (typeof o.text === "string") return o.text;
|
|
650
|
+
}
|
|
651
|
+
return "";
|
|
652
|
+
}).join("");
|
|
653
|
+
}
|
|
654
|
+
var A2AServer = class {
|
|
655
|
+
constructor(opts) {
|
|
656
|
+
this.opts = opts;
|
|
657
|
+
this.maxContexts = Math.max(1, opts.maxContexts ?? 1e3);
|
|
658
|
+
}
|
|
659
|
+
opts;
|
|
660
|
+
server;
|
|
661
|
+
contexts = /* @__PURE__ */ new Map();
|
|
662
|
+
maxContexts;
|
|
663
|
+
corsHeaders(origin) {
|
|
664
|
+
const allowed = this.opts.corsOrigins ?? [];
|
|
665
|
+
if (!allowed.length || !origin) return {};
|
|
666
|
+
if (!allowed.includes("*") && !allowed.includes(origin)) return {};
|
|
667
|
+
return {
|
|
668
|
+
"access-control-allow-origin": allowed.includes("*") ? "*" : origin,
|
|
669
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
670
|
+
"access-control-allow-headers": "content-type, authorization"
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
runOpts(contextId) {
|
|
674
|
+
return this.opts.checkpointer ? { checkpointer: this.opts.checkpointer, threadId: contextId } : {};
|
|
675
|
+
}
|
|
676
|
+
async agentFor(contextId) {
|
|
677
|
+
const existing = this.contexts.get(contextId);
|
|
678
|
+
if (existing) {
|
|
679
|
+
this.contexts.delete(contextId);
|
|
680
|
+
this.contexts.set(contextId, existing);
|
|
681
|
+
return existing;
|
|
682
|
+
}
|
|
683
|
+
const agent = await this.opts.agentFactory(contextId);
|
|
684
|
+
this.contexts.set(contextId, agent);
|
|
685
|
+
while (this.contexts.size > this.maxContexts) {
|
|
686
|
+
const oldest = this.contexts.keys().next().value;
|
|
687
|
+
if (oldest === void 0) break;
|
|
688
|
+
this.contexts.delete(oldest);
|
|
689
|
+
}
|
|
690
|
+
return agent;
|
|
691
|
+
}
|
|
692
|
+
/** Read a request body with a hard cap; JSON.parse errors to the caller. */
|
|
693
|
+
readBody(req) {
|
|
694
|
+
const max = this.opts.maxBodyBytes ?? 10 * 1024 * 1024;
|
|
695
|
+
return new Promise((resolve4, reject) => {
|
|
696
|
+
let data = "";
|
|
697
|
+
req.on("data", (c) => {
|
|
698
|
+
data += c;
|
|
699
|
+
if (data.length > max) {
|
|
700
|
+
req.destroy();
|
|
701
|
+
reject(new AgentError("A2A_BODY_TOO_LARGE", `Request body exceeds ${max} bytes`, false));
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
req.on("end", () => resolve4(data));
|
|
705
|
+
req.on("error", reject);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
async handle(req, res) {
|
|
709
|
+
const cors = this.corsHeaders(req.headers.origin);
|
|
710
|
+
const send = (code, body) => {
|
|
711
|
+
res.writeHead(code, { "content-type": "application/json", ...cors });
|
|
712
|
+
res.end(JSON.stringify(body));
|
|
713
|
+
};
|
|
714
|
+
try {
|
|
715
|
+
if (req.method === "OPTIONS") {
|
|
716
|
+
res.writeHead(204, { ...cors });
|
|
717
|
+
res.end();
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (req.method === "GET" && (req.url === "/ok" || req.url === "/health")) {
|
|
721
|
+
send(200, { ok: true });
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (req.method === "GET" && req.url === "/.well-known/agent-card.json") {
|
|
725
|
+
send(200, this.opts.card);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (req.method === "POST" && (req.url === "/" || req.url === "")) {
|
|
729
|
+
const rpc = JSON.parse(await this.readBody(req));
|
|
730
|
+
if (rpc.method !== "message/send") {
|
|
731
|
+
send(200, { jsonrpc: "2.0", id: rpc.id, error: { code: -32601, message: `Unknown method: ${rpc.method}` } });
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const contextId = rpc.params?.contextId ?? `ctx-${Date.now().toString(36)}`;
|
|
735
|
+
const text = textOfParts(rpc.params?.message?.parts);
|
|
736
|
+
const result = await (await this.agentFor(contextId)).run(text, this.runOpts(contextId));
|
|
737
|
+
send(200, {
|
|
738
|
+
jsonrpc: "2.0",
|
|
739
|
+
id: rpc.id,
|
|
740
|
+
result: { message: { role: "agent", parts: [{ kind: "text", text: result.text }] }, contextId }
|
|
741
|
+
});
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (req.method === "POST" && req.url === "/stream") {
|
|
745
|
+
const rpc = JSON.parse(await this.readBody(req));
|
|
746
|
+
const contextId = rpc.params?.contextId ?? `ctx-${Date.now().toString(36)}`;
|
|
747
|
+
const text = textOfParts(rpc.params?.message?.parts);
|
|
748
|
+
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
|
|
749
|
+
const write = (payload) => {
|
|
750
|
+
res.write(`data: ${JSON.stringify(payload)}
|
|
751
|
+
|
|
752
|
+
`);
|
|
753
|
+
};
|
|
754
|
+
const disconnect = new AbortController();
|
|
755
|
+
res.on("close", () => {
|
|
756
|
+
if (!res.writableEnded) disconnect.abort();
|
|
757
|
+
});
|
|
758
|
+
try {
|
|
759
|
+
const gen = (await this.agentFor(contextId)).stream(text, {
|
|
760
|
+
signal: disconnect.signal,
|
|
761
|
+
...this.runOpts(contextId)
|
|
762
|
+
});
|
|
763
|
+
for (; ; ) {
|
|
764
|
+
const next = await gen.next();
|
|
765
|
+
if (next.done) {
|
|
766
|
+
write({ contextId, done: true, text: next.value.text });
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
write({ contextId, event: next.value });
|
|
770
|
+
}
|
|
771
|
+
} catch (err) {
|
|
772
|
+
write({ contextId, error: err instanceof Error ? err.message : String(err) });
|
|
773
|
+
}
|
|
774
|
+
write("[DONE]");
|
|
775
|
+
res.end();
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
send(404, { error: "not found" });
|
|
779
|
+
} catch (err) {
|
|
780
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
781
|
+
if (res.headersSent) {
|
|
782
|
+
try {
|
|
783
|
+
res.write(`data: ${JSON.stringify({ error: message })}
|
|
784
|
+
|
|
785
|
+
`);
|
|
786
|
+
res.write("data: [DONE]\n\n");
|
|
787
|
+
res.end();
|
|
788
|
+
} catch {
|
|
789
|
+
}
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
send(
|
|
793
|
+
err instanceof AgentError && err.code === "A2A_BODY_TOO_LARGE" ? 413 : 200,
|
|
794
|
+
{ jsonrpc: "2.0", id: null, error: { code: -32603, message } }
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
async start(port, host2) {
|
|
799
|
+
const listenPort = port ?? this.opts.port ?? 0;
|
|
800
|
+
const listenHost = host2 ?? this.opts.host ?? "127.0.0.1";
|
|
801
|
+
this.server = createServer((req, res) => void this.handle(req, res));
|
|
802
|
+
await new Promise((resolve4) => this.server?.listen(listenPort, listenHost, () => resolve4()));
|
|
803
|
+
return this.server.address().port;
|
|
804
|
+
}
|
|
805
|
+
async stop() {
|
|
806
|
+
await new Promise(
|
|
807
|
+
(resolve4, reject) => this.server ? this.server.close((e) => e ? reject(e) : resolve4()) : resolve4()
|
|
808
|
+
);
|
|
809
|
+
this.server = void 0;
|
|
810
|
+
this.contexts.clear();
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
// src/skills/index.ts
|
|
815
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
816
|
+
import { join as join2 } from "path";
|
|
817
|
+
import { z as z3 } from "zod";
|
|
818
|
+
var FrontmatterSchema = z3.object({
|
|
819
|
+
name: z3.string().min(1),
|
|
820
|
+
description: z3.string().min(1),
|
|
821
|
+
version: z3.string().default("0.1.0")
|
|
822
|
+
});
|
|
823
|
+
function parseSkillFile(content, path = "<inline>") {
|
|
824
|
+
const raw = content.startsWith("\uFEFF") ? content.slice(1) : content;
|
|
825
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
826
|
+
if (!match) throw new Error(`Invalid SKILL.md (missing frontmatter): ${path}`);
|
|
827
|
+
const [, fm, body] = match;
|
|
828
|
+
const meta = {};
|
|
829
|
+
for (const line of fm.split("\n")) {
|
|
830
|
+
const i = line.indexOf(":");
|
|
831
|
+
if (i > 0) meta[line.slice(0, i).trim()] = line.slice(i + 1).trim().replace(/^["']|["']$/g, "");
|
|
832
|
+
}
|
|
833
|
+
const parsed = FrontmatterSchema.parse(meta);
|
|
834
|
+
if (!body.trim()) throw new Error(`Invalid SKILL.md (empty body): ${path}`);
|
|
835
|
+
return { ...parsed, path, body: body.trim() };
|
|
836
|
+
}
|
|
837
|
+
var SkillRegistry = class {
|
|
838
|
+
skills = /* @__PURE__ */ new Map();
|
|
839
|
+
register(skill) {
|
|
840
|
+
if (this.skills.has(skill.name)) throw new Error(`Skill already registered: ${skill.name}`);
|
|
841
|
+
this.skills.set(skill.name, skill);
|
|
842
|
+
return this;
|
|
843
|
+
}
|
|
844
|
+
get(name) {
|
|
845
|
+
return this.skills.get(name);
|
|
846
|
+
}
|
|
847
|
+
list() {
|
|
848
|
+
return [...this.skills.values()];
|
|
849
|
+
}
|
|
850
|
+
/** Expose each skill's tools (if any) for Agent consumption. */
|
|
851
|
+
asTools() {
|
|
852
|
+
return this.list().flatMap((s) => s.tools ?? []);
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
async function discoverSkills(dirs, opts = {}) {
|
|
856
|
+
const found = [];
|
|
857
|
+
for (const dir of dirs) {
|
|
858
|
+
let entries;
|
|
859
|
+
try {
|
|
860
|
+
entries = readdirSync(dir);
|
|
861
|
+
} catch {
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
for (const entry of entries) {
|
|
865
|
+
const full = join2(dir, entry);
|
|
866
|
+
let stat2;
|
|
867
|
+
try {
|
|
868
|
+
stat2 = statSync(full);
|
|
869
|
+
} catch {
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
const file2 = stat2.isDirectory() ? join2(full, "SKILL.md") : full;
|
|
873
|
+
if (!file2.endsWith("SKILL.md")) continue;
|
|
874
|
+
try {
|
|
875
|
+
found.push({ ...parseSkillFile(readFileSync(file2, "utf8"), file2), tools: [] });
|
|
876
|
+
} catch (err) {
|
|
877
|
+
if (opts.strict) throw err;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return found;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/checkpoint/index.ts
|
|
885
|
+
import { mkdir, open, readFile, stat, unlink, writeFile } from "fs/promises";
|
|
886
|
+
import { join as join3 } from "path";
|
|
887
|
+
async function withFileLock(lockPath, fn, opts = {}) {
|
|
888
|
+
const staleMs = opts.staleMs ?? 3e4;
|
|
889
|
+
await mkdir(join3(lockPath, ".."), { recursive: true });
|
|
890
|
+
for (; ; ) {
|
|
891
|
+
try {
|
|
892
|
+
const handle = await open(lockPath, "wx");
|
|
893
|
+
await handle.close();
|
|
894
|
+
break;
|
|
895
|
+
} catch (err) {
|
|
896
|
+
const code = err.code;
|
|
897
|
+
if (code !== "EEXIST") throw err;
|
|
898
|
+
try {
|
|
899
|
+
const info = await stat(lockPath);
|
|
900
|
+
if (Date.now() - info.mtimeMs < staleMs) {
|
|
901
|
+
throw new Error(`Lock held: ${lockPath}`);
|
|
902
|
+
}
|
|
903
|
+
} catch (statErr) {
|
|
904
|
+
if (statErr.code !== "ENOENT") throw statErr;
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
await unlink(lockPath);
|
|
908
|
+
} catch {
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
try {
|
|
913
|
+
return await fn();
|
|
914
|
+
} finally {
|
|
915
|
+
try {
|
|
916
|
+
await unlink(lockPath);
|
|
917
|
+
} catch {
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
var FileCheckpointer = class {
|
|
922
|
+
constructor(root) {
|
|
923
|
+
this.root = root;
|
|
924
|
+
}
|
|
925
|
+
root;
|
|
926
|
+
/** threadId lands in a filesystem path — keep it a single safe directory name. */
|
|
927
|
+
threadDir(threadId) {
|
|
928
|
+
if (threadId === "" || threadId === "." || threadId === ".." || threadId.includes("/")) {
|
|
929
|
+
throw new AgentError("THREAD_ID_INVALID", `Invalid threadId: ${JSON.stringify(threadId)}`, false);
|
|
930
|
+
}
|
|
931
|
+
return join3(this.root, "threads", threadId);
|
|
932
|
+
}
|
|
933
|
+
file(threadId) {
|
|
934
|
+
return join3(this.threadDir(threadId), "checkpoint.json");
|
|
935
|
+
}
|
|
936
|
+
lock(threadId) {
|
|
937
|
+
return join3(this.threadDir(threadId), ".lock");
|
|
938
|
+
}
|
|
939
|
+
async save(threadId, checkpoint) {
|
|
940
|
+
const record = { threadId, savedAt: (/* @__PURE__ */ new Date()).toISOString(), ...checkpoint };
|
|
941
|
+
await withFileLock(this.lock(threadId), async () => {
|
|
942
|
+
await mkdir(join3(this.file(threadId), ".."), { recursive: true });
|
|
943
|
+
await writeFile(this.file(threadId), JSON.stringify(record), "utf8");
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
async load(threadId) {
|
|
947
|
+
return withFileLock(this.lock(threadId), async () => {
|
|
948
|
+
try {
|
|
949
|
+
return JSON.parse(await readFile(this.file(threadId), "utf8"));
|
|
950
|
+
} catch {
|
|
951
|
+
return void 0;
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
async delete(threadId) {
|
|
956
|
+
await withFileLock(this.lock(threadId), async () => {
|
|
957
|
+
try {
|
|
958
|
+
await unlink(this.file(threadId));
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// src/serve/index.ts
|
|
966
|
+
var AgentDefSchema = z4.object({
|
|
967
|
+
name: z4.string().min(1),
|
|
968
|
+
description: z4.string().default(""),
|
|
969
|
+
systemPrompt: z4.string().default("You are a helpful agent. Use tools when needed. Be concise."),
|
|
970
|
+
model: z4.string(),
|
|
971
|
+
port: z4.number().int().positive().optional(),
|
|
972
|
+
skills: z4.array(z4.string()).default([]),
|
|
973
|
+
filesystem: z4.string().optional(),
|
|
974
|
+
todos: z4.boolean().default(false),
|
|
975
|
+
maxSteps: z4.number().int().positive().default(8),
|
|
976
|
+
sessionDir: z4.string().optional(),
|
|
977
|
+
/** Extra env file for this agent (keys override framework-level). */
|
|
978
|
+
envFile: z4.string().optional()
|
|
979
|
+
});
|
|
980
|
+
var AgentEntrySchema = z4.union([
|
|
981
|
+
AgentDefSchema,
|
|
982
|
+
z4.object({ file: z4.string() }).catchall(z4.unknown())
|
|
983
|
+
]);
|
|
984
|
+
var FrameworkFileSchema = z4.object({
|
|
985
|
+
server: z4.object({ host: z4.string().default("127.0.0.1"), basePort: z4.number().int().min(0).default(8484) }).default({}),
|
|
986
|
+
/** Framework-level env file(s), loaded before agents resolve models. */
|
|
987
|
+
envFile: z4.union([z4.string(), z4.array(z4.string())]).optional(),
|
|
988
|
+
/** Inline env mapping (applied after env files, overrides them). */
|
|
989
|
+
env: z4.record(z4.string()).optional(),
|
|
990
|
+
/** Local checkpoint dir for served agents (durable HITL threads). */
|
|
991
|
+
checkpointer: z4.object({ dir: z4.string() }).optional(),
|
|
992
|
+
http: z4.object({ corsOrigins: z4.array(z4.string()).default([]) }).default({}),
|
|
993
|
+
agents: z4.array(AgentEntrySchema).min(1)
|
|
994
|
+
});
|
|
995
|
+
function loadEnvFile(path, override = true) {
|
|
996
|
+
dotenvConfig({ path, override });
|
|
997
|
+
}
|
|
998
|
+
function resolveAgentPaths(def, dir) {
|
|
999
|
+
return {
|
|
1000
|
+
...def,
|
|
1001
|
+
skills: def.skills.map((s) => resolve2(dir, s)),
|
|
1002
|
+
...def.filesystem ? { filesystem: resolve2(dir, def.filesystem) } : {},
|
|
1003
|
+
...def.sessionDir ? { sessionDir: resolve2(dir, def.sessionDir) } : {},
|
|
1004
|
+
...def.envFile ? { envFile: resolve2(dir, def.envFile) } : {}
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function resolveAgentEntry(fileRaw, overrides, agentDir, frameworkDir) {
|
|
1008
|
+
const fileKeys = new Set(Object.keys(fileRaw));
|
|
1009
|
+
const merged = AgentDefSchema.parse({ ...fileRaw, ...overrides });
|
|
1010
|
+
const against = (key) => fileKeys.has(key) && !(key in overrides) ? agentDir : frameworkDir;
|
|
1011
|
+
return {
|
|
1012
|
+
...merged,
|
|
1013
|
+
skills: merged.skills.map((s) => resolve2(against("skills"), s)),
|
|
1014
|
+
...merged.filesystem ? { filesystem: resolve2(against("filesystem"), merged.filesystem) } : {},
|
|
1015
|
+
...merged.sessionDir ? { sessionDir: resolve2(against("sessionDir"), merged.sessionDir) } : {},
|
|
1016
|
+
...merged.envFile ? { envFile: resolve2(against("envFile"), merged.envFile) } : {}
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function loadFrameworkConfig(path) {
|
|
1020
|
+
const raw = JSON.parse(readFileSync2(path, "utf8"));
|
|
1021
|
+
const parsed = FrameworkFileSchema.parse(raw);
|
|
1022
|
+
const dir = dirname2(resolve2(path));
|
|
1023
|
+
for (const envFile of parsed.envFile ? [parsed.envFile].flat() : []) {
|
|
1024
|
+
loadEnvFile(resolve2(dir, envFile), false);
|
|
1025
|
+
}
|
|
1026
|
+
for (const [key, value] of Object.entries(parsed.env ?? {})) {
|
|
1027
|
+
process.env[key] = value;
|
|
1028
|
+
}
|
|
1029
|
+
return {
|
|
1030
|
+
server: parsed.server,
|
|
1031
|
+
...parsed.envFile ? { envFile: parsed.envFile } : {},
|
|
1032
|
+
...parsed.env ? { env: parsed.env } : {},
|
|
1033
|
+
...parsed.checkpointer ? { checkpointer: { dir: resolve2(dir, parsed.checkpointer.dir) } } : {},
|
|
1034
|
+
http: parsed.http,
|
|
1035
|
+
agents: parsed.agents.map((entry) => {
|
|
1036
|
+
if ("file" in entry && typeof entry.file === "string") {
|
|
1037
|
+
const agentPath = resolve2(dir, entry.file);
|
|
1038
|
+
const agentDir = dirname2(agentPath);
|
|
1039
|
+
const fileRaw = JSON.parse(readFileSync2(agentPath, "utf8"));
|
|
1040
|
+
const { file: _file, ...overrides } = entry;
|
|
1041
|
+
void _file;
|
|
1042
|
+
const resolved2 = resolveAgentEntry(fileRaw, overrides, agentDir, dir);
|
|
1043
|
+
if (resolved2.envFile) loadEnvFile(resolved2.envFile);
|
|
1044
|
+
return resolved2;
|
|
1045
|
+
}
|
|
1046
|
+
const resolved = resolveAgentPaths(AgentDefSchema.parse(entry), dir);
|
|
1047
|
+
if (resolved.envFile) loadEnvFile(resolved.envFile);
|
|
1048
|
+
return resolved;
|
|
1049
|
+
})
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
async function buildServedAgent(def, opts = {}) {
|
|
1053
|
+
const middlewares = [];
|
|
1054
|
+
if (def.filesystem) middlewares.push(filesystemMiddleware({ root: def.filesystem }));
|
|
1055
|
+
if (def.todos) middlewares.push(todoListMiddleware());
|
|
1056
|
+
let systemPrompt = def.systemPrompt;
|
|
1057
|
+
if (def.skills.length) {
|
|
1058
|
+
const registry = new SkillRegistry();
|
|
1059
|
+
for (const dir of def.skills) {
|
|
1060
|
+
for (const s of await discoverSkills([dir])) {
|
|
1061
|
+
try {
|
|
1062
|
+
registry.register(s);
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const catalog = registry.list().map((s) => `- ${s.name}: ${s.description}`).join("\n");
|
|
1068
|
+
if (catalog) systemPrompt += `
|
|
1069
|
+
|
|
1070
|
+
Available skill packs (mention $name to invoke):
|
|
1071
|
+
${catalog}`;
|
|
1072
|
+
}
|
|
1073
|
+
const agent = new Agent({
|
|
1074
|
+
name: def.name,
|
|
1075
|
+
description: def.description,
|
|
1076
|
+
systemPrompt,
|
|
1077
|
+
model: initChatModel(def.model),
|
|
1078
|
+
maxSteps: def.maxSteps,
|
|
1079
|
+
middleware: middlewares,
|
|
1080
|
+
...def.sessionDir ? {
|
|
1081
|
+
// Context-scoped id keeps one def's sessions from mingling when
|
|
1082
|
+
// the server runs a fresh agent per A2A contextId.
|
|
1083
|
+
sessionManager: new SessionManager({
|
|
1084
|
+
sessionId: opts.sessionId ?? def.name,
|
|
1085
|
+
storage: new LocalFileStorage(def.sessionDir)
|
|
1086
|
+
})
|
|
1087
|
+
} : {}
|
|
1088
|
+
});
|
|
1089
|
+
return { agent, card: { name: def.name, ...def.description ? { description: def.description } : {} } };
|
|
1090
|
+
}
|
|
1091
|
+
async function serveFramework(file2) {
|
|
1092
|
+
const served = [];
|
|
1093
|
+
const checkpointer = file2.checkpointer ? new FileCheckpointer(file2.checkpointer.dir) : void 0;
|
|
1094
|
+
let port = file2.server.basePort;
|
|
1095
|
+
for (const def of file2.agents) {
|
|
1096
|
+
const { card } = await buildServedAgent(def);
|
|
1097
|
+
const server = new A2AServer({
|
|
1098
|
+
card,
|
|
1099
|
+
// Fresh agent per A2A contextId — sharing one instance would leak one
|
|
1100
|
+
// conversation into another context's history.
|
|
1101
|
+
agentFactory: (contextId) => buildServedAgent(def, def.sessionDir ? { sessionId: `${def.name}:${contextId}` } : {}).then((r) => r.agent),
|
|
1102
|
+
...checkpointer ? { checkpointer } : {},
|
|
1103
|
+
corsOrigins: file2.http.corsOrigins,
|
|
1104
|
+
host: file2.server.host
|
|
1105
|
+
});
|
|
1106
|
+
const actual = await server.start(def.port ?? port, file2.server.host);
|
|
1107
|
+
port = actual + 1;
|
|
1108
|
+
const url = `http://${file2.server.host}:${actual}`;
|
|
1109
|
+
served.push({ name: def.name, url, cardUrl: `${url}/.well-known/agent-card.json`, server });
|
|
1110
|
+
}
|
|
1111
|
+
return {
|
|
1112
|
+
agents: served,
|
|
1113
|
+
stop: async () => {
|
|
1114
|
+
for (const s of served) await s.server.stop();
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/observability/index.ts
|
|
1120
|
+
function supportsColor() {
|
|
1121
|
+
return !process.env.NO_COLOR && !!process.stdout?.isTTY;
|
|
1122
|
+
}
|
|
1123
|
+
var C = {
|
|
1124
|
+
reset: "\x1B[0m",
|
|
1125
|
+
bold: "\x1B[1m",
|
|
1126
|
+
dim: "\x1B[2m",
|
|
1127
|
+
cyan: "\x1B[36m",
|
|
1128
|
+
yellow: "\x1B[33m",
|
|
1129
|
+
green: "\x1B[32m",
|
|
1130
|
+
magenta: "\x1B[35m",
|
|
1131
|
+
red: "\x1B[31m"
|
|
1132
|
+
};
|
|
1133
|
+
function paint(color, text, on) {
|
|
1134
|
+
if (!on || color === "reset") return text;
|
|
1135
|
+
return `${C[color]}${text}${C.reset}`;
|
|
1136
|
+
}
|
|
1137
|
+
function logError(prefix, err, colors) {
|
|
1138
|
+
const on = colors ?? supportsColor();
|
|
1139
|
+
const message = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
1140
|
+
console.error(paint("red", `${prefix} error:`, on), message);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// src/serve/cli.ts
|
|
1144
|
+
var HELP = `framework \u2014 launch Framework agents as local servers.
|
|
1145
|
+
|
|
1146
|
+
Usage:
|
|
1147
|
+
framework-serve [command] [options]
|
|
1148
|
+
|
|
1149
|
+
Commands:
|
|
1150
|
+
dev [-c framework.json] [--host H] [--port N] local dev server (default)
|
|
1151
|
+
serve|up [-c framework.json] serve agents
|
|
1152
|
+
validate [-c framework.json] validate config and print agents
|
|
1153
|
+
--help this help
|
|
1154
|
+
|
|
1155
|
+
framework.json keys: server{host,basePort}, envFile, env{}, checkpointer{dir},
|
|
1156
|
+
http{corsOrigins}, agents[{name, model, file?, envFile?, skills?, filesystem?,
|
|
1157
|
+
todos?, maxSteps?, sessionDir?, port?}]. Agent files + relative paths resolve
|
|
1158
|
+
against their own file's directory; inline overrides against framework.json.
|
|
1159
|
+
`;
|
|
1160
|
+
function arg(flag, short) {
|
|
1161
|
+
const i = process.argv.findIndex((a) => a === flag || short && a === short);
|
|
1162
|
+
return i >= 0 ? process.argv[i + 1] : void 0;
|
|
1163
|
+
}
|
|
1164
|
+
var command = process.argv[2] ?? "dev";
|
|
1165
|
+
if (command === "--help" || command === "-h" || process.argv.includes("--help")) {
|
|
1166
|
+
console.log(HELP);
|
|
1167
|
+
process.exit(0);
|
|
1168
|
+
}
|
|
1169
|
+
if (!["dev", "serve", "up", "validate"].includes(command)) {
|
|
1170
|
+
console.error(`Unknown command: ${command}
|
|
1171
|
+
${HELP}`);
|
|
1172
|
+
process.exit(1);
|
|
1173
|
+
}
|
|
1174
|
+
var configPath = resolve3(arg("-c", "--config") ?? "framework.json");
|
|
1175
|
+
var file;
|
|
1176
|
+
try {
|
|
1177
|
+
file = loadFrameworkConfig(configPath);
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
logError("[framework]", err);
|
|
1180
|
+
process.exit(1);
|
|
1181
|
+
}
|
|
1182
|
+
if (command === "validate") {
|
|
1183
|
+
console.log(`OK: ${configPath}`);
|
|
1184
|
+
for (const a of file.agents) {
|
|
1185
|
+
console.log(`- ${a.name} [${a.model}]${a.port ? ` :${a.port}` : ""}${a.envFile ? ` (env: ${a.envFile})` : ""}`);
|
|
1186
|
+
}
|
|
1187
|
+
process.exit(0);
|
|
1188
|
+
}
|
|
1189
|
+
var host = arg("--host") ?? file.server.host;
|
|
1190
|
+
var portArg = arg("--port");
|
|
1191
|
+
if (portArg) {
|
|
1192
|
+
const port = Number.parseInt(portArg, 10);
|
|
1193
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
1194
|
+
console.error(`Invalid --port: ${portArg}`);
|
|
1195
|
+
process.exit(1);
|
|
1196
|
+
}
|
|
1197
|
+
file.server.basePort = port;
|
|
1198
|
+
}
|
|
1199
|
+
var { agents, stop } = await serveFramework({ ...file, server: { ...file.server, host } });
|
|
1200
|
+
for (const a of agents) {
|
|
1201
|
+
console.log(`[framework] ${a.name} -> ${a.url} (card: ${a.cardUrl})`);
|
|
1202
|
+
}
|
|
1203
|
+
console.log(`[framework] ${command === "dev" ? "dev server" : "serving"} ${agents.length} agent(s). Ctrl-C to stop.`);
|
|
1204
|
+
var shutdown = async () => {
|
|
1205
|
+
await stop();
|
|
1206
|
+
process.exit(0);
|
|
1207
|
+
};
|
|
1208
|
+
process.on("SIGINT", () => void shutdown());
|
|
1209
|
+
process.on("SIGTERM", () => void shutdown());
|