@faapi/agent 3.2.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +119 -14
- package/dist/index.js +331 -98
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/dist/index.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// src/providers/openai.ts
|
|
2
2
|
var DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
3
|
-
var RESERVED_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
3
|
+
var RESERVED_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
4
|
+
"provider",
|
|
5
|
+
"apiKey",
|
|
6
|
+
"model",
|
|
7
|
+
"baseURL",
|
|
8
|
+
"models",
|
|
9
|
+
"timeoutMs",
|
|
10
|
+
"maxRetries"
|
|
11
|
+
]);
|
|
4
12
|
var LLMProviderError = class extends Error {
|
|
5
13
|
/** HTTP 状态码(网络错误 / JSON 解析错误为 undefined) */
|
|
6
14
|
status;
|
|
@@ -16,6 +24,7 @@ var LLMProviderError = class extends Error {
|
|
|
16
24
|
function createOpenAIProvider(config) {
|
|
17
25
|
const baseURL = (config.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
18
26
|
const apiKey = config.apiKey;
|
|
27
|
+
const maxRetries = Math.max(0, typeof config.maxRetries === "number" ? config.maxRetries : 2);
|
|
19
28
|
function buildRequestBody(request) {
|
|
20
29
|
const modelName = request.model ?? Object.keys(config.models)[0];
|
|
21
30
|
const modelConfig = modelName ? config.models[modelName] : void 0;
|
|
@@ -58,34 +67,101 @@ function createOpenAIProvider(config) {
|
|
|
58
67
|
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
59
68
|
return headers;
|
|
60
69
|
}
|
|
61
|
-
|
|
70
|
+
function classifyAbortError(err, externalSignal) {
|
|
71
|
+
if (externalSignal?.aborted) {
|
|
72
|
+
return new AgentAbortError();
|
|
73
|
+
}
|
|
74
|
+
const errName = err?.name;
|
|
75
|
+
if (errName === "AbortError" || errName === "TimeoutError") {
|
|
76
|
+
return new LLMProviderError(`LLM request timed out after ${config.timeoutMs}ms`);
|
|
77
|
+
}
|
|
78
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
79
|
+
return new LLMProviderError(`Network error: ${reason}`, { cause: err });
|
|
80
|
+
}
|
|
81
|
+
function isAbortLike(err) {
|
|
82
|
+
const name = err?.name;
|
|
83
|
+
return name === "AbortError" || name === "TimeoutError";
|
|
84
|
+
}
|
|
85
|
+
async function safeFetch(url, init, externalSignal) {
|
|
62
86
|
try {
|
|
63
87
|
return await fetch(url, init);
|
|
64
88
|
} catch (err) {
|
|
65
|
-
|
|
66
|
-
throw new LLMProviderError(`Network error: ${reason}`, { cause: err });
|
|
89
|
+
throw classifyAbortError(err, externalSignal);
|
|
67
90
|
}
|
|
68
91
|
}
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
function isRetryable(err) {
|
|
93
|
+
if (!(err instanceof LLMProviderError)) return false;
|
|
94
|
+
if (err.status === void 0) return true;
|
|
95
|
+
return err.status === 429 || err.status >= 500;
|
|
96
|
+
}
|
|
97
|
+
function retryDelayMs(response, attempt) {
|
|
98
|
+
const retryAfter = response?.headers.get("retry-after");
|
|
99
|
+
if (retryAfter) {
|
|
100
|
+
const seconds = Number(retryAfter);
|
|
101
|
+
if (!isNaN(seconds) && seconds >= 0) return Math.min(seconds * 1e3, 3e4);
|
|
102
|
+
}
|
|
103
|
+
return 500 * 2 ** attempt;
|
|
104
|
+
}
|
|
105
|
+
function effectiveSignal(request) {
|
|
106
|
+
const { signal } = request;
|
|
107
|
+
const timeoutMs = typeof config.timeoutMs === "number" ? config.timeoutMs : void 0;
|
|
108
|
+
if (!signal && !timeoutMs) return void 0;
|
|
109
|
+
const signals = [];
|
|
110
|
+
if (signal) signals.push(signal);
|
|
111
|
+
if (timeoutMs) signals.push(AbortSignal.timeout(timeoutMs));
|
|
112
|
+
return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
113
|
+
}
|
|
114
|
+
async function fetchOkWithRetry(url, baseInit, request, maxRetries2) {
|
|
115
|
+
let lastErr;
|
|
116
|
+
let lastResponse;
|
|
117
|
+
for (let attempt = 0; attempt <= maxRetries2; attempt++) {
|
|
118
|
+
if (request.signal?.aborted) {
|
|
119
|
+
throw new AgentAbortError();
|
|
120
|
+
}
|
|
121
|
+
if (attempt > 0) {
|
|
122
|
+
await new Promise((r) => setTimeout(r, retryDelayMs(lastResponse, attempt - 1)));
|
|
123
|
+
}
|
|
124
|
+
const init = { ...baseInit, signal: effectiveSignal(request) };
|
|
125
|
+
let response;
|
|
126
|
+
try {
|
|
127
|
+
response = await safeFetch(url, init, request.signal);
|
|
128
|
+
} catch (err2) {
|
|
129
|
+
if (!isRetryable(err2)) throw err2;
|
|
130
|
+
lastErr = err2;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (response.ok) return response;
|
|
134
|
+
let excerpt = "<body unreadable>";
|
|
135
|
+
try {
|
|
136
|
+
excerpt = (await response.text()).slice(0, 500);
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
const err = new LLMProviderError(`HTTP ${response.status}: ${excerpt}`, {
|
|
140
|
+
status: response.status,
|
|
141
|
+
body: excerpt
|
|
142
|
+
});
|
|
143
|
+
if (!isRetryable(err)) throw err;
|
|
144
|
+
lastErr = err;
|
|
145
|
+
lastResponse = response;
|
|
146
|
+
}
|
|
147
|
+
throw lastErr;
|
|
77
148
|
}
|
|
78
149
|
async function complete(request) {
|
|
79
150
|
const url = `${baseURL}/chat/completions`;
|
|
80
151
|
const body = buildRequestBody(request);
|
|
81
|
-
const
|
|
152
|
+
const baseInit = {
|
|
82
153
|
method: "POST",
|
|
83
154
|
headers: buildHeaders(),
|
|
84
155
|
body: JSON.stringify(body)
|
|
85
156
|
};
|
|
86
|
-
const response = await
|
|
87
|
-
|
|
88
|
-
|
|
157
|
+
const response = await fetchOkWithRetry(url, baseInit, request, maxRetries);
|
|
158
|
+
let bodyText;
|
|
159
|
+
try {
|
|
160
|
+
bodyText = await response.text();
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (!isAbortLike(err)) throw err;
|
|
163
|
+
throw classifyAbortError(err, request.signal);
|
|
164
|
+
}
|
|
89
165
|
let json;
|
|
90
166
|
try {
|
|
91
167
|
json = JSON.parse(bodyText);
|
|
@@ -121,13 +197,12 @@ function createOpenAIProvider(config) {
|
|
|
121
197
|
const url = `${baseURL}/chat/completions`;
|
|
122
198
|
const body = buildRequestBody(request);
|
|
123
199
|
body.stream = true;
|
|
124
|
-
const
|
|
200
|
+
const baseInit = {
|
|
125
201
|
method: "POST",
|
|
126
202
|
headers: buildHeaders(),
|
|
127
203
|
body: JSON.stringify(body)
|
|
128
204
|
};
|
|
129
|
-
const response = await
|
|
130
|
-
await ensureOk(response);
|
|
205
|
+
const response = await fetchOkWithRetry(url, baseInit, request, maxRetries);
|
|
131
206
|
if (!response.body) {
|
|
132
207
|
throw new LLMProviderError("Response body is null (streaming unsupported)", {
|
|
133
208
|
status: response.status
|
|
@@ -144,10 +219,12 @@ function createOpenAIProvider(config) {
|
|
|
144
219
|
const { done, value } = await reader.read();
|
|
145
220
|
if (done) break;
|
|
146
221
|
buffer += decoder.decode(value, { stream: true });
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
222
|
+
while (true) {
|
|
223
|
+
const boundary = findSseEventBoundary(buffer);
|
|
224
|
+
if (!boundary) break;
|
|
225
|
+
const [start, length] = boundary;
|
|
226
|
+
const eventStr = buffer.slice(0, start);
|
|
227
|
+
buffer = buffer.slice(start + length);
|
|
151
228
|
const data = extractSSEData(eventStr);
|
|
152
229
|
if (data === null) continue;
|
|
153
230
|
if (data === "[DONE]") {
|
|
@@ -181,8 +258,15 @@ function createOpenAIProvider(config) {
|
|
|
181
258
|
}
|
|
182
259
|
}
|
|
183
260
|
yield finalizeStreamChunk(accumulators, finishReason, usage);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (!isAbortLike(err)) throw err;
|
|
263
|
+
throw classifyAbortError(err, request.signal);
|
|
184
264
|
} finally {
|
|
185
|
-
|
|
265
|
+
try {
|
|
266
|
+
await reader.cancel();
|
|
267
|
+
} catch {
|
|
268
|
+
reader.releaseLock();
|
|
269
|
+
}
|
|
186
270
|
}
|
|
187
271
|
}
|
|
188
272
|
return { complete, stream };
|
|
@@ -260,7 +344,7 @@ function mapUsage(u) {
|
|
|
260
344
|
}
|
|
261
345
|
function extractSSEData(event) {
|
|
262
346
|
const dataLines = [];
|
|
263
|
-
for (const line of event.split(
|
|
347
|
+
for (const line of event.split(/\r\n|\n|\r/)) {
|
|
264
348
|
if (line === "" || line.startsWith(":")) continue;
|
|
265
349
|
if (line.startsWith("data:")) {
|
|
266
350
|
dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
@@ -269,6 +353,32 @@ function extractSSEData(event) {
|
|
|
269
353
|
if (dataLines.length === 0) return null;
|
|
270
354
|
return dataLines.join("\n");
|
|
271
355
|
}
|
|
356
|
+
function findSseEventBoundary(buffer) {
|
|
357
|
+
let eolCount = 0;
|
|
358
|
+
let boundaryStart = -1;
|
|
359
|
+
for (let i = 0; i < buffer.length; ) {
|
|
360
|
+
const ch = buffer[i];
|
|
361
|
+
let eolLen = 0;
|
|
362
|
+
if (ch === "\n") {
|
|
363
|
+
eolLen = 1;
|
|
364
|
+
} else if (ch === "\r") {
|
|
365
|
+
eolLen = buffer[i + 1] === "\n" ? 2 : 1;
|
|
366
|
+
}
|
|
367
|
+
if (eolLen === 0) {
|
|
368
|
+
eolCount = 0;
|
|
369
|
+
i++;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
eolCount++;
|
|
373
|
+
if (eolCount === 1) {
|
|
374
|
+
boundaryStart = i;
|
|
375
|
+
} else {
|
|
376
|
+
return [boundaryStart, i + eolLen - boundaryStart];
|
|
377
|
+
}
|
|
378
|
+
i += eolLen;
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
272
382
|
function accumulateToolCall(accumulators, tc) {
|
|
273
383
|
const idx = tc.index ?? 0;
|
|
274
384
|
const acc = accumulators.get(idx) ?? { argsString: "" };
|
|
@@ -308,6 +418,12 @@ function finalizeStreamChunk(accumulators, finishReason, usage) {
|
|
|
308
418
|
}
|
|
309
419
|
|
|
310
420
|
// src/provider.ts
|
|
421
|
+
var AgentAbortError = class extends Error {
|
|
422
|
+
constructor(message = "Agent execution aborted") {
|
|
423
|
+
super(message);
|
|
424
|
+
this.name = "AgentAbortError";
|
|
425
|
+
}
|
|
426
|
+
};
|
|
311
427
|
function createProvider(config) {
|
|
312
428
|
switch (config.provider) {
|
|
313
429
|
case "openai":
|
|
@@ -358,6 +474,44 @@ function buildInitialMessages(input, systemPrompt) {
|
|
|
358
474
|
messages.push({ role: "user", content: input });
|
|
359
475
|
return messages;
|
|
360
476
|
}
|
|
477
|
+
function estimateTokens(chars) {
|
|
478
|
+
return Math.ceil(chars / 2);
|
|
479
|
+
}
|
|
480
|
+
function estimateMessageTokens(message) {
|
|
481
|
+
let chars = message.content.length;
|
|
482
|
+
if (message.toolCalls) {
|
|
483
|
+
chars += JSON.stringify(message.toolCalls).length;
|
|
484
|
+
}
|
|
485
|
+
if (message.toolCallId) {
|
|
486
|
+
chars += message.toolCallId.length;
|
|
487
|
+
}
|
|
488
|
+
return estimateTokens(chars);
|
|
489
|
+
}
|
|
490
|
+
function trimHistory(messages, maxTokens) {
|
|
491
|
+
let headEnd = 0;
|
|
492
|
+
while (headEnd < messages.length && messages[headEnd].role !== "assistant") {
|
|
493
|
+
headEnd++;
|
|
494
|
+
}
|
|
495
|
+
const head = messages.slice(0, headEnd);
|
|
496
|
+
const turns = [];
|
|
497
|
+
for (const message of messages.slice(headEnd)) {
|
|
498
|
+
if (message.role === "assistant" || turns.length === 0) {
|
|
499
|
+
turns.push([message]);
|
|
500
|
+
} else {
|
|
501
|
+
turns[turns.length - 1].push(message);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (turns.length === 0) return messages;
|
|
505
|
+
const kept = [];
|
|
506
|
+
let total = head.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
|
|
507
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
508
|
+
const turnTokens = turns[i].reduce((sum, m) => sum + estimateMessageTokens(m), 0);
|
|
509
|
+
if (kept.length > 0 && total + turnTokens > maxTokens) break;
|
|
510
|
+
kept.unshift(turns[i]);
|
|
511
|
+
total += turnTokens;
|
|
512
|
+
}
|
|
513
|
+
return [...head, ...kept.flat()];
|
|
514
|
+
}
|
|
361
515
|
function buildRequestExtras(config) {
|
|
362
516
|
return {
|
|
363
517
|
tools: config.tools,
|
|
@@ -377,7 +531,7 @@ function extractSubAgentName(toolName) {
|
|
|
377
531
|
return toolName;
|
|
378
532
|
}
|
|
379
533
|
async function reactLoop(input, config) {
|
|
380
|
-
const enableTracing = config.enableTracing ??
|
|
534
|
+
const enableTracing = config.enableTracing ?? false;
|
|
381
535
|
const messages = buildInitialMessages(input, config.systemPrompt);
|
|
382
536
|
const maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
383
537
|
const extras = buildRequestExtras(config);
|
|
@@ -386,12 +540,17 @@ async function reactLoop(input, config) {
|
|
|
386
540
|
const traceStartedAt = enableTracing ? nowMs() : 0;
|
|
387
541
|
const traceEvents = enableTracing ? [] : void 0;
|
|
388
542
|
while (turns < maxTurns) {
|
|
543
|
+
if (config.signal?.aborted) {
|
|
544
|
+
throw new AgentAbortError();
|
|
545
|
+
}
|
|
389
546
|
turns++;
|
|
390
547
|
const llmStartedAt = enableTracing ? nowMs() : 0;
|
|
391
|
-
const
|
|
548
|
+
const outgoing = config.maxHistoryTokens ? trimHistory(messages, config.maxHistoryTokens) : messages;
|
|
549
|
+
const inputSnapshot = enableTracing ? [...outgoing] : void 0;
|
|
392
550
|
const response = await config.provider.complete({
|
|
393
|
-
messages: [...
|
|
394
|
-
...extras
|
|
551
|
+
messages: [...outgoing],
|
|
552
|
+
...extras,
|
|
553
|
+
signal: config.signal
|
|
395
554
|
});
|
|
396
555
|
if (response.usage) {
|
|
397
556
|
totalUsage = accumulateUsage(totalUsage, response.usage);
|
|
@@ -431,27 +590,48 @@ async function reactLoop(input, config) {
|
|
|
431
590
|
} : void 0
|
|
432
591
|
};
|
|
433
592
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
593
|
+
const settled = await Promise.all(
|
|
594
|
+
response.message.toolCalls.map(async (toolCall) => {
|
|
595
|
+
const toolStartedAt = enableTracing ? nowMs() : 0;
|
|
596
|
+
let resultStr;
|
|
597
|
+
let rawResult;
|
|
598
|
+
let toolErr;
|
|
599
|
+
let hasError = false;
|
|
600
|
+
try {
|
|
601
|
+
rawResult = await config.executeTool(toolCall.name, toolCall.arguments);
|
|
602
|
+
if (isTracingToolResult(rawResult)) {
|
|
603
|
+
resultStr = stringifyResult(rawResult.result);
|
|
604
|
+
} else {
|
|
605
|
+
resultStr = stringifyResult(rawResult);
|
|
606
|
+
}
|
|
607
|
+
} catch (err) {
|
|
608
|
+
hasError = true;
|
|
609
|
+
toolErr = err;
|
|
610
|
+
resultStr = stringifyError(err);
|
|
611
|
+
rawResult = void 0;
|
|
446
612
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
613
|
+
const toolEndedAt = enableTracing ? nowMs() : 0;
|
|
614
|
+
return {
|
|
615
|
+
toolCall,
|
|
616
|
+
toolStartedAt,
|
|
617
|
+
toolEndedAt,
|
|
618
|
+
resultStr,
|
|
619
|
+
rawResult,
|
|
620
|
+
toolErr,
|
|
621
|
+
hasError
|
|
622
|
+
};
|
|
623
|
+
})
|
|
624
|
+
);
|
|
625
|
+
for (const {
|
|
626
|
+
toolCall,
|
|
627
|
+
toolStartedAt,
|
|
628
|
+
toolEndedAt,
|
|
629
|
+
resultStr,
|
|
630
|
+
rawResult,
|
|
631
|
+
toolErr,
|
|
632
|
+
hasError
|
|
633
|
+
} of settled) {
|
|
453
634
|
if (enableTracing) {
|
|
454
|
-
const toolEndedAt = nowMs();
|
|
455
635
|
if (isTracingToolResult(rawResult)) {
|
|
456
636
|
traceEvents.push({
|
|
457
637
|
type: "subagent_call",
|
|
@@ -491,23 +671,28 @@ async function reactLoop(input, config) {
|
|
|
491
671
|
);
|
|
492
672
|
}
|
|
493
673
|
async function* reactLoopStream(input, config) {
|
|
494
|
-
const enableTracing = config.enableTracing ??
|
|
674
|
+
const enableTracing = config.enableTracing ?? false;
|
|
495
675
|
const messages = buildInitialMessages(input, config.systemPrompt);
|
|
496
676
|
const maxTurns = config.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
497
677
|
const extras = buildRequestExtras(config);
|
|
498
678
|
let totalUsage;
|
|
499
679
|
let turns = 0;
|
|
500
680
|
while (turns < maxTurns) {
|
|
681
|
+
if (config.signal?.aborted) {
|
|
682
|
+
throw new AgentAbortError();
|
|
683
|
+
}
|
|
501
684
|
turns++;
|
|
502
685
|
const llmStartedAt = enableTracing ? nowMs() : 0;
|
|
503
|
-
const
|
|
686
|
+
const outgoing = config.maxHistoryTokens ? trimHistory(messages, config.maxHistoryTokens) : messages;
|
|
687
|
+
const inputSnapshot = enableTracing ? [...outgoing] : void 0;
|
|
504
688
|
let turnContent = "";
|
|
505
689
|
let toolCalls;
|
|
506
690
|
let finishReason;
|
|
507
691
|
let turnUsage;
|
|
508
692
|
for await (const chunk of config.provider.stream({
|
|
509
|
-
messages: [...
|
|
510
|
-
...extras
|
|
693
|
+
messages: [...outgoing],
|
|
694
|
+
...extras,
|
|
695
|
+
signal: config.signal
|
|
511
696
|
})) {
|
|
512
697
|
if (typeof chunk.deltaContent === "string" && chunk.deltaContent.length > 0) {
|
|
513
698
|
turnContent += chunk.deltaContent;
|
|
@@ -687,7 +872,7 @@ var Agent = class _Agent {
|
|
|
687
872
|
const config = await this.buildLoopConfig(options);
|
|
688
873
|
const result = await reactLoop(input, config);
|
|
689
874
|
if (result.trace) {
|
|
690
|
-
result.trace.agentName = this.deps.agentName;
|
|
875
|
+
result.trace.agentName = options?.agent ?? this.deps.agentName;
|
|
691
876
|
}
|
|
692
877
|
return result;
|
|
693
878
|
}
|
|
@@ -747,24 +932,30 @@ var Agent = class _Agent {
|
|
|
747
932
|
/**
|
|
748
933
|
* 组装 ReactLoopConfig
|
|
749
934
|
*
|
|
750
|
-
* 1.
|
|
935
|
+
* 1. 解析有效 agent 名:`options.agent` > `deps.agentName`(`config.agent.defaultAgent`)
|
|
936
|
+
* 2. 查 agent 元数据(未注册抛 AgentError)——用 `getAgent` 拿 AgentCore
|
|
751
937
|
* (LLM-facing 字段:systemPrompt / model / maxTurns)
|
|
752
|
-
*
|
|
753
|
-
*
|
|
938
|
+
* 3. buildToolDefinitions 组装 tool 列表(用有效 agent 名查 tools / sub-agents)
|
|
939
|
+
* 4. config 字段优先级(高 → 低):`options` > agent 元数据 > 全局 AgentRuntimeConfig / deps.defaultProvider
|
|
754
940
|
*
|
|
755
941
|
* `options.model` 是字符串 key,由 {@link resolveModelKey} 解析为 provider + model
|
|
756
942
|
* (支持 llms key 精确匹配 / `provider/model` 一体化 / 纯 model 名模糊匹配)。
|
|
757
943
|
* 不传 `options.model` 时用 `deps.defaultProvider` + agent 元数据 `config.model`。
|
|
758
944
|
* 详见 [agentHandle.md](./agentHandle.md) 的「`options.model` 字符串 key 解析规则」。
|
|
945
|
+
*
|
|
946
|
+
* `options.agent` 覆盖本次调用的 agent 名——不传时用 `deps.agentName`(来自
|
|
947
|
+
* `config.agent.defaultAgent`)。`defaultAgent` 未设且 `options.agent` 未传时抛
|
|
948
|
+
* `AgentError`。
|
|
759
949
|
*/
|
|
760
950
|
async buildLoopConfig(options) {
|
|
761
|
-
const
|
|
951
|
+
const agentName = options?.agent ?? this.deps.agentName;
|
|
952
|
+
const meta = this.deps.getAgent(agentName);
|
|
762
953
|
if (!meta) {
|
|
763
|
-
throw new AgentError(`Agent "${
|
|
954
|
+
throw new AgentError(`Agent "${agentName}" is not registered`);
|
|
764
955
|
}
|
|
765
|
-
const tools = await this.buildToolDefinitions();
|
|
956
|
+
const tools = await this.buildToolDefinitions(agentName);
|
|
766
957
|
const { provider, model } = this.resolveModelKey(options?.model, meta);
|
|
767
|
-
const enableTracing = options?.enableTracing ?? this.deps.config?.enableTracing ??
|
|
958
|
+
const enableTracing = options?.enableTracing ?? this.deps.config?.enableTracing ?? false;
|
|
768
959
|
return {
|
|
769
960
|
provider,
|
|
770
961
|
systemPrompt: meta.systemPrompt,
|
|
@@ -773,6 +964,7 @@ var Agent = class _Agent {
|
|
|
773
964
|
maxTokens: options?.maxTokens,
|
|
774
965
|
maxTurns: meta.maxTurns ?? this.deps.config?.maxTurns,
|
|
775
966
|
tools,
|
|
967
|
+
signal: options?.signal,
|
|
776
968
|
enableTracing,
|
|
777
969
|
executeTool: async (name, args) => this.executeTool(name, args, enableTracing)
|
|
778
970
|
};
|
|
@@ -850,9 +1042,9 @@ var Agent = class _Agent {
|
|
|
850
1042
|
*
|
|
851
1043
|
* sub-agent 的 `input` 始终为 `{ type: 'object' }`(agent 参数开放)。
|
|
852
1044
|
*/
|
|
853
|
-
async buildToolDefinitions() {
|
|
1045
|
+
async buildToolDefinitions(agentName) {
|
|
854
1046
|
const definitions = /* @__PURE__ */ new Map();
|
|
855
|
-
for (const tool of this.deps.resolveAgentTools(
|
|
1047
|
+
for (const tool of this.deps.resolveAgentTools(agentName)) {
|
|
856
1048
|
if (definitions.has(tool.name)) continue;
|
|
857
1049
|
const schemaRes = await this.getToolSchema(tool);
|
|
858
1050
|
definitions.set(tool.name, {
|
|
@@ -861,7 +1053,7 @@ var Agent = class _Agent {
|
|
|
861
1053
|
input: schemaRes?.jsonSchema ?? { type: "object" }
|
|
862
1054
|
});
|
|
863
1055
|
}
|
|
864
|
-
for (const subAgent of this.deps.resolveSubAgents(
|
|
1056
|
+
for (const subAgent of this.deps.resolveSubAgents(agentName)) {
|
|
865
1057
|
const name = `agent.${subAgent.name}`;
|
|
866
1058
|
if (definitions.has(name)) continue;
|
|
867
1059
|
definitions.set(name, {
|
|
@@ -870,7 +1062,9 @@ var Agent = class _Agent {
|
|
|
870
1062
|
input: { type: "object" }
|
|
871
1063
|
});
|
|
872
1064
|
}
|
|
873
|
-
|
|
1065
|
+
const defs = Array.from(definitions.values());
|
|
1066
|
+
const filtered = this.deps.config?.filterTools?.(defs, this.deps.ctx);
|
|
1067
|
+
return filtered ?? defs;
|
|
874
1068
|
}
|
|
875
1069
|
/**
|
|
876
1070
|
* tool 执行路由(由 reactLoop 调用)
|
|
@@ -886,9 +1080,28 @@ var Agent = class _Agent {
|
|
|
886
1080
|
*
|
|
887
1081
|
* **tool 未找到 / 加载失败**:抛错,被 reactLoop catch 后同样回传 LLM。
|
|
888
1082
|
*/
|
|
889
|
-
async executeTool(
|
|
1083
|
+
async executeTool(rawName, rawArgs, enableTracing) {
|
|
1084
|
+
const name = rawName;
|
|
1085
|
+
let args = rawArgs;
|
|
1086
|
+
const guard = this.deps.config?.beforeToolCall?.(name, args, this.deps.ctx);
|
|
1087
|
+
if (guard) {
|
|
1088
|
+
if ("error" in guard) return { error: guard.error };
|
|
1089
|
+
if ("args" in guard) args = guard.args;
|
|
1090
|
+
}
|
|
1091
|
+
const declared = /* @__PURE__ */ new Set();
|
|
1092
|
+
for (const tool2 of this.deps.resolveAgentTools(this.deps.agentName)) {
|
|
1093
|
+
declared.add(tool2.name);
|
|
1094
|
+
}
|
|
1095
|
+
for (const sub of this.deps.resolveSubAgents(this.deps.agentName)) {
|
|
1096
|
+
declared.add(`agent.${sub.name}`);
|
|
1097
|
+
}
|
|
1098
|
+
if (!declared.has(name)) {
|
|
1099
|
+
return {
|
|
1100
|
+
error: `Tool "${name}" is not declared by agent "${this.deps.agentName}" (add it to the agent's tools/agents declaration)`
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
890
1103
|
if (name.startsWith("agent.")) {
|
|
891
|
-
return this.executeSubAgent(name.slice(6), args, enableTracing);
|
|
1104
|
+
return await this.executeSubAgent(name.slice(6), args, enableTracing);
|
|
892
1105
|
}
|
|
893
1106
|
const tool = this.deps.getTool(name);
|
|
894
1107
|
if (!tool) {
|
|
@@ -897,14 +1110,16 @@ var Agent = class _Agent {
|
|
|
897
1110
|
const schemaRes = await this.getToolSchema(tool);
|
|
898
1111
|
let callArgs = args;
|
|
899
1112
|
if (schemaRes) {
|
|
900
|
-
const
|
|
901
|
-
if (!
|
|
902
|
-
return { error:
|
|
1113
|
+
const result2 = schemaRes.validate(args);
|
|
1114
|
+
if (!result2.ok) {
|
|
1115
|
+
return { error: result2.error };
|
|
903
1116
|
}
|
|
904
|
-
callArgs =
|
|
1117
|
+
callArgs = result2.value ?? args;
|
|
905
1118
|
}
|
|
906
1119
|
const mod = await this.deps.loadToolModule(tool.filePath, tool.functionName);
|
|
907
|
-
|
|
1120
|
+
const result = await mod.handler(callArgs, this.deps.ctx);
|
|
1121
|
+
this.deps.config?.afterToolCall?.(name, args, result, this.deps.ctx);
|
|
1122
|
+
return result;
|
|
908
1123
|
}
|
|
909
1124
|
/**
|
|
910
1125
|
* sub-agent 递归执行
|
|
@@ -941,12 +1156,15 @@ var Agent = class _Agent {
|
|
|
941
1156
|
if (entry?.hasRun) {
|
|
942
1157
|
const mod = await this.deps.loadAgentModule(entry.filePath, entry.hasRun);
|
|
943
1158
|
if (mod.run) {
|
|
944
|
-
|
|
1159
|
+
const result2 = await mod.run(args, this.deps.ctx);
|
|
1160
|
+
this.deps.config?.afterToolCall?.(`agent.${subName}`, args, result2, this.deps.ctx);
|
|
1161
|
+
return result2;
|
|
945
1162
|
}
|
|
946
1163
|
}
|
|
947
1164
|
const result = await subAgent.run(typeof args === "string" ? args : JSON.stringify(args), {
|
|
948
1165
|
enableTracing
|
|
949
1166
|
});
|
|
1167
|
+
this.deps.config?.afterToolCall?.(`agent.${subName}`, args, result.content, this.deps.ctx);
|
|
950
1168
|
if (enableTracing && result.trace) {
|
|
951
1169
|
return {
|
|
952
1170
|
__trace: true,
|
|
@@ -960,16 +1178,12 @@ var Agent = class _Agent {
|
|
|
960
1178
|
|
|
961
1179
|
// src/plugin.ts
|
|
962
1180
|
import {
|
|
963
|
-
registerAgentHandleFactory,
|
|
964
|
-
getAgent,
|
|
965
|
-
getAgentEntry,
|
|
966
|
-
getTool,
|
|
967
|
-
resolveAgentTools,
|
|
968
|
-
resolveSubAgents,
|
|
969
1181
|
loadAgentModule,
|
|
970
1182
|
loadToolModule,
|
|
971
|
-
loadToolSchema
|
|
1183
|
+
loadToolSchema,
|
|
1184
|
+
getToolSchemaPath
|
|
972
1185
|
} from "@faapi/faapi";
|
|
1186
|
+
import { statSync } from "fs";
|
|
973
1187
|
import { z } from "zod";
|
|
974
1188
|
async function resolveToolSchemaImpl(tool, rootDir) {
|
|
975
1189
|
const schemaMod = await loadToolSchema(tool, rootDir);
|
|
@@ -994,6 +1208,7 @@ function readAgentConfig(ctx) {
|
|
|
994
1208
|
var agentPlugin = {
|
|
995
1209
|
name: "@faapi/agent",
|
|
996
1210
|
setup(ctx) {
|
|
1211
|
+
const registries = ctx.registries;
|
|
997
1212
|
const agentConfig = readAgentConfig(ctx);
|
|
998
1213
|
if (!agentConfig?.llms) {
|
|
999
1214
|
console.warn(
|
|
@@ -1001,12 +1216,7 @@ var agentPlugin = {
|
|
|
1001
1216
|
);
|
|
1002
1217
|
return;
|
|
1003
1218
|
}
|
|
1004
|
-
|
|
1005
|
-
console.warn(
|
|
1006
|
-
"! @faapi/agent: config.agent.defaultAgent not configured, agent parameter injection disabled"
|
|
1007
|
-
);
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1219
|
+
const defaultAgent = agentConfig.defaultAgent ?? "";
|
|
1010
1220
|
const llms = agentConfig.llms;
|
|
1011
1221
|
const providers = /* @__PURE__ */ new Map();
|
|
1012
1222
|
for (const [name, llmConfig] of Object.entries(llms)) {
|
|
@@ -1022,28 +1232,50 @@ var agentPlugin = {
|
|
|
1022
1232
|
}
|
|
1023
1233
|
const runtimeConfig = {
|
|
1024
1234
|
maxTurns: agentConfig.maxTurns,
|
|
1025
|
-
maxAgentDepth: agentConfig.maxAgentDepth
|
|
1235
|
+
maxAgentDepth: agentConfig.maxAgentDepth,
|
|
1236
|
+
maxHistoryTokens: agentConfig.maxHistoryTokens,
|
|
1237
|
+
// 鉴权钩子(authHooks,见 ./authHooks.md)——业务方在 config.agent 声明
|
|
1238
|
+
beforeToolCall: agentConfig.beforeToolCall,
|
|
1239
|
+
afterToolCall: agentConfig.afterToolCall,
|
|
1240
|
+
filterTools: agentConfig.filterTools
|
|
1026
1241
|
};
|
|
1027
1242
|
const rootDir = ctx.rootDir;
|
|
1028
|
-
const
|
|
1029
|
-
const resolveToolSchema = (tool) =>
|
|
1030
|
-
|
|
1243
|
+
const schemaCache = /* @__PURE__ */ new Map();
|
|
1244
|
+
const resolveToolSchema = (tool) => {
|
|
1245
|
+
const zodPath = getToolSchemaPath(tool, rootDir);
|
|
1246
|
+
const key = `${zodPath}#${tool.inputTypeName ?? ""}`;
|
|
1247
|
+
let mtimeMs = -1;
|
|
1248
|
+
try {
|
|
1249
|
+
mtimeMs = statSync(zodPath).mtimeMs;
|
|
1250
|
+
} catch {
|
|
1251
|
+
}
|
|
1252
|
+
const hit = schemaCache.get(key);
|
|
1253
|
+
if (hit && hit.mtimeMs === mtimeMs) {
|
|
1254
|
+
return hit.resolution;
|
|
1255
|
+
}
|
|
1256
|
+
const resolution = resolveToolSchemaImpl(tool, rootDir);
|
|
1257
|
+
schemaCache.set(key, { mtimeMs, resolution });
|
|
1258
|
+
return resolution;
|
|
1259
|
+
};
|
|
1260
|
+
ctx.registries.agentHandle.register((ctx2) => {
|
|
1031
1261
|
return new Agent({
|
|
1032
1262
|
providers,
|
|
1033
1263
|
defaultProvider,
|
|
1034
1264
|
llms,
|
|
1035
1265
|
defaultLlm,
|
|
1036
|
-
agentName: defaultAgent,
|
|
1266
|
+
agentName: defaultAgent ?? "",
|
|
1037
1267
|
rootDir,
|
|
1038
1268
|
config: runtimeConfig,
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1269
|
+
// ctx 传递链(authHooks):捕获请求上下文,tool handler / sub-agent /
|
|
1270
|
+
// 鉴权钩子均可读取中间件塞入的身份信息(ctx.user / ctx.workspace 等)
|
|
1271
|
+
ctx: ctx2,
|
|
1272
|
+
// 注册表访问器——app 实例(PluginContext.registries),非全局单例
|
|
1041
1273
|
// getAgent 返回 AgentCore(LLM-facing);getAgentEntry 返回 AgentMetadata(含 filePath/hasRun,供加载 handler.js)
|
|
1042
|
-
getAgent,
|
|
1043
|
-
getAgentEntry,
|
|
1044
|
-
getTool,
|
|
1045
|
-
resolveAgentTools,
|
|
1046
|
-
resolveSubAgents,
|
|
1274
|
+
getAgent: registries.agent.getAgent,
|
|
1275
|
+
getAgentEntry: registries.agent.getAgentEntry,
|
|
1276
|
+
getTool: registries.tool.get,
|
|
1277
|
+
resolveAgentTools: registries.agent.resolveAgentTools,
|
|
1278
|
+
resolveSubAgents: registries.agent.resolveSubAgents,
|
|
1047
1279
|
// 加载器包装:注入 rootDir 用于 dev 按需编译模式
|
|
1048
1280
|
loadToolModule: (filePath, functionName) => loadToolModule(filePath, functionName, rootDir),
|
|
1049
1281
|
loadAgentModule: (filePath, hasRun) => loadAgentModule(filePath, hasRun, rootDir),
|
|
@@ -1052,13 +1284,14 @@ var agentPlugin = {
|
|
|
1052
1284
|
});
|
|
1053
1285
|
});
|
|
1054
1286
|
console.log(
|
|
1055
|
-
`- @faapi/agent: default agent "${defaultAgent}" (provider: ${defaultLlm}) available via agent parameter injection`
|
|
1287
|
+
defaultAgent ? `- @faapi/agent: default agent "${defaultAgent}" (provider: ${defaultLlm}) available via agent parameter injection` : `- @faapi/agent: no defaultAgent set \u2014 use agent.run(input, { agent: 'name' }) to specify agent (provider: ${defaultLlm})`
|
|
1056
1288
|
);
|
|
1057
1289
|
}
|
|
1058
1290
|
};
|
|
1059
1291
|
var plugin_default = agentPlugin;
|
|
1060
1292
|
export {
|
|
1061
1293
|
Agent,
|
|
1294
|
+
AgentAbortError,
|
|
1062
1295
|
AgentError,
|
|
1063
1296
|
AgentRecursionError,
|
|
1064
1297
|
LLMProviderError,
|