@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.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(["provider", "apiKey", "model", "baseURL", "models"]);
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
- async function safeFetch(url, init) {
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
- const reason = err instanceof Error ? err.message : String(err);
66
- throw new LLMProviderError(`Network error: ${reason}`, { cause: err });
89
+ throw classifyAbortError(err, externalSignal);
67
90
  }
68
91
  }
69
- async function ensureOk(response) {
70
- if (response.ok) return "";
71
- const bodyText = await response.text();
72
- const excerpt = bodyText.slice(0, 500);
73
- throw new LLMProviderError(`HTTP ${response.status}: ${excerpt}`, {
74
- status: response.status,
75
- body: excerpt
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 init = {
152
+ const baseInit = {
82
153
  method: "POST",
83
154
  headers: buildHeaders(),
84
155
  body: JSON.stringify(body)
85
156
  };
86
- const response = await safeFetch(url, init);
87
- await ensureOk(response);
88
- const bodyText = await response.text();
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 init = {
200
+ const baseInit = {
125
201
  method: "POST",
126
202
  headers: buildHeaders(),
127
203
  body: JSON.stringify(body)
128
204
  };
129
- const response = await safeFetch(url, init);
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
- let sep;
148
- while ((sep = buffer.indexOf("\n\n")) >= 0) {
149
- const eventStr = buffer.slice(0, sep);
150
- buffer = buffer.slice(sep + 2);
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
- reader.releaseLock();
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("\n")) {
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 ?? true;
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 inputSnapshot = enableTracing ? [...messages] : void 0;
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: [...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
- for (const toolCall of response.message.toolCalls) {
435
- const toolStartedAt = enableTracing ? nowMs() : 0;
436
- let resultStr;
437
- let rawResult;
438
- let toolErr;
439
- let hasError = false;
440
- try {
441
- rawResult = await config.executeTool(toolCall.name, toolCall.arguments);
442
- if (isTracingToolResult(rawResult)) {
443
- resultStr = stringifyResult(rawResult.result);
444
- } else {
445
- resultStr = stringifyResult(rawResult);
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
- } catch (err) {
448
- hasError = true;
449
- toolErr = err;
450
- resultStr = stringifyError(err);
451
- rawResult = void 0;
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 ?? true;
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 inputSnapshot = enableTracing ? [...messages] : void 0;
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: [...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. agent 元数据(未注册抛 AgentError)——用 `getAgent` 拿 AgentCore
935
+ * 1. 解析有效 agent 名:`options.agent` > `deps.agentName`(`config.agent.defaultAgent`)
936
+ * 2. 查 agent 元数据(未注册抛 AgentError)——用 `getAgent` 拿 AgentCore
751
937
  * (LLM-facing 字段:systemPrompt / model / maxTurns)
752
- * 2. buildToolDefinitions 组装 tool 列表
753
- * 3. config 字段优先级(高 → 低):`options` > agent 元数据 > 全局 AgentRuntimeConfig / deps.defaultProvider
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 meta = this.deps.getAgent(this.deps.agentName);
951
+ const agentName = options?.agent ?? this.deps.agentName;
952
+ const meta = this.deps.getAgent(agentName);
762
953
  if (!meta) {
763
- throw new AgentError(`Agent "${this.deps.agentName}" is not registered`);
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 ?? true;
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(this.deps.agentName)) {
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(this.deps.agentName)) {
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
- return Array.from(definitions.values());
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(name, args, enableTracing) {
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 result = schemaRes.validate(args);
901
- if (!result.ok) {
902
- return { error: result.error };
1113
+ const result2 = schemaRes.validate(args);
1114
+ if (!result2.ok) {
1115
+ return { error: result2.error };
903
1116
  }
904
- callArgs = result.value ?? args;
1117
+ callArgs = result2.value ?? args;
905
1118
  }
906
1119
  const mod = await this.deps.loadToolModule(tool.filePath, tool.functionName);
907
- return await mod.handler(callArgs);
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
- return await mod.run(args);
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
- if (!agentConfig.defaultAgent) {
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 defaultAgent = agentConfig.defaultAgent;
1029
- const resolveToolSchema = (tool) => resolveToolSchemaImpl(tool, rootDir);
1030
- registerAgentHandleFactory(() => {
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
- // 注册表/加载器访问器——从 @faapi/faapi import 的单例模块
1040
- // createAppBase 启动时已水合 agentRegistry / toolRegistry
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,