@prestyj/agent 4.2.77 → 4.3.15

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.cjs CHANGED
@@ -25,7 +25,8 @@ __export(index_exports, {
25
25
  agentLoop: () => agentLoop,
26
26
  isAbortError: () => isAbortError,
27
27
  isBillingError: () => isBillingError,
28
- isContextOverflow: () => isContextOverflow
28
+ isContextOverflow: () => isContextOverflow,
29
+ setStreamDiagnostic: () => setStreamDiagnostic
29
30
  });
30
31
  module.exports = __toCommonJS(index_exports);
31
32
 
@@ -34,7 +35,14 @@ var import_ai2 = require("@prestyj/ai");
34
35
 
35
36
  // src/agent-loop.ts
36
37
  var import_ai = require("@prestyj/ai");
37
- var DEFAULT_MAX_TURNS = 100;
38
+ var DEFAULT_MAX_TURNS = 200;
39
+ var _diagFn = null;
40
+ function setStreamDiagnostic(fn) {
41
+ _diagFn = fn;
42
+ }
43
+ function diag(phase, data) {
44
+ _diagFn?.(phase, data);
45
+ }
38
46
  function isAbortError(err) {
39
47
  if (!(err instanceof Error)) return false;
40
48
  if (err.name === "AbortError") return true;
@@ -49,7 +57,12 @@ function isContextOverflow(err) {
49
57
  function isBillingError(err) {
50
58
  if (!(err instanceof Error)) return false;
51
59
  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");
60
+ return msg.includes("insufficient balance") || msg.includes("no resource package") || msg.includes("quota exceeded") || msg.includes("billing") || msg.includes("recharge") || msg.includes("subscription plan") || msg.includes("does not yet include access");
61
+ }
62
+ function isToolPairingError(err) {
63
+ if (!(err instanceof Error)) return false;
64
+ const msg = err.message.toLowerCase();
65
+ return msg.includes("tool_use") && msg.includes("tool_result") || msg.includes("unexpected `tool_use_id`") || msg.includes("tool_use ids found without");
53
66
  }
54
67
  function isOverloaded(err) {
55
68
  if (!(err instanceof Error)) return false;
@@ -65,18 +78,45 @@ async function* agentLoop(messages, options) {
65
78
  let turn = 0;
66
79
  let firstTurn = true;
67
80
  let consecutivePauses = 0;
81
+ let toolPairingRepaired = false;
68
82
  let overflowRetries = 0;
69
83
  let overloadRetries = 0;
70
84
  let emptyResponseRetries = 0;
85
+ let stallRetries = 0;
71
86
  const MAX_OVERFLOW_RETRIES = 3;
72
87
  const MAX_OVERLOAD_RETRIES = 10;
73
- const MAX_EMPTY_RESPONSE_RETRIES = 3;
88
+ const MAX_EMPTY_RESPONSE_RETRIES = 2;
89
+ const MAX_STALL_RETRIES = 5;
90
+ const STALL_DELAY_MS = 1e3;
74
91
  const OVERLOAD_BASE_DELAY_MS = 2e3;
75
92
  const OVERLOAD_MAX_DELAY_MS = 3e4;
93
+ const STREAM_FIRST_EVENT_TIMEOUT_MS = 45e3;
94
+ const STREAM_IDLE_TIMEOUT_MS = 3e4;
95
+ const STREAM_HARD_TIMEOUT_MS = 9e4;
96
+ const STREAM_OUTPUT_HARD_TIMEOUT_MS = 3e5;
97
+ const STREAM_THINKING_IDLE_TIMEOUT_MS = 3e5;
98
+ const STREAM_THINKING_HARD_TIMEOUT_MS = 6e5;
76
99
  try {
77
100
  while (turn < maxTurns) {
78
101
  options.signal?.throwIfAborted();
79
102
  turn++;
103
+ let msgChars = 0;
104
+ for (const m of messages) {
105
+ if (typeof m.content === "string") msgChars += m.content.length;
106
+ else if (Array.isArray(m.content)) {
107
+ for (const p of m.content) {
108
+ if ("text" in p && typeof p.text === "string") msgChars += p.text.length;
109
+ if ("content" in p && typeof p.content === "string") msgChars += p.content.length;
110
+ }
111
+ }
112
+ }
113
+ diag("turn_start", {
114
+ turn,
115
+ messages: messages.length,
116
+ chars: msgChars,
117
+ provider: options.provider,
118
+ model: options.model
119
+ });
80
120
  if (firstTurn && options.getSteeringMessages) {
81
121
  const steering = await options.getSteeringMessages();
82
122
  if (steering && steering.length > 0) {
@@ -88,14 +128,62 @@ async function* agentLoop(messages, options) {
88
128
  }
89
129
  firstTurn = false;
90
130
  if (options.transformContext) {
131
+ diag("transform_start");
91
132
  const transformed = await options.transformContext(messages);
92
133
  if (transformed !== messages) {
134
+ diag("transform_compacted", {
135
+ before: messages.length,
136
+ after: transformed.length
137
+ });
93
138
  messages.length = 0;
94
139
  messages.push(...transformed);
95
140
  }
141
+ diag("transform_end");
96
142
  }
143
+ repairToolPairingAdjacent(messages);
97
144
  let response;
145
+ const streamController = new AbortController();
146
+ let idleTimer = null;
147
+ let hardTimer = null;
148
+ let idleTimedOut = false;
149
+ let streamEventCount = 0;
150
+ let lastEventTime = Date.now();
151
+ let streamCallStart = Date.now();
152
+ const eventTypeCounts = {};
153
+ let lastEventType = "";
154
+ let lastYieldEndTime = Date.now();
155
+ let maxConsumerLagMs = 0;
156
+ const forwardAbort = () => streamController.abort();
157
+ options.signal?.addEventListener("abort", forwardAbort, { once: true });
158
+ let hasReceivedEvent = false;
159
+ let hasReceivedThinking = false;
160
+ const resetIdleTimer = () => {
161
+ if (idleTimer) clearTimeout(idleTimer);
162
+ const timeoutMs = hasReceivedEvent ? STREAM_IDLE_TIMEOUT_MS : hasReceivedThinking ? STREAM_THINKING_IDLE_TIMEOUT_MS : STREAM_FIRST_EVENT_TIMEOUT_MS;
163
+ idleTimer = setTimeout(() => {
164
+ diag("idle_timeout_fired", {
165
+ events: streamEventCount,
166
+ sinceLastEventMs: Date.now() - lastEventTime,
167
+ lastEventType,
168
+ maxConsumerLagMs,
169
+ phase: hasReceivedEvent ? "mid_stream" : hasReceivedThinking ? "post_thinking" : "first_event",
170
+ eventTypes: eventTypeCounts
171
+ });
172
+ idleTimedOut = true;
173
+ streamController.abort();
174
+ }, timeoutMs);
175
+ };
176
+ let hardTimeoutMs = STREAM_HARD_TIMEOUT_MS;
177
+ hardTimer = setTimeout(() => {
178
+ diag("hard_timeout_fired", {
179
+ events: typeof streamEventCount !== "undefined" ? streamEventCount : 0
180
+ });
181
+ idleTimedOut = true;
182
+ streamController.abort();
183
+ }, hardTimeoutMs);
98
184
  try {
185
+ diag("stream_call");
186
+ streamCallStart = Date.now();
99
187
  const result = (0, import_ai.stream)({
100
188
  provider: options.provider,
101
189
  model: options.model,
@@ -108,15 +196,63 @@ async function* agentLoop(messages, options) {
108
196
  thinking: options.thinking,
109
197
  apiKey: options.apiKey,
110
198
  baseUrl: options.baseUrl,
111
- signal: options.signal,
199
+ signal: streamController.signal,
112
200
  accountId: options.accountId,
113
201
  cacheRetention: options.cacheRetention,
114
202
  compaction: options.compaction,
115
203
  clearToolUses: options.clearToolUses
116
204
  });
205
+ diag("stream_created", { setupMs: Date.now() - streamCallStart });
117
206
  result.response.catch(() => {
118
207
  });
208
+ streamEventCount = 0;
209
+ hasReceivedEvent = false;
210
+ lastEventTime = Date.now();
211
+ streamCallStart = Date.now();
212
+ resetIdleTimer();
119
213
  for await (const event of result) {
214
+ const pullTime = Date.now();
215
+ const consumerLag = pullTime - lastYieldEndTime;
216
+ if (consumerLag > maxConsumerLagMs) maxConsumerLagMs = consumerLag;
217
+ streamEventCount++;
218
+ eventTypeCounts[event.type] = (eventTypeCounts[event.type] ?? 0) + 1;
219
+ lastEventType = event.type;
220
+ if ((event.type === "text_delta" || event.type === "server_toolcall" || event.type === "toolcall_delta") && !hasReceivedEvent) {
221
+ hasReceivedEvent = true;
222
+ if (hardTimer && hardTimeoutMs < STREAM_OUTPUT_HARD_TIMEOUT_MS) {
223
+ clearTimeout(hardTimer);
224
+ hardTimeoutMs = STREAM_OUTPUT_HARD_TIMEOUT_MS;
225
+ hardTimer = setTimeout(() => {
226
+ diag("hard_timeout_fired", { events: streamEventCount });
227
+ idleTimedOut = true;
228
+ streamController.abort();
229
+ }, hardTimeoutMs);
230
+ }
231
+ }
232
+ if (event.type === "thinking_delta" && !hasReceivedThinking) {
233
+ hasReceivedThinking = true;
234
+ if (hardTimer) clearTimeout(hardTimer);
235
+ hardTimeoutMs = STREAM_THINKING_HARD_TIMEOUT_MS;
236
+ hardTimer = setTimeout(() => {
237
+ diag("hard_timeout_fired", { events: streamEventCount });
238
+ idleTimedOut = true;
239
+ streamController.abort();
240
+ }, hardTimeoutMs);
241
+ }
242
+ const now = Date.now();
243
+ const gap = now - lastEventTime;
244
+ if (streamEventCount === 1) {
245
+ diag("first_event", { type: event.type, ttfMs: now - streamCallStart });
246
+ } else if (gap > 3e3) {
247
+ diag("slow_gap", {
248
+ type: event.type,
249
+ gapMs: gap,
250
+ eventNum: streamEventCount,
251
+ sinceStartMs: now - streamCallStart
252
+ });
253
+ }
254
+ lastEventTime = now;
255
+ resetIdleTimer();
120
256
  if (event.type === "text_delta") {
121
257
  yield { type: "text_delta", text: event.text };
122
258
  } else if (event.type === "thinking_delta") {
@@ -135,12 +271,40 @@ async function* agentLoop(messages, options) {
135
271
  resultType: event.resultType,
136
272
  data: event.data
137
273
  };
274
+ } else if (event.type === "toolcall_delta") {
275
+ yield {
276
+ type: "toolcall_delta",
277
+ chars: event.argsJson?.length ?? 0
278
+ };
138
279
  }
280
+ lastYieldEndTime = Date.now();
139
281
  }
282
+ diag("stream_done", {
283
+ events: streamEventCount,
284
+ totalMs: Date.now() - streamCallStart,
285
+ maxConsumerLagMs,
286
+ eventTypes: eventTypeCounts
287
+ });
140
288
  response = await result.response;
141
289
  } catch (err) {
290
+ const errMsg = err instanceof Error ? err.message : String(err);
291
+ diag("stream_error", {
292
+ error: errMsg.slice(0, 200),
293
+ events: streamEventCount,
294
+ totalMs: Date.now() - streamCallStart,
295
+ idleTimedOut,
296
+ aborted: !!options.signal?.aborted,
297
+ eventTypes: eventTypeCounts,
298
+ provider: options.provider,
299
+ model: options.model
300
+ });
142
301
  if (overflowRetries < MAX_OVERFLOW_RETRIES && isContextOverflow(err) && options.transformContext) {
143
302
  overflowRetries++;
303
+ diag("retry", {
304
+ reason: "context_overflow",
305
+ attempt: overflowRetries,
306
+ maxAttempts: MAX_OVERFLOW_RETRIES
307
+ });
144
308
  yield {
145
309
  type: "retry",
146
310
  reason: "context_overflow",
@@ -162,6 +326,12 @@ async function* agentLoop(messages, options) {
162
326
  OVERLOAD_BASE_DELAY_MS * 2 ** (overloadRetries - 1),
163
327
  OVERLOAD_MAX_DELAY_MS
164
328
  );
329
+ diag("retry", {
330
+ reason: "overloaded",
331
+ attempt: overloadRetries,
332
+ maxAttempts: MAX_OVERLOAD_RETRIES,
333
+ delayMs
334
+ });
165
335
  yield {
166
336
  type: "retry",
167
337
  reason: "overloaded",
@@ -173,16 +343,83 @@ async function* agentLoop(messages, options) {
173
343
  turn--;
174
344
  continue;
175
345
  }
346
+ if (idleTimedOut && !options.signal?.aborted && stallRetries < MAX_STALL_RETRIES) {
347
+ stallRetries++;
348
+ const delayMs = Math.min(STALL_DELAY_MS * 2 ** (stallRetries - 1), 8e3);
349
+ diag("retry", {
350
+ reason: "stream_stall",
351
+ attempt: stallRetries,
352
+ maxAttempts: MAX_STALL_RETRIES,
353
+ delayMs,
354
+ events: streamEventCount
355
+ });
356
+ yield {
357
+ type: "retry",
358
+ reason: "stream_stall",
359
+ attempt: stallRetries,
360
+ maxAttempts: MAX_STALL_RETRIES,
361
+ delayMs,
362
+ silent: stallRetries <= 2
363
+ };
364
+ await new Promise((r) => setTimeout(r, delayMs));
365
+ turn--;
366
+ continue;
367
+ }
368
+ if (idleTimedOut && !options.signal?.aborted) {
369
+ diag("stall_exhausted", {
370
+ stallRetries: MAX_STALL_RETRIES,
371
+ provider: options.provider,
372
+ model: options.model
373
+ });
374
+ yield {
375
+ type: "error",
376
+ error: new Error(
377
+ `The API provider's stream stalled ${MAX_STALL_RETRIES} times \u2014 the provider may be experiencing capacity issues. Your conversation is preserved. Send another message to retry.`
378
+ )
379
+ };
380
+ break;
381
+ }
382
+ if (isToolPairingError(err) && !toolPairingRepaired) {
383
+ toolPairingRepaired = true;
384
+ diag("tool_pairing_repair", { error: errMsg.slice(0, 200) });
385
+ repairToolPairingAdjacent(messages);
386
+ turn--;
387
+ continue;
388
+ }
176
389
  if (isAbortError(err) || options.signal?.aborted) {
390
+ diag("aborted", { turn, provider: options.provider, model: options.model });
177
391
  break;
178
392
  }
393
+ diag("unhandled_error", {
394
+ error: errMsg.slice(0, 500),
395
+ turn,
396
+ provider: options.provider,
397
+ model: options.model
398
+ });
179
399
  throw err;
400
+ } finally {
401
+ if (idleTimer) clearTimeout(idleTimer);
402
+ if (hardTimer) clearTimeout(hardTimer);
403
+ options.signal?.removeEventListener("abort", forwardAbort);
180
404
  }
181
405
  overflowRetries = 0;
182
406
  overloadRetries = 0;
183
- if (response.usage.outputTokens === 0 && (response.message.content === "" || Array.isArray(response.message.content) && response.message.content.length === 0)) {
407
+ stallRetries = 0;
408
+ const contentArr = Array.isArray(response.message.content) ? response.message.content : null;
409
+ const hasActionableContent = response.message.content !== "" && contentArr !== null && contentArr.some(
410
+ (p) => p.type === "text" || p.type === "tool_call" || p.type === "server_tool_call"
411
+ );
412
+ if (!hasActionableContent) {
184
413
  if (emptyResponseRetries < MAX_EMPTY_RESPONSE_RETRIES) {
185
414
  emptyResponseRetries++;
415
+ diag("retry", {
416
+ reason: "empty_response",
417
+ attempt: emptyResponseRetries,
418
+ maxAttempts: MAX_EMPTY_RESPONSE_RETRIES,
419
+ provider: options.provider,
420
+ model: options.model,
421
+ contentTypes: contentArr?.map((p) => p.type).join(",") ?? "empty"
422
+ });
186
423
  yield {
187
424
  type: "retry",
188
425
  reason: "empty_response",
@@ -442,6 +679,59 @@ function sanitizeOrphanedServerTools(messages) {
442
679
  break;
443
680
  }
444
681
  }
682
+ function repairToolPairingAdjacent(messages) {
683
+ for (let i = 0; i < messages.length; i++) {
684
+ const msg = messages[i];
685
+ if (msg.role !== "assistant") continue;
686
+ if (typeof msg.content === "string" || !Array.isArray(msg.content)) continue;
687
+ const toolCallIds = msg.content.filter((p) => p.type === "tool_call").map((p) => p.id);
688
+ if (toolCallIds.length === 0) continue;
689
+ const next = messages[i + 1];
690
+ if (next?.role === "tool" && Array.isArray(next.content)) {
691
+ const existingIds = new Set(next.content.map((r) => r.toolCallId));
692
+ const missing = toolCallIds.filter((id) => !existingIds.has(id));
693
+ if (missing.length > 0) {
694
+ for (const id of missing) {
695
+ next.content.push({
696
+ type: "tool_result",
697
+ toolCallId: id,
698
+ content: "Tool execution was interrupted.",
699
+ isError: true
700
+ });
701
+ }
702
+ }
703
+ } else {
704
+ messages.splice(i + 1, 0, {
705
+ role: "tool",
706
+ content: toolCallIds.map((id) => ({
707
+ type: "tool_result",
708
+ toolCallId: id,
709
+ content: "Tool execution was interrupted.",
710
+ isError: true
711
+ }))
712
+ });
713
+ }
714
+ }
715
+ const toolCallIdSet = /* @__PURE__ */ new Set();
716
+ for (let i = 0; i < messages.length; i++) {
717
+ const msg = messages[i];
718
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
719
+ for (const p of msg.content) {
720
+ if (p.type === "tool_call") toolCallIdSet.add(p.id);
721
+ }
722
+ }
723
+ if (msg.role === "tool" && Array.isArray(msg.content)) {
724
+ const results = msg.content;
725
+ const filtered = results.filter((r) => toolCallIdSet.has(r.toolCallId));
726
+ if (filtered.length === 0) {
727
+ messages.splice(i, 1);
728
+ i--;
729
+ } else if (filtered.length < results.length) {
730
+ msg.content = filtered;
731
+ }
732
+ }
733
+ }
734
+ }
445
735
 
446
736
  // src/agent.ts
447
737
  var AgentStream = class {
@@ -548,6 +838,7 @@ var Agent = class {
548
838
  agentLoop,
549
839
  isAbortError,
550
840
  isBillingError,
551
- isContextOverflow
841
+ isContextOverflow,
842
+ setStreamDiagnostic
552
843
  });
553
844
  //# sourceMappingURL=index.cjs.map