@prestyj/agent 4.2.15 → 4.2.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Nolan Grout
3
+ Copyright (c) 2025 KenKai
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  Give an LLM tools. It calls them. Results go back in. It loops until it's done. That's it.
13
13
 
14
- Built on top of [`@prestyj/ai`](../ai/README.md). Part of the [EZCoder](../../README.md) monorepo.
14
+ Built on top of [`@prestyj/ai`](../ai/README.md). Part of the [EZCoder Framework](../../README.md) monorepo.
15
15
 
16
16
  ---
17
17
 
package/dist/index.cjs CHANGED
@@ -23,6 +23,8 @@ __export(index_exports, {
23
23
  Agent: () => Agent,
24
24
  AgentStream: () => AgentStream,
25
25
  agentLoop: () => agentLoop,
26
+ isAbortError: () => isAbortError,
27
+ isBillingError: () => isBillingError,
26
28
  isContextOverflow: () => isContextOverflow
27
29
  });
28
30
  module.exports = __toCommonJS(index_exports);
@@ -33,17 +35,26 @@ var import_ai2 = require("@prestyj/ai");
33
35
  // src/agent-loop.ts
34
36
  var import_ai = require("@prestyj/ai");
35
37
  var DEFAULT_MAX_TURNS = 100;
38
+ function isAbortError(err) {
39
+ if (!(err instanceof Error)) return false;
40
+ if (err.name === "AbortError") return true;
41
+ const msg = err.message.toLowerCase();
42
+ return msg.includes("aborted") || msg.includes("abort");
43
+ }
36
44
  function isContextOverflow(err) {
37
45
  if (!(err instanceof Error)) return false;
38
46
  const msg = err.message.toLowerCase();
39
47
  return msg.includes("prompt is too long") || msg.includes("context_length_exceeded") || msg.includes("maximum context length") || msg.includes("token") && msg.includes("exceed");
40
48
  }
49
+ function isBillingError(err) {
50
+ if (!(err instanceof Error)) return false;
51
+ const msg = err.message.toLowerCase();
52
+ return msg.includes("insufficient balance") || msg.includes("no resource package") || msg.includes("quota exceeded") || msg.includes("billing") || msg.includes("recharge");
53
+ }
41
54
  function isOverloaded(err) {
42
55
  if (!(err instanceof Error)) return false;
56
+ if (isBillingError(err)) return false;
43
57
  const msg = err.message.toLowerCase();
44
- if (msg.includes("insufficient balance") || msg.includes("quota exceeded") || msg.includes("billing")) {
45
- return false;
46
- }
47
58
  return msg.includes("overloaded") || msg.includes("rate limit") || msg.includes("too many requests") || msg.includes("429") || msg.includes("529");
48
59
  }
49
60
  async function* agentLoop(messages, options) {
@@ -55,217 +66,263 @@ async function* agentLoop(messages, options) {
55
66
  let consecutivePauses = 0;
56
67
  let overflowRetries = 0;
57
68
  let overloadRetries = 0;
69
+ let emptyResponseRetries = 0;
58
70
  const MAX_OVERFLOW_RETRIES = 3;
59
71
  const MAX_OVERLOAD_RETRIES = 3;
72
+ const MAX_EMPTY_RESPONSE_RETRIES = 3;
60
73
  const OVERLOAD_RETRY_DELAY_MS = 3e3;
61
- while (turn < maxTurns) {
62
- options.signal?.throwIfAborted();
63
- turn++;
64
- if (options.transformContext) {
65
- const transformed = await options.transformContext(messages);
66
- if (transformed !== messages) {
67
- messages.length = 0;
68
- messages.push(...transformed);
69
- }
70
- }
71
- let response;
72
- try {
73
- const result = (0, import_ai.stream)({
74
- provider: options.provider,
75
- model: options.model,
76
- messages,
77
- tools: options.tools,
78
- serverTools: options.serverTools,
79
- webSearch: options.webSearch,
80
- maxTokens: options.maxTokens,
81
- temperature: options.temperature,
82
- thinking: options.thinking,
83
- apiKey: options.apiKey,
84
- baseUrl: options.baseUrl,
85
- signal: options.signal,
86
- accountId: options.accountId,
87
- cacheRetention: options.cacheRetention,
88
- compaction: options.compaction
89
- });
90
- result.response.catch(() => {
91
- });
92
- for await (const event of result) {
93
- if (event.type === "text_delta") {
94
- yield { type: "text_delta", text: event.text };
95
- } else if (event.type === "thinking_delta") {
96
- yield { type: "thinking_delta", text: event.text };
97
- } else if (event.type === "server_toolcall") {
98
- yield {
99
- type: "server_tool_call",
100
- id: event.id,
101
- name: event.name,
102
- input: event.input
103
- };
104
- } else if (event.type === "server_toolresult") {
105
- yield {
106
- type: "server_tool_result",
107
- toolUseId: event.toolUseId,
108
- resultType: event.resultType,
109
- data: event.data
110
- };
111
- }
112
- }
113
- response = await result.response;
114
- } catch (err) {
115
- if (overflowRetries < MAX_OVERFLOW_RETRIES && isContextOverflow(err) && options.transformContext) {
116
- overflowRetries++;
117
- const transformed = await options.transformContext(messages, { force: true });
74
+ try {
75
+ while (turn < maxTurns) {
76
+ options.signal?.throwIfAborted();
77
+ turn++;
78
+ if (options.transformContext) {
79
+ const transformed = await options.transformContext(messages);
118
80
  if (transformed !== messages) {
119
81
  messages.length = 0;
120
82
  messages.push(...transformed);
121
83
  }
122
- turn--;
123
- continue;
124
84
  }
125
- if (overloadRetries < MAX_OVERLOAD_RETRIES && isOverloaded(err)) {
126
- overloadRetries++;
127
- await new Promise((r) => setTimeout(r, OVERLOAD_RETRY_DELAY_MS));
128
- turn--;
129
- continue;
85
+ let response;
86
+ try {
87
+ const result = (0, import_ai.stream)({
88
+ provider: options.provider,
89
+ model: options.model,
90
+ messages,
91
+ tools: options.tools,
92
+ serverTools: options.serverTools,
93
+ webSearch: options.webSearch,
94
+ maxTokens: options.maxTokens,
95
+ temperature: options.temperature,
96
+ thinking: options.thinking,
97
+ apiKey: options.apiKey,
98
+ baseUrl: options.baseUrl,
99
+ signal: options.signal,
100
+ accountId: options.accountId,
101
+ cacheRetention: options.cacheRetention,
102
+ compaction: options.compaction
103
+ });
104
+ result.response.catch(() => {
105
+ });
106
+ for await (const event of result) {
107
+ if (event.type === "text_delta") {
108
+ yield { type: "text_delta", text: event.text };
109
+ } else if (event.type === "thinking_delta") {
110
+ yield { type: "thinking_delta", text: event.text };
111
+ } else if (event.type === "server_toolcall") {
112
+ yield {
113
+ type: "server_tool_call",
114
+ id: event.id,
115
+ name: event.name,
116
+ input: event.input
117
+ };
118
+ } else if (event.type === "server_toolresult") {
119
+ yield {
120
+ type: "server_tool_result",
121
+ toolUseId: event.toolUseId,
122
+ resultType: event.resultType,
123
+ data: event.data
124
+ };
125
+ }
126
+ }
127
+ response = await result.response;
128
+ } catch (err) {
129
+ if (overflowRetries < MAX_OVERFLOW_RETRIES && isContextOverflow(err) && options.transformContext) {
130
+ overflowRetries++;
131
+ const transformed = await options.transformContext(messages, { force: true });
132
+ if (transformed !== messages) {
133
+ messages.length = 0;
134
+ messages.push(...transformed);
135
+ }
136
+ turn--;
137
+ continue;
138
+ }
139
+ if (overloadRetries < MAX_OVERLOAD_RETRIES && isOverloaded(err)) {
140
+ overloadRetries++;
141
+ await new Promise((r) => setTimeout(r, OVERLOAD_RETRY_DELAY_MS));
142
+ turn--;
143
+ continue;
144
+ }
145
+ if (isAbortError(err) || options.signal?.aborted) {
146
+ break;
147
+ }
148
+ throw err;
130
149
  }
131
- throw err;
132
- }
133
- overflowRetries = 0;
134
- overloadRetries = 0;
135
- totalUsage.inputTokens += response.usage.inputTokens;
136
- totalUsage.outputTokens += response.usage.outputTokens;
137
- if (response.usage.cacheRead) {
138
- totalUsage.cacheRead = (totalUsage.cacheRead ?? 0) + response.usage.cacheRead;
139
- }
140
- if (response.usage.cacheWrite) {
141
- totalUsage.cacheWrite = (totalUsage.cacheWrite ?? 0) + response.usage.cacheWrite;
142
- }
143
- messages.push(response.message);
144
- yield {
145
- type: "turn_end",
146
- turn,
147
- stopReason: response.stopReason,
148
- usage: response.usage
149
- };
150
- if (response.stopReason === "pause_turn") {
151
- consecutivePauses++;
152
- if (consecutivePauses >= maxContinuations) {
153
- break;
150
+ overflowRetries = 0;
151
+ overloadRetries = 0;
152
+ if (response.usage.outputTokens === 0 && (response.message.content === "" || Array.isArray(response.message.content) && response.message.content.length === 0)) {
153
+ if (emptyResponseRetries < MAX_EMPTY_RESPONSE_RETRIES) {
154
+ emptyResponseRetries++;
155
+ turn--;
156
+ continue;
157
+ }
154
158
  }
155
- continue;
156
- }
157
- consecutivePauses = 0;
158
- if (response.stopReason !== "tool_use") {
159
+ emptyResponseRetries = 0;
160
+ totalUsage.inputTokens += response.usage.inputTokens;
161
+ totalUsage.outputTokens += response.usage.outputTokens;
162
+ if (response.usage.cacheRead) {
163
+ totalUsage.cacheRead = (totalUsage.cacheRead ?? 0) + response.usage.cacheRead;
164
+ }
165
+ if (response.usage.cacheWrite) {
166
+ totalUsage.cacheWrite = (totalUsage.cacheWrite ?? 0) + response.usage.cacheWrite;
167
+ }
168
+ messages.push(response.message);
159
169
  yield {
160
- type: "agent_done",
161
- totalTurns: turn,
162
- totalUsage: { ...totalUsage }
163
- };
164
- return {
165
- message: response.message,
166
- totalTurns: turn,
167
- totalUsage: { ...totalUsage }
170
+ type: "turn_end",
171
+ turn,
172
+ stopReason: response.stopReason,
173
+ usage: response.usage
168
174
  };
169
- }
170
- const allToolCalls = extractToolCalls(response.message.content);
171
- const toolCalls = [];
172
- const toolResults = [];
173
- for (const tc of allToolCalls) {
174
- if (tc.name.startsWith("$")) {
175
- toolResults.push({
176
- type: "tool_result",
177
- toolCallId: tc.id,
178
- content: JSON.stringify(tc.args)
179
- });
180
- } else {
181
- toolCalls.push(tc);
175
+ if (response.stopReason === "pause_turn") {
176
+ consecutivePauses++;
177
+ if (consecutivePauses >= maxContinuations) {
178
+ break;
179
+ }
180
+ continue;
182
181
  }
183
- }
184
- const eventStream = new import_ai.EventStream();
185
- const executions = toolCalls.map(async (toolCall) => {
186
- const startTime = Date.now();
187
- eventStream.push({
188
- type: "tool_call_start",
189
- toolCallId: toolCall.id,
190
- name: toolCall.name,
191
- args: toolCall.args
192
- });
193
- let resultContent;
194
- let details;
195
- let isError = false;
196
- const tool = toolMap.get(toolCall.name);
197
- if (!tool) {
198
- resultContent = `Unknown tool: ${toolCall.name}`;
199
- isError = true;
200
- } else {
201
- try {
202
- const parsed = tool.parameters.parse(toolCall.args);
203
- const ctx = {
204
- signal: options.signal ?? AbortSignal.timeout(3e5),
205
- toolCallId: toolCall.id,
206
- onUpdate: (update) => {
207
- eventStream.push({
208
- type: "tool_call_update",
209
- toolCallId: toolCall.id,
210
- update
211
- });
182
+ consecutivePauses = 0;
183
+ const allToolCalls = extractToolCalls(response.message.content);
184
+ if (response.stopReason !== "tool_use" && allToolCalls.length === 0) {
185
+ if (options.getSteeringMessages) {
186
+ const steering = await options.getSteeringMessages();
187
+ if (steering && steering.length > 0) {
188
+ for (const msg of steering) {
189
+ yield { type: "steering_message", content: msg.content };
190
+ messages.push(msg);
212
191
  }
213
- };
214
- const raw = await tool.execute(parsed, ctx);
215
- const normalized = normalizeToolResult(raw);
216
- resultContent = normalized.content;
217
- details = normalized.details;
218
- } catch (err) {
219
- isError = true;
220
- resultContent = err instanceof Error ? err.message : String(err);
192
+ continue;
193
+ }
221
194
  }
195
+ yield {
196
+ type: "agent_done",
197
+ totalTurns: turn,
198
+ totalUsage: { ...totalUsage }
199
+ };
200
+ return {
201
+ message: response.message,
202
+ totalTurns: turn,
203
+ totalUsage: { ...totalUsage }
204
+ };
222
205
  }
223
- const durationMs = Date.now() - startTime;
224
- eventStream.push({
225
- type: "tool_call_end",
226
- toolCallId: toolCall.id,
227
- result: resultContent,
228
- details,
229
- isError,
230
- durationMs
231
- });
232
- return { toolCallId: toolCall.id, content: resultContent, isError };
233
- });
234
- const abortHandler = () => eventStream.abort(new Error("aborted"));
235
- options.signal?.addEventListener("abort", abortHandler, { once: true });
236
- let toolResultsFinalized = false;
237
- Promise.all(executions).then((results) => {
238
- if (toolResultsFinalized) return;
239
- for (const tc of toolCalls) {
240
- const r = results.find((x) => x.toolCallId === tc.id);
241
- toolResults.push({
242
- type: "tool_result",
243
- toolCallId: tc.id,
244
- content: r.content,
245
- isError: r.isError || void 0
246
- });
247
- }
248
- eventStream.close();
249
- }).catch((err) => eventStream.abort(err instanceof Error ? err : new Error(String(err))));
250
- try {
251
- for await (const event of eventStream) {
252
- yield event;
206
+ const toolCalls = [];
207
+ const toolResults = [];
208
+ for (const tc of allToolCalls) {
209
+ if (tc.name.startsWith("$")) {
210
+ toolResults.push({
211
+ type: "tool_result",
212
+ toolCallId: tc.id,
213
+ content: JSON.stringify(tc.args)
214
+ });
215
+ } else {
216
+ toolCalls.push(tc);
217
+ }
253
218
  }
254
- } finally {
255
- options.signal?.removeEventListener("abort", abortHandler);
256
- toolResultsFinalized = true;
257
- for (const tc of toolCalls) {
258
- if (!toolResults.some((r) => r.toolCallId === tc.id)) {
219
+ const eventStream = new import_ai.EventStream();
220
+ const executions = toolCalls.map(async (toolCall) => {
221
+ const startTime = Date.now();
222
+ eventStream.push({
223
+ type: "tool_call_start",
224
+ toolCallId: toolCall.id,
225
+ name: toolCall.name,
226
+ args: toolCall.args
227
+ });
228
+ let resultContent;
229
+ let details;
230
+ let isError = false;
231
+ const tool = toolMap.get(toolCall.name);
232
+ if (!tool) {
233
+ resultContent = `Unknown tool: ${toolCall.name}`;
234
+ isError = true;
235
+ } else {
236
+ try {
237
+ const parsed = tool.parameters.parse(toolCall.args);
238
+ const ctx = {
239
+ signal: options.signal ?? AbortSignal.timeout(3e5),
240
+ toolCallId: toolCall.id,
241
+ onUpdate: (update) => {
242
+ eventStream.push({
243
+ type: "tool_call_update",
244
+ toolCallId: toolCall.id,
245
+ update
246
+ });
247
+ }
248
+ };
249
+ const raw = await tool.execute(parsed, ctx);
250
+ const normalized = normalizeToolResult(raw);
251
+ resultContent = normalized.content;
252
+ details = normalized.details;
253
+ } catch (err) {
254
+ isError = true;
255
+ resultContent = err instanceof Error ? err.message : String(err);
256
+ }
257
+ }
258
+ const durationMs = Date.now() - startTime;
259
+ eventStream.push({
260
+ type: "tool_call_end",
261
+ toolCallId: toolCall.id,
262
+ result: resultContent,
263
+ details,
264
+ isError,
265
+ durationMs
266
+ });
267
+ return { toolCallId: toolCall.id, content: resultContent, isError };
268
+ });
269
+ const abortHandler = () => eventStream.abort(new Error("aborted"));
270
+ options.signal?.addEventListener("abort", abortHandler, { once: true });
271
+ let toolResultsFinalized = false;
272
+ Promise.all(executions).then((results) => {
273
+ if (toolResultsFinalized) return;
274
+ const resultsMap = new Map(results.map((r) => [r.toolCallId, r]));
275
+ for (const tc of toolCalls) {
276
+ const r = resultsMap.get(tc.id);
259
277
  toolResults.push({
260
278
  type: "tool_result",
261
279
  toolCallId: tc.id,
262
- content: "Tool execution was aborted.",
263
- isError: true
280
+ content: r.content,
281
+ isError: r.isError || void 0
264
282
  });
265
283
  }
284
+ eventStream.close();
285
+ }).catch((err) => eventStream.abort(err instanceof Error ? err : new Error(String(err))));
286
+ let toolsAborted = false;
287
+ try {
288
+ for await (const event of eventStream) {
289
+ yield event;
290
+ }
291
+ } catch (err) {
292
+ if (isAbortError(err) || options.signal?.aborted) {
293
+ toolsAborted = true;
294
+ } else {
295
+ throw err;
296
+ }
297
+ } finally {
298
+ options.signal?.removeEventListener("abort", abortHandler);
299
+ toolResultsFinalized = true;
300
+ const resolvedIds = new Set(toolResults.map((r) => r.toolCallId));
301
+ for (const tc of toolCalls) {
302
+ if (!resolvedIds.has(tc.id)) {
303
+ toolResults.push({
304
+ type: "tool_result",
305
+ toolCallId: tc.id,
306
+ content: "Tool execution was aborted.",
307
+ isError: true
308
+ });
309
+ }
310
+ }
311
+ messages.push({ role: "tool", content: toolResults });
312
+ }
313
+ if (toolsAborted) break;
314
+ if (options.getSteeringMessages) {
315
+ const steering = await options.getSteeringMessages();
316
+ if (steering && steering.length > 0) {
317
+ for (const msg of steering) {
318
+ yield { type: "steering_message", content: msg.content };
319
+ messages.push(msg);
320
+ }
321
+ }
266
322
  }
267
- messages.push({ role: "tool", content: toolResults });
268
323
  }
324
+ } finally {
325
+ sanitizeOrphanedServerTools(messages);
269
326
  }
270
327
  let lastAssistant;
271
328
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -292,6 +349,33 @@ function extractToolCalls(content) {
292
349
  if (typeof content === "string") return [];
293
350
  return content.filter((part) => part.type === "tool_call");
294
351
  }
352
+ function sanitizeOrphanedServerTools(messages) {
353
+ for (let i = messages.length - 1; i >= 0; i--) {
354
+ const msg = messages[i];
355
+ if (msg.role !== "assistant") continue;
356
+ if (typeof msg.content === "string" || !Array.isArray(msg.content)) break;
357
+ const serverToolIds = /* @__PURE__ */ new Set();
358
+ const resultToolIds = /* @__PURE__ */ new Set();
359
+ for (const part of msg.content) {
360
+ if (part.type === "server_tool_call") serverToolIds.add(part.id);
361
+ if (part.type === "server_tool_result") resultToolIds.add(part.toolUseId);
362
+ }
363
+ const orphanedIds = /* @__PURE__ */ new Set();
364
+ for (const id of serverToolIds) {
365
+ if (!resultToolIds.has(id)) orphanedIds.add(id);
366
+ }
367
+ if (orphanedIds.size === 0) break;
368
+ const filtered = msg.content.filter(
369
+ (part) => !(part.type === "server_tool_call" && orphanedIds.has(part.id))
370
+ );
371
+ if (filtered.length === 0) {
372
+ messages.splice(i, 1);
373
+ } else {
374
+ msg.content = filtered;
375
+ }
376
+ break;
377
+ }
378
+ }
295
379
 
296
380
  // src/agent.ts
297
381
  var AgentStream = class {
@@ -371,6 +455,8 @@ var Agent = class {
371
455
  Agent,
372
456
  AgentStream,
373
457
  agentLoop,
458
+ isAbortError,
459
+ isBillingError,
374
460
  isContextOverflow
375
461
  });
376
462
  //# sourceMappingURL=index.cjs.map