@aigne/core 1.55.0 → 1.56.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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.56.0](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.55.1...core-v1.56.0) (2025-08-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * **models:** add retry mechanism for network errors and structured output validation errors ([#418](https://github.com/AIGNE-io/aigne-framework/issues/418)) ([52bc9ee](https://github.com/AIGNE-io/aigne-framework/commit/52bc9eec5f4f4fa3c3f26881c405f4f89dad01c9))
9
+
10
+ ## [1.55.1](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.55.0...core-v1.55.1) (2025-08-26)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **cli:** reduce memory usage of AIGNE CLI ([#411](https://github.com/AIGNE-io/aigne-framework/issues/411)) ([9c36969](https://github.com/AIGNE-io/aigne-framework/commit/9c369699d966d37abf2d6a1624eac3d2fda4123b))
16
+
17
+
18
+ ### Dependencies
19
+
20
+ * The following workspace dependencies were updated
21
+ * dependencies
22
+ * @aigne/observability-api bumped to 0.10.1
23
+
3
24
  ## [1.55.0](https://github.com/AIGNE-io/aigne-framework/compare/core-v1.54.0...core-v1.55.0) (2025-08-21)
4
25
 
5
26
 
@@ -122,6 +122,7 @@ export interface AgentOptions<I extends Message = Message, O extends Message = M
122
122
  */
123
123
  maxRetrieveMemoryCount?: number;
124
124
  hooks?: AgentHooks<I, O> | AgentHooks<I, O>[];
125
+ retryOnError?: Agent<I, O>["retryOnError"] | boolean;
125
126
  }
126
127
  export declare const agentOptionsSchema: ZodObject<{
127
128
  [key in keyof AgentOptions]: ZodType<AgentOptions[key]>;
@@ -217,6 +218,13 @@ export declare abstract class Agent<I extends Message = any, O extends Message =
217
218
  * {@includeCode ../../test/agents/agent.test.ts#example-agent-hooks}
218
219
  */
219
220
  readonly hooks: AgentHooks<I, O>[];
221
+ retryOnError?: {
222
+ retries?: number;
223
+ minTimeout?: number;
224
+ factor?: number;
225
+ randomize?: boolean;
226
+ shouldRetry?: (error: Error) => boolean | Promise<boolean>;
227
+ };
220
228
  /**
221
229
  * List of GuideRail agents applied to this agent
222
230
  *
@@ -57,6 +57,9 @@ const type_utils_js_1 = require("../utils/type-utils.js");
57
57
  const types_js_1 = require("./types.js");
58
58
  __exportStar(require("./types.js"), exports);
59
59
  exports.DEFAULT_INPUT_ACTION_GET = "$get";
60
+ const DEFAULT_RETRIES = 3;
61
+ const DEFAULT_RETRY_MIN_TIMEOUT = 1000;
62
+ const DEFAULT_RETRY_FACTOR = 2;
60
63
  const hooksSchema = zod_1.z.object({
61
64
  onStart: zod_1.z.custom().optional(),
62
65
  onEnd: zod_1.z.custom().optional(),
@@ -82,6 +85,18 @@ exports.agentOptionsSchema = zod_1.z.object({
82
85
  maxRetrieveMemoryCount: zod_1.z.number().optional(),
83
86
  hooks: zod_1.z.union([zod_1.z.array(hooksSchema), hooksSchema]).optional(),
84
87
  guideRails: zod_1.z.array(zod_1.z.custom()).optional(),
88
+ retryOnError: zod_1.z
89
+ .union([
90
+ zod_1.z.boolean(),
91
+ zod_1.z.object({
92
+ retries: zod_1.z.number().int().min(0),
93
+ minTimeout: zod_1.z.number().min(0).optional(),
94
+ factor: zod_1.z.number().min(1).optional(),
95
+ randomize: zod_1.z.boolean().optional(),
96
+ shouldRetry: zod_1.z.custom().optional(),
97
+ }),
98
+ ])
99
+ .optional(),
85
100
  });
86
101
  /**
87
102
  * Agent is the base class for all agents.
@@ -107,6 +122,7 @@ exports.agentOptionsSchema = zod_1.z.object({
107
122
  */
108
123
  class Agent {
109
124
  constructor(options = {}) {
125
+ (0, type_utils_js_1.checkArguments)("Agent options", exports.agentOptionsSchema, options);
110
126
  const { inputSchema, outputSchema } = options;
111
127
  this.name = options.name || this.constructor.name;
112
128
  this.alias = options.alias;
@@ -135,6 +151,12 @@ class Agent {
135
151
  this.asyncMemoryRecord = options.asyncMemoryRecord;
136
152
  this.maxRetrieveMemoryCount = options.maxRetrieveMemoryCount;
137
153
  this.hooks = (0, type_utils_js_1.flat)(options.hooks);
154
+ this.retryOnError =
155
+ options.retryOnError === false
156
+ ? undefined
157
+ : options.retryOnError === true
158
+ ? { retries: DEFAULT_RETRIES }
159
+ : options.retryOnError;
138
160
  this.guideRails = options.guideRails;
139
161
  }
140
162
  /**
@@ -158,6 +180,7 @@ class Agent {
158
180
  * {@includeCode ../../test/agents/agent.test.ts#example-agent-hooks}
159
181
  */
160
182
  hooks;
183
+ retryOnError;
161
184
  /**
162
185
  * List of GuideRail agents applied to this agent
163
186
  *
@@ -387,6 +410,7 @@ class Agent {
387
410
  }
388
411
  async *processStreamingAndRetry(input, options) {
389
412
  let output = {};
413
+ let attempt = 0;
390
414
  for (;;) {
391
415
  // Reset output to avoid accumulating old data
392
416
  const resetOutput = Object.fromEntries(Object.entries(output).map(([key]) => [key, null]));
@@ -415,6 +439,15 @@ class Agent {
415
439
  break;
416
440
  }
417
441
  catch (error) {
442
+ if (this.retryOnError?.retries) {
443
+ const { retries, minTimeout = DEFAULT_RETRY_MIN_TIMEOUT, factor = DEFAULT_RETRY_FACTOR, randomize = false, shouldRetry, } = this.retryOnError;
444
+ if (attempt++ < retries && (!shouldRetry || (await shouldRetry(error)))) {
445
+ const timeout = minTimeout * factor ** (attempt - 1) * (randomize ? 1 + Math.random() : 1);
446
+ logger_js_1.logger.warn(`Agent ${this.name} attempt ${attempt} of ${retries} failed with error: ${error}. Retrying in ${timeout}ms...`);
447
+ await new Promise((resolve) => setTimeout(resolve, timeout));
448
+ continue;
449
+ }
450
+ }
418
451
  const res = await this.processAgentError(input, error, options);
419
452
  if (!res.retry)
420
453
  throw res.error ?? error;
@@ -506,9 +539,6 @@ class Agent {
506
539
  * @param options Invocation options
507
540
  */
508
541
  async processAgentError(input, error, options) {
509
- if ("$error_has_been_processed" in error && error.$error_has_been_processed)
510
- return {};
511
- Object.defineProperty(error, "$error_has_been_processed", { value: true, enumerable: false });
512
542
  logger_js_1.logger.error("Invoke agent %s failed with error: %O", this.name, error);
513
543
  if (!this.disableEvents)
514
544
  options.context.emit("agentFailed", { agent: this, error });
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
- import type { PromiseOrValue } from "../utils/type-utils.js";
2
+ import { type PromiseOrValue } from "../utils/type-utils.js";
3
3
  import { Agent, type AgentInvokeOptions, type AgentOptions, type AgentProcessResult, type Message } from "./agent.js";
4
+ export declare class StructuredOutputError extends Error {
5
+ }
4
6
  /**
5
7
  * ChatModel is an abstract base class for interacting with Large Language Models (LLMs).
6
8
  *
@@ -1,8 +1,21 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.chatModelOutputUsageSchema = exports.ChatModel = void 0;
6
+ exports.chatModelOutputUsageSchema = exports.ChatModel = exports.StructuredOutputError = void 0;
7
+ const ajv_1 = require("ajv");
8
+ const is_network_error_1 = __importDefault(require("is-network-error"));
4
9
  const zod_1 = require("zod");
10
+ const type_utils_js_1 = require("../utils/type-utils.js");
5
11
  const agent_js_1 = require("./agent.js");
12
+ const CHAT_MODEL_DEFAULT_RETRY_OPTIONS = {
13
+ retries: 3,
14
+ shouldRetry: (error) => error instanceof StructuredOutputError || (0, is_network_error_1.default)(error),
15
+ };
16
+ class StructuredOutputError extends Error {
17
+ }
18
+ exports.StructuredOutputError = StructuredOutputError;
6
19
  /**
7
20
  * ChatModel is an abstract base class for interacting with Large Language Models (LLMs).
8
21
  *
@@ -29,10 +42,21 @@ const agent_js_1 = require("./agent.js");
29
42
  class ChatModel extends agent_js_1.Agent {
30
43
  tag = "ChatModelAgent";
31
44
  constructor(options) {
45
+ if (options)
46
+ (0, type_utils_js_1.checkArguments)("ChatModel", agent_js_1.agentOptionsSchema, options);
47
+ const retryOnError = options?.retryOnError === false
48
+ ? false
49
+ : options?.retryOnError === true
50
+ ? CHAT_MODEL_DEFAULT_RETRY_OPTIONS
51
+ : {
52
+ ...CHAT_MODEL_DEFAULT_RETRY_OPTIONS,
53
+ ...options?.retryOnError,
54
+ };
32
55
  super({
33
56
  ...options,
34
57
  inputSchema: chatModelInputSchema,
35
58
  outputSchema: chatModelOutputSchema,
59
+ retryOnError,
36
60
  });
37
61
  }
38
62
  get credential() {
@@ -137,6 +161,14 @@ class ChatModel extends agent_js_1.Agent {
137
161
  }
138
162
  }
139
163
  }
164
+ if (input.responseFormat?.type === "json_schema" &&
165
+ // NOTE: Should not validate if there are tool calls
166
+ !output.toolCalls?.length) {
167
+ const ajv = new ajv_1.Ajv();
168
+ if (!ajv.validate(input.responseFormat.jsonSchema.schema, output.json)) {
169
+ throw new StructuredOutputError(`Output JSON does not conform to the provided JSON schema: ${ajv.errorsText()}`);
170
+ }
171
+ }
140
172
  super.postprocess(input, output, options);
141
173
  const { usage } = output;
142
174
  if (usage) {
@@ -196,25 +196,34 @@ class TeamAgent extends agent_js_1.Agent {
196
196
  let arr = input[this.iterateOn];
197
197
  arr = Array.isArray(arr) ? [...arr] : (0, type_utils_js_1.isNil)(arr) ? [arr] : [];
198
198
  const results = new Array(arr.length);
199
+ let error;
199
200
  const queue = fastq_1.default.promise(async ({ item, index }) => {
200
- if (!(0, type_utils_js_1.isRecord)(item))
201
- throw new TypeError(`Expected ${String(key)} to be an object, got ${typeof item}`);
202
- const o = await (0, agent_js_1.agentProcessResultToObject)(await this._processNonIterator({ ...input, [key]: arr, ...item }, { ...options, streaming: false }));
203
- const res = (0, type_utils_js_1.omit)(o, key);
204
- // Merge the item result with the original item used for next iteration
205
- if (this.iterateWithPreviousOutput) {
206
- arr = (0, immer_1.produce)(arr, (draft) => {
207
- const item = draft[index];
208
- (0, node_assert_1.default)(item);
209
- Object.assign(item, res);
210
- });
201
+ try {
202
+ if (!(0, type_utils_js_1.isRecord)(item))
203
+ throw new TypeError(`Expected ${String(key)} to be an object, got ${typeof item}`);
204
+ const o = await (0, agent_js_1.agentProcessResultToObject)(await this._processNonIterator({ ...input, [key]: arr, ...item }, { ...options, streaming: false }));
205
+ const res = (0, type_utils_js_1.omit)(o, key);
206
+ // Merge the item result with the original item used for next iteration
207
+ if (this.iterateWithPreviousOutput) {
208
+ arr = (0, immer_1.produce)(arr, (draft) => {
209
+ const item = draft[index];
210
+ (0, node_assert_1.default)(item);
211
+ Object.assign(item, res);
212
+ });
213
+ }
214
+ results[index] = res;
215
+ }
216
+ catch (e) {
217
+ error = e;
218
+ queue.killAndDrain();
211
219
  }
212
- results[index] = res;
213
220
  }, this.concurrency);
214
221
  for (let index = 0; index < arr.length; index++) {
215
222
  queue.push({ index, item: arr[index] });
216
223
  }
217
224
  await queue.drained();
225
+ if (error)
226
+ throw error;
218
227
  yield { delta: { json: { [key]: results } } };
219
228
  }
220
229
  _processNonIterator(input, options) {
@@ -163,7 +163,6 @@ export declare class AIGNEContext implements Context {
163
163
  get observer(): AIGNEObserver | undefined;
164
164
  get limits(): ContextLimits | undefined;
165
165
  get status(): "normal" | "timeout";
166
- get spans(): Span[];
167
166
  get usage(): ContextUsage;
168
167
  get userContext(): Context["userContext"];
169
168
  set userContext(userContext: Context["userContext"]);
@@ -181,7 +180,6 @@ export declare class AIGNEContext implements Context {
181
180
  subscribe: Context["subscribe"];
182
181
  unsubscribe: Context["unsubscribe"];
183
182
  emit<K extends keyof ContextEmitEventMap>(eventName: K, ...args: Args<K, ContextEmitEventMap>): boolean;
184
- private endAllSpans;
185
183
  private trace;
186
184
  on<K extends keyof ContextEventMap>(eventName: K, listener: Listener<K, ContextEventMap>): this;
187
185
  once<K extends keyof ContextEventMap>(eventName: K, listener: Listener<K, ContextEventMap>): this;
@@ -189,7 +187,6 @@ export declare class AIGNEContext implements Context {
189
187
  }
190
188
  declare class AIGNEContextShared {
191
189
  private readonly parent?;
192
- spans: Span[];
193
190
  constructor(parent?: (Pick<Context, "model" | "imageModel" | "agents" | "skills" | "limits" | "observer"> & {
194
191
  messageQueue?: MessageQueue;
195
192
  events?: Emitter<any>;
@@ -202,7 +199,6 @@ declare class AIGNEContextShared {
202
199
  get agents(): Agent<any, any>[];
203
200
  get observer(): AIGNEObserver | undefined;
204
201
  get limits(): ContextLimits | undefined;
205
- addSpan(span: Span): void;
206
202
  usage: ContextUsage;
207
203
  userContext: Context["userContext"];
208
204
  memories: Context["memories"];
@@ -45,9 +45,6 @@ class AIGNEContext {
45
45
  // 修改了 rootId 是否会之前的有影响?,之前为 this.id
46
46
  this.rootId = this.span?.spanContext?.().traceId ?? (0, uuid_1.v7)();
47
47
  }
48
- if (this.span) {
49
- this.internal.addSpan(this.span);
50
- }
51
48
  this.id = this.span?.spanContext()?.spanId ?? (0, uuid_1.v7)();
52
49
  }
53
50
  id;
@@ -79,9 +76,6 @@ class AIGNEContext {
79
76
  get status() {
80
77
  return this.internal.status;
81
78
  }
82
- get spans() {
83
- return this.internal.spans;
84
- }
85
79
  get usage() {
86
80
  return this.internal.usage;
87
81
  }
@@ -122,18 +116,12 @@ class AIGNEContext {
122
116
  const newContext = options?.newContext === false ? this : this.newContext();
123
117
  return Promise.resolve(newContext.internal.invoke(agent, message, newContext, options)).then(async (response) => {
124
118
  if (!options?.streaming) {
125
- try {
126
- let { __activeAgent__: activeAgent, ...output } = await (0, stream_utils_js_1.agentResponseStreamToObject)(response);
127
- output = await this.onInvocationResult(output, options);
128
- if (options?.returnActiveAgent) {
129
- return [output, activeAgent];
130
- }
131
- return output;
132
- }
133
- catch (error) {
134
- this.endAllSpans(error);
135
- throw error;
119
+ let { __activeAgent__: activeAgent, ...output } = await (0, stream_utils_js_1.agentResponseStreamToObject)(response);
120
+ output = await this.onInvocationResult(output, options);
121
+ if (options?.returnActiveAgent) {
122
+ return [output, activeAgent];
136
123
  }
124
+ return output;
137
125
  }
138
126
  const activeAgentPromise = (0, promise_js_1.promiseWithResolvers)();
139
127
  const stream = (0, stream_utils_js_1.onAgentResponseStreamEnd)((0, stream_utils_js_1.asyncGeneratorToReadableStream)(response), {
@@ -152,10 +140,6 @@ class AIGNEContext {
152
140
  activeAgentPromise.resolve(output.__activeAgent__);
153
141
  return await this.onInvocationResult(output, options);
154
142
  },
155
- onError: (error) => {
156
- this.endAllSpans(error);
157
- return error;
158
- },
159
143
  });
160
144
  const finalStream = !options.returnProgressChunks
161
145
  ? stream
@@ -217,12 +201,6 @@ class AIGNEContext {
217
201
  this.trace(eventName, args, b);
218
202
  return this.internal.events.emit(eventName, ...newArgs);
219
203
  }
220
- async endAllSpans(error) {
221
- this.spans.forEach((span) => {
222
- span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error?.message });
223
- span.end();
224
- });
225
- }
226
204
  async trace(eventName, args, b) {
227
205
  const span = this.span;
228
206
  if (!span)
@@ -298,7 +276,6 @@ class AIGNEContext {
298
276
  exports.AIGNEContext = AIGNEContext;
299
277
  class AIGNEContextShared {
300
278
  parent;
301
- spans = [];
302
279
  constructor(parent) {
303
280
  this.parent = parent;
304
281
  this.messageQueue = this.parent?.messageQueue ?? new message_queue_js_1.MessageQueue();
@@ -324,9 +301,6 @@ class AIGNEContextShared {
324
301
  get limits() {
325
302
  return this.parent?.limits;
326
303
  }
327
- addSpan(span) {
328
- this.spans.push(span);
329
- }
330
304
  usage = (0, usage_js_1.newEmptyContextUsage)();
331
305
  userContext = {};
332
306
  memories = [];
@@ -122,6 +122,7 @@ export interface AgentOptions<I extends Message = Message, O extends Message = M
122
122
  */
123
123
  maxRetrieveMemoryCount?: number;
124
124
  hooks?: AgentHooks<I, O> | AgentHooks<I, O>[];
125
+ retryOnError?: Agent<I, O>["retryOnError"] | boolean;
125
126
  }
126
127
  export declare const agentOptionsSchema: ZodObject<{
127
128
  [key in keyof AgentOptions]: ZodType<AgentOptions[key]>;
@@ -217,6 +218,13 @@ export declare abstract class Agent<I extends Message = any, O extends Message =
217
218
  * {@includeCode ../../test/agents/agent.test.ts#example-agent-hooks}
218
219
  */
219
220
  readonly hooks: AgentHooks<I, O>[];
221
+ retryOnError?: {
222
+ retries?: number;
223
+ minTimeout?: number;
224
+ factor?: number;
225
+ randomize?: boolean;
226
+ shouldRetry?: (error: Error) => boolean | Promise<boolean>;
227
+ };
220
228
  /**
221
229
  * List of GuideRail agents applied to this agent
222
230
  *
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
- import type { PromiseOrValue } from "../utils/type-utils.js";
2
+ import { type PromiseOrValue } from "../utils/type-utils.js";
3
3
  import { Agent, type AgentInvokeOptions, type AgentOptions, type AgentProcessResult, type Message } from "./agent.js";
4
+ export declare class StructuredOutputError extends Error {
5
+ }
4
6
  /**
5
7
  * ChatModel is an abstract base class for interacting with Large Language Models (LLMs).
6
8
  *
@@ -163,7 +163,6 @@ export declare class AIGNEContext implements Context {
163
163
  get observer(): AIGNEObserver | undefined;
164
164
  get limits(): ContextLimits | undefined;
165
165
  get status(): "normal" | "timeout";
166
- get spans(): Span[];
167
166
  get usage(): ContextUsage;
168
167
  get userContext(): Context["userContext"];
169
168
  set userContext(userContext: Context["userContext"]);
@@ -181,7 +180,6 @@ export declare class AIGNEContext implements Context {
181
180
  subscribe: Context["subscribe"];
182
181
  unsubscribe: Context["unsubscribe"];
183
182
  emit<K extends keyof ContextEmitEventMap>(eventName: K, ...args: Args<K, ContextEmitEventMap>): boolean;
184
- private endAllSpans;
185
183
  private trace;
186
184
  on<K extends keyof ContextEventMap>(eventName: K, listener: Listener<K, ContextEventMap>): this;
187
185
  once<K extends keyof ContextEventMap>(eventName: K, listener: Listener<K, ContextEventMap>): this;
@@ -189,7 +187,6 @@ export declare class AIGNEContext implements Context {
189
187
  }
190
188
  declare class AIGNEContextShared {
191
189
  private readonly parent?;
192
- spans: Span[];
193
190
  constructor(parent?: (Pick<Context, "model" | "imageModel" | "agents" | "skills" | "limits" | "observer"> & {
194
191
  messageQueue?: MessageQueue;
195
192
  events?: Emitter<any>;
@@ -202,7 +199,6 @@ declare class AIGNEContextShared {
202
199
  get agents(): Agent<any, any>[];
203
200
  get observer(): AIGNEObserver | undefined;
204
201
  get limits(): ContextLimits | undefined;
205
- addSpan(span: Span): void;
206
202
  usage: ContextUsage;
207
203
  userContext: Context["userContext"];
208
204
  memories: Context["memories"];
@@ -122,6 +122,7 @@ export interface AgentOptions<I extends Message = Message, O extends Message = M
122
122
  */
123
123
  maxRetrieveMemoryCount?: number;
124
124
  hooks?: AgentHooks<I, O> | AgentHooks<I, O>[];
125
+ retryOnError?: Agent<I, O>["retryOnError"] | boolean;
125
126
  }
126
127
  export declare const agentOptionsSchema: ZodObject<{
127
128
  [key in keyof AgentOptions]: ZodType<AgentOptions[key]>;
@@ -217,6 +218,13 @@ export declare abstract class Agent<I extends Message = any, O extends Message =
217
218
  * {@includeCode ../../test/agents/agent.test.ts#example-agent-hooks}
218
219
  */
219
220
  readonly hooks: AgentHooks<I, O>[];
221
+ retryOnError?: {
222
+ retries?: number;
223
+ minTimeout?: number;
224
+ factor?: number;
225
+ randomize?: boolean;
226
+ shouldRetry?: (error: Error) => boolean | Promise<boolean>;
227
+ };
220
228
  /**
221
229
  * List of GuideRail agents applied to this agent
222
230
  *
@@ -9,6 +9,9 @@ import { checkArguments, createAccessorArray, flat, isEmpty, isNil, isNonNullabl
9
9
  import { replaceTransferAgentToName, transferToAgentOutput, } from "./types.js";
10
10
  export * from "./types.js";
11
11
  export const DEFAULT_INPUT_ACTION_GET = "$get";
12
+ const DEFAULT_RETRIES = 3;
13
+ const DEFAULT_RETRY_MIN_TIMEOUT = 1000;
14
+ const DEFAULT_RETRY_FACTOR = 2;
12
15
  const hooksSchema = z.object({
13
16
  onStart: z.custom().optional(),
14
17
  onEnd: z.custom().optional(),
@@ -34,6 +37,18 @@ export const agentOptionsSchema = z.object({
34
37
  maxRetrieveMemoryCount: z.number().optional(),
35
38
  hooks: z.union([z.array(hooksSchema), hooksSchema]).optional(),
36
39
  guideRails: z.array(z.custom()).optional(),
40
+ retryOnError: z
41
+ .union([
42
+ z.boolean(),
43
+ z.object({
44
+ retries: z.number().int().min(0),
45
+ minTimeout: z.number().min(0).optional(),
46
+ factor: z.number().min(1).optional(),
47
+ randomize: z.boolean().optional(),
48
+ shouldRetry: z.custom().optional(),
49
+ }),
50
+ ])
51
+ .optional(),
37
52
  });
38
53
  /**
39
54
  * Agent is the base class for all agents.
@@ -59,6 +74,7 @@ export const agentOptionsSchema = z.object({
59
74
  */
60
75
  export class Agent {
61
76
  constructor(options = {}) {
77
+ checkArguments("Agent options", agentOptionsSchema, options);
62
78
  const { inputSchema, outputSchema } = options;
63
79
  this.name = options.name || this.constructor.name;
64
80
  this.alias = options.alias;
@@ -87,6 +103,12 @@ export class Agent {
87
103
  this.asyncMemoryRecord = options.asyncMemoryRecord;
88
104
  this.maxRetrieveMemoryCount = options.maxRetrieveMemoryCount;
89
105
  this.hooks = flat(options.hooks);
106
+ this.retryOnError =
107
+ options.retryOnError === false
108
+ ? undefined
109
+ : options.retryOnError === true
110
+ ? { retries: DEFAULT_RETRIES }
111
+ : options.retryOnError;
90
112
  this.guideRails = options.guideRails;
91
113
  }
92
114
  /**
@@ -110,6 +132,7 @@ export class Agent {
110
132
  * {@includeCode ../../test/agents/agent.test.ts#example-agent-hooks}
111
133
  */
112
134
  hooks;
135
+ retryOnError;
113
136
  /**
114
137
  * List of GuideRail agents applied to this agent
115
138
  *
@@ -339,6 +362,7 @@ export class Agent {
339
362
  }
340
363
  async *processStreamingAndRetry(input, options) {
341
364
  let output = {};
365
+ let attempt = 0;
342
366
  for (;;) {
343
367
  // Reset output to avoid accumulating old data
344
368
  const resetOutput = Object.fromEntries(Object.entries(output).map(([key]) => [key, null]));
@@ -367,6 +391,15 @@ export class Agent {
367
391
  break;
368
392
  }
369
393
  catch (error) {
394
+ if (this.retryOnError?.retries) {
395
+ const { retries, minTimeout = DEFAULT_RETRY_MIN_TIMEOUT, factor = DEFAULT_RETRY_FACTOR, randomize = false, shouldRetry, } = this.retryOnError;
396
+ if (attempt++ < retries && (!shouldRetry || (await shouldRetry(error)))) {
397
+ const timeout = minTimeout * factor ** (attempt - 1) * (randomize ? 1 + Math.random() : 1);
398
+ logger.warn(`Agent ${this.name} attempt ${attempt} of ${retries} failed with error: ${error}. Retrying in ${timeout}ms...`);
399
+ await new Promise((resolve) => setTimeout(resolve, timeout));
400
+ continue;
401
+ }
402
+ }
370
403
  const res = await this.processAgentError(input, error, options);
371
404
  if (!res.retry)
372
405
  throw res.error ?? error;
@@ -458,9 +491,6 @@ export class Agent {
458
491
  * @param options Invocation options
459
492
  */
460
493
  async processAgentError(input, error, options) {
461
- if ("$error_has_been_processed" in error && error.$error_has_been_processed)
462
- return {};
463
- Object.defineProperty(error, "$error_has_been_processed", { value: true, enumerable: false });
464
494
  logger.error("Invoke agent %s failed with error: %O", this.name, error);
465
495
  if (!this.disableEvents)
466
496
  options.context.emit("agentFailed", { agent: this, error });
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
- import type { PromiseOrValue } from "../utils/type-utils.js";
2
+ import { type PromiseOrValue } from "../utils/type-utils.js";
3
3
  import { Agent, type AgentInvokeOptions, type AgentOptions, type AgentProcessResult, type Message } from "./agent.js";
4
+ export declare class StructuredOutputError extends Error {
5
+ }
4
6
  /**
5
7
  * ChatModel is an abstract base class for interacting with Large Language Models (LLMs).
6
8
  *
@@ -1,5 +1,14 @@
1
+ import { Ajv } from "ajv";
2
+ import isNetworkError from "is-network-error";
1
3
  import { z } from "zod";
2
- import { Agent, } from "./agent.js";
4
+ import { checkArguments } from "../utils/type-utils.js";
5
+ import { Agent, agentOptionsSchema, } from "./agent.js";
6
+ const CHAT_MODEL_DEFAULT_RETRY_OPTIONS = {
7
+ retries: 3,
8
+ shouldRetry: (error) => error instanceof StructuredOutputError || isNetworkError(error),
9
+ };
10
+ export class StructuredOutputError extends Error {
11
+ }
3
12
  /**
4
13
  * ChatModel is an abstract base class for interacting with Large Language Models (LLMs).
5
14
  *
@@ -26,10 +35,21 @@ import { Agent, } from "./agent.js";
26
35
  export class ChatModel extends Agent {
27
36
  tag = "ChatModelAgent";
28
37
  constructor(options) {
38
+ if (options)
39
+ checkArguments("ChatModel", agentOptionsSchema, options);
40
+ const retryOnError = options?.retryOnError === false
41
+ ? false
42
+ : options?.retryOnError === true
43
+ ? CHAT_MODEL_DEFAULT_RETRY_OPTIONS
44
+ : {
45
+ ...CHAT_MODEL_DEFAULT_RETRY_OPTIONS,
46
+ ...options?.retryOnError,
47
+ };
29
48
  super({
30
49
  ...options,
31
50
  inputSchema: chatModelInputSchema,
32
51
  outputSchema: chatModelOutputSchema,
52
+ retryOnError,
33
53
  });
34
54
  }
35
55
  get credential() {
@@ -134,6 +154,14 @@ export class ChatModel extends Agent {
134
154
  }
135
155
  }
136
156
  }
157
+ if (input.responseFormat?.type === "json_schema" &&
158
+ // NOTE: Should not validate if there are tool calls
159
+ !output.toolCalls?.length) {
160
+ const ajv = new Ajv();
161
+ if (!ajv.validate(input.responseFormat.jsonSchema.schema, output.json)) {
162
+ throw new StructuredOutputError(`Output JSON does not conform to the provided JSON schema: ${ajv.errorsText()}`);
163
+ }
164
+ }
137
165
  super.postprocess(input, output, options);
138
166
  const { usage } = output;
139
167
  if (usage) {
@@ -190,25 +190,34 @@ export class TeamAgent extends Agent {
190
190
  let arr = input[this.iterateOn];
191
191
  arr = Array.isArray(arr) ? [...arr] : isNil(arr) ? [arr] : [];
192
192
  const results = new Array(arr.length);
193
+ let error;
193
194
  const queue = fastq.promise(async ({ item, index }) => {
194
- if (!isRecord(item))
195
- throw new TypeError(`Expected ${String(key)} to be an object, got ${typeof item}`);
196
- const o = await agentProcessResultToObject(await this._processNonIterator({ ...input, [key]: arr, ...item }, { ...options, streaming: false }));
197
- const res = omit(o, key);
198
- // Merge the item result with the original item used for next iteration
199
- if (this.iterateWithPreviousOutput) {
200
- arr = produce(arr, (draft) => {
201
- const item = draft[index];
202
- assert(item);
203
- Object.assign(item, res);
204
- });
195
+ try {
196
+ if (!isRecord(item))
197
+ throw new TypeError(`Expected ${String(key)} to be an object, got ${typeof item}`);
198
+ const o = await agentProcessResultToObject(await this._processNonIterator({ ...input, [key]: arr, ...item }, { ...options, streaming: false }));
199
+ const res = omit(o, key);
200
+ // Merge the item result with the original item used for next iteration
201
+ if (this.iterateWithPreviousOutput) {
202
+ arr = produce(arr, (draft) => {
203
+ const item = draft[index];
204
+ assert(item);
205
+ Object.assign(item, res);
206
+ });
207
+ }
208
+ results[index] = res;
209
+ }
210
+ catch (e) {
211
+ error = e;
212
+ queue.killAndDrain();
205
213
  }
206
- results[index] = res;
207
214
  }, this.concurrency);
208
215
  for (let index = 0; index < arr.length; index++) {
209
216
  queue.push({ index, item: arr[index] });
210
217
  }
211
218
  await queue.drained();
219
+ if (error)
220
+ throw error;
212
221
  yield { delta: { json: { [key]: results } } };
213
222
  }
214
223
  _processNonIterator(input, options) {
@@ -163,7 +163,6 @@ export declare class AIGNEContext implements Context {
163
163
  get observer(): AIGNEObserver | undefined;
164
164
  get limits(): ContextLimits | undefined;
165
165
  get status(): "normal" | "timeout";
166
- get spans(): Span[];
167
166
  get usage(): ContextUsage;
168
167
  get userContext(): Context["userContext"];
169
168
  set userContext(userContext: Context["userContext"]);
@@ -181,7 +180,6 @@ export declare class AIGNEContext implements Context {
181
180
  subscribe: Context["subscribe"];
182
181
  unsubscribe: Context["unsubscribe"];
183
182
  emit<K extends keyof ContextEmitEventMap>(eventName: K, ...args: Args<K, ContextEmitEventMap>): boolean;
184
- private endAllSpans;
185
183
  private trace;
186
184
  on<K extends keyof ContextEventMap>(eventName: K, listener: Listener<K, ContextEventMap>): this;
187
185
  once<K extends keyof ContextEventMap>(eventName: K, listener: Listener<K, ContextEventMap>): this;
@@ -189,7 +187,6 @@ export declare class AIGNEContext implements Context {
189
187
  }
190
188
  declare class AIGNEContextShared {
191
189
  private readonly parent?;
192
- spans: Span[];
193
190
  constructor(parent?: (Pick<Context, "model" | "imageModel" | "agents" | "skills" | "limits" | "observer"> & {
194
191
  messageQueue?: MessageQueue;
195
192
  events?: Emitter<any>;
@@ -202,7 +199,6 @@ declare class AIGNEContextShared {
202
199
  get agents(): Agent<any, any>[];
203
200
  get observer(): AIGNEObserver | undefined;
204
201
  get limits(): ContextLimits | undefined;
205
- addSpan(span: Span): void;
206
202
  usage: ContextUsage;
207
203
  userContext: Context["userContext"];
208
204
  memories: Context["memories"];
@@ -39,9 +39,6 @@ export class AIGNEContext {
39
39
  // 修改了 rootId 是否会之前的有影响?,之前为 this.id
40
40
  this.rootId = this.span?.spanContext?.().traceId ?? v7();
41
41
  }
42
- if (this.span) {
43
- this.internal.addSpan(this.span);
44
- }
45
42
  this.id = this.span?.spanContext()?.spanId ?? v7();
46
43
  }
47
44
  id;
@@ -73,9 +70,6 @@ export class AIGNEContext {
73
70
  get status() {
74
71
  return this.internal.status;
75
72
  }
76
- get spans() {
77
- return this.internal.spans;
78
- }
79
73
  get usage() {
80
74
  return this.internal.usage;
81
75
  }
@@ -116,18 +110,12 @@ export class AIGNEContext {
116
110
  const newContext = options?.newContext === false ? this : this.newContext();
117
111
  return Promise.resolve(newContext.internal.invoke(agent, message, newContext, options)).then(async (response) => {
118
112
  if (!options?.streaming) {
119
- try {
120
- let { __activeAgent__: activeAgent, ...output } = await agentResponseStreamToObject(response);
121
- output = await this.onInvocationResult(output, options);
122
- if (options?.returnActiveAgent) {
123
- return [output, activeAgent];
124
- }
125
- return output;
126
- }
127
- catch (error) {
128
- this.endAllSpans(error);
129
- throw error;
113
+ let { __activeAgent__: activeAgent, ...output } = await agentResponseStreamToObject(response);
114
+ output = await this.onInvocationResult(output, options);
115
+ if (options?.returnActiveAgent) {
116
+ return [output, activeAgent];
130
117
  }
118
+ return output;
131
119
  }
132
120
  const activeAgentPromise = promiseWithResolvers();
133
121
  const stream = onAgentResponseStreamEnd(asyncGeneratorToReadableStream(response), {
@@ -146,10 +134,6 @@ export class AIGNEContext {
146
134
  activeAgentPromise.resolve(output.__activeAgent__);
147
135
  return await this.onInvocationResult(output, options);
148
136
  },
149
- onError: (error) => {
150
- this.endAllSpans(error);
151
- return error;
152
- },
153
137
  });
154
138
  const finalStream = !options.returnProgressChunks
155
139
  ? stream
@@ -211,12 +195,6 @@ export class AIGNEContext {
211
195
  this.trace(eventName, args, b);
212
196
  return this.internal.events.emit(eventName, ...newArgs);
213
197
  }
214
- async endAllSpans(error) {
215
- this.spans.forEach((span) => {
216
- span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message });
217
- span.end();
218
- });
219
- }
220
198
  async trace(eventName, args, b) {
221
199
  const span = this.span;
222
200
  if (!span)
@@ -291,7 +269,6 @@ export class AIGNEContext {
291
269
  }
292
270
  class AIGNEContextShared {
293
271
  parent;
294
- spans = [];
295
272
  constructor(parent) {
296
273
  this.parent = parent;
297
274
  this.messageQueue = this.parent?.messageQueue ?? new MessageQueue();
@@ -317,9 +294,6 @@ class AIGNEContextShared {
317
294
  get limits() {
318
295
  return this.parent?.limits;
319
296
  }
320
- addSpan(span) {
321
- this.spans.push(span);
322
- }
323
297
  usage = newEmptyContextUsage();
324
298
  userContext = {};
325
299
  memories = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/core",
3
- "version": "1.55.0",
3
+ "version": "1.56.0",
4
4
  "description": "The functional core of agentic AI",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -71,6 +71,7 @@
71
71
  "@opentelemetry/api": "^1.9.0",
72
72
  "@opentelemetry/sdk-trace-base": "^2.0.1",
73
73
  "@types/debug": "^4.1.12",
74
+ "ajv": "^8.17.1",
74
75
  "camelize-ts": "^3.0.0",
75
76
  "content-type": "^1.0.5",
76
77
  "debug": "^4.4.1",
@@ -78,6 +79,7 @@
78
79
  "fast-deep-equal": "^3.1.3",
79
80
  "fastq": "^1.19.1",
80
81
  "immer": "^10.1.1",
82
+ "is-network-error": "^1.1.0",
81
83
  "jaison": "^2.0.2",
82
84
  "jsonata": "^2.0.6",
83
85
  "nunjucks": "^3.2.4",
@@ -89,7 +91,7 @@
89
91
  "yaml": "^2.8.0",
90
92
  "zod": "^3.25.67",
91
93
  "zod-to-json-schema": "^3.24.6",
92
- "@aigne/observability-api": "^0.10.0",
94
+ "@aigne/observability-api": "^0.10.1",
93
95
  "@aigne/platform-helpers": "^0.6.2"
94
96
  },
95
97
  "devDependencies": {