llmist 18.1.0 → 18.2.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.cjs CHANGED
@@ -1926,7 +1926,8 @@ function resolveRetryConfig(config) {
1926
1926
  onRetriesExhausted: config.onRetriesExhausted,
1927
1927
  shouldRetry: config.shouldRetry,
1928
1928
  respectRetryAfter: config.respectRetryAfter ?? DEFAULT_RETRY_CONFIG.respectRetryAfter,
1929
- maxRetryAfterMs: config.maxRetryAfterMs ?? DEFAULT_RETRY_CONFIG.maxRetryAfterMs
1929
+ maxRetryAfterMs: config.maxRetryAfterMs ?? DEFAULT_RETRY_CONFIG.maxRetryAfterMs,
1930
+ retryOnEmpty: config.retryOnEmpty ?? DEFAULT_RETRY_CONFIG.retryOnEmpty
1930
1931
  };
1931
1932
  }
1932
1933
  function getErrorStatusCode(error) {
@@ -2178,8 +2179,9 @@ var init_retry = __esm({
2178
2179
  factor: 2,
2179
2180
  randomize: true,
2180
2181
  respectRetryAfter: true,
2181
- maxRetryAfterMs: 12e4
2182
+ maxRetryAfterMs: 12e4,
2182
2183
  // 2 minutes cap
2184
+ retryOnEmpty: true
2183
2185
  };
2184
2186
  }
2185
2187
  });
@@ -5188,11 +5190,51 @@ var init_output_limit_manager = __esm({
5188
5190
  }
5189
5191
  });
5190
5192
 
5193
+ // src/core/errors.ts
5194
+ function isAbortError(error) {
5195
+ if (!(error instanceof Error)) return false;
5196
+ if (error.name === "AbortError") return true;
5197
+ if (error.name === "APIConnectionAbortedError") return true;
5198
+ if (error.name === "APIUserAbortError") return true;
5199
+ const message = error.message.toLowerCase();
5200
+ if (message.includes("abort")) return true;
5201
+ if (message.includes("cancelled")) return true;
5202
+ if (message.includes("canceled")) return true;
5203
+ return false;
5204
+ }
5205
+ var EmptyCompletionError;
5206
+ var init_errors = __esm({
5207
+ "src/core/errors.ts"() {
5208
+ "use strict";
5209
+ EmptyCompletionError = class extends Error {
5210
+ /** Agent iteration on which the empty completion was observed. */
5211
+ iteration;
5212
+ /** Finish reason reported alongside the empty body (often null). */
5213
+ finishReason;
5214
+ constructor(params) {
5215
+ super(
5216
+ `LLM returned an empty completion (no text, tool calls, or reasoning) on iteration ${params.iteration}`
5217
+ );
5218
+ this.name = "EmptyCompletionError";
5219
+ this.iteration = params.iteration;
5220
+ this.finishReason = params.finishReason;
5221
+ }
5222
+ };
5223
+ }
5224
+ });
5225
+
5191
5226
  // src/agent/retry-orchestrator.ts
5227
+ function isEmptyCompletion(meta) {
5228
+ if (meta.didExecuteGadgets) return false;
5229
+ if (meta.rawResponse?.trim() || meta.finalMessage?.trim()) return false;
5230
+ if (meta.thinkingContent?.trim()) return false;
5231
+ return true;
5232
+ }
5192
5233
  var RetryOrchestrator;
5193
5234
  var init_retry_orchestrator = __esm({
5194
5235
  "src/agent/retry-orchestrator.ts"() {
5195
5236
  "use strict";
5237
+ init_errors();
5196
5238
  init_retry();
5197
5239
  init_safe_observe();
5198
5240
  init_tree_hook_bridge();
@@ -5258,6 +5300,7 @@ var init_retry_orchestrator = __esm({
5258
5300
  let gadgetCallCount = 0;
5259
5301
  const textOutputs = [];
5260
5302
  const gadgetResults = [];
5303
+ let emptyFailure = null;
5261
5304
  while (streamAttempt < maxStreamAttempts) {
5262
5305
  streamAttempt++;
5263
5306
  try {
@@ -5266,6 +5309,7 @@ var init_retry_orchestrator = __esm({
5266
5309
  for await (const event of processor.process(stream2)) {
5267
5310
  if (event.type === "stream_complete") {
5268
5311
  streamMetadata = event;
5312
+ continue;
5269
5313
  }
5270
5314
  if (event.type === "llm_response_end") {
5271
5315
  this.tree.endLLMResponse(llmNodeId, {
@@ -5287,43 +5331,44 @@ var init_retry_orchestrator = __esm({
5287
5331
  for (const id of processor.getFailedInvocationIds()) {
5288
5332
  this.failedInvocationIds.add(id);
5289
5333
  }
5334
+ if (this.retryConfig.enabled && this.retryConfig.retryOnEmpty && streamMetadata !== null && !streamMetadata.finishReason && isEmptyCompletion(streamMetadata)) {
5335
+ const emptyError = new EmptyCompletionError({
5336
+ iteration,
5337
+ finishReason: streamMetadata.finishReason
5338
+ });
5339
+ if (streamAttempt < maxStreamAttempts) {
5340
+ await this.backoffBeforeRetry(
5341
+ emptyError,
5342
+ streamAttempt,
5343
+ maxStreamAttempts,
5344
+ iteration,
5345
+ llmNodeId
5346
+ );
5347
+ streamMetadata = null;
5348
+ gadgetCallCount = 0;
5349
+ textOutputs.length = 0;
5350
+ gadgetResults.length = 0;
5351
+ continue;
5352
+ }
5353
+ emptyFailure = emptyError;
5354
+ break;
5355
+ }
5356
+ if (streamMetadata !== null) {
5357
+ yield streamMetadata;
5358
+ }
5290
5359
  break;
5291
5360
  } catch (streamError) {
5292
5361
  const error = streamError;
5293
5362
  const canRetry = this.retryConfig.enabled && streamAttempt < maxStreamAttempts;
5294
5363
  const shouldRetryError = this.retryConfig.shouldRetry ? this.retryConfig.shouldRetry(error) : isRetryableError(error);
5295
5364
  if (canRetry && shouldRetryError) {
5296
- const retryAfterMs = this.retryConfig.respectRetryAfter ? extractRetryAfterMs(error) : null;
5297
- const baseDelay = this.retryConfig.minTimeout * this.retryConfig.factor ** (streamAttempt - 1);
5298
- const cappedBaseDelay = Math.min(baseDelay, this.retryConfig.maxTimeout);
5299
- const delay = retryAfterMs !== null ? Math.min(retryAfterMs, this.retryConfig.maxRetryAfterMs) : cappedBaseDelay;
5300
- const finalDelay = this.retryConfig.randomize ? delay * (0.5 + Math.random()) : delay;
5301
- this.logger.warn(
5302
- `Stream iteration failed (attempt ${streamAttempt}/${maxStreamAttempts}), retrying...`,
5303
- {
5304
- error: error.message,
5305
- retriesLeft: maxStreamAttempts - streamAttempt,
5306
- delayMs: Math.round(finalDelay),
5307
- retryAfterMs
5308
- }
5365
+ await this.backoffBeforeRetry(
5366
+ error,
5367
+ streamAttempt,
5368
+ maxStreamAttempts,
5369
+ iteration,
5370
+ llmNodeId
5309
5371
  );
5310
- this.retryConfig.onRetry?.(error, streamAttempt);
5311
- await safeObserve(async () => {
5312
- if (this.hooks.observers?.onRetryAttempt) {
5313
- const subagentContext = getSubagentContextForNode(this.tree, llmNodeId);
5314
- const hookContext = {
5315
- iteration,
5316
- attemptNumber: streamAttempt,
5317
- retriesLeft: maxStreamAttempts - streamAttempt,
5318
- error,
5319
- retryAfterMs: retryAfterMs ?? void 0,
5320
- logger: this.logger,
5321
- subagentContext
5322
- };
5323
- await this.hooks.observers.onRetryAttempt(hookContext);
5324
- }
5325
- }, this.logger);
5326
- await this.sleep(finalDelay);
5327
5372
  streamMetadata = null;
5328
5373
  gadgetCallCount = 0;
5329
5374
  textOutputs.length = 0;
@@ -5340,8 +5385,55 @@ var init_retry_orchestrator = __esm({
5340
5385
  throw error;
5341
5386
  }
5342
5387
  }
5388
+ if (emptyFailure !== null) {
5389
+ this.logger.error(`LLM returned empty completions on all ${streamAttempt} attempts`, {
5390
+ iteration
5391
+ });
5392
+ this.retryConfig.onRetriesExhausted?.(emptyFailure, streamAttempt);
5393
+ throw emptyFailure;
5394
+ }
5343
5395
  return streamMetadata !== null ? { streamMetadata, textOutputs, gadgetResults, gadgetCallCount } : null;
5344
5396
  }
5397
+ /**
5398
+ * Apply the configured backoff before a retry attempt: compute the delay
5399
+ * (Retry-After hint or exponential backoff, with optional jitter), emit the
5400
+ * retry log, fire the `onRetry` callback and `onRetryAttempt` observer, then
5401
+ * sleep. Shared by the error-retry and empty-completion-retry paths so both
5402
+ * honour identical backoff and observer semantics.
5403
+ */
5404
+ async backoffBeforeRetry(error, streamAttempt, maxStreamAttempts, iteration, llmNodeId) {
5405
+ const retryAfterMs = this.retryConfig.respectRetryAfter ? extractRetryAfterMs(error) : null;
5406
+ const baseDelay = this.retryConfig.minTimeout * this.retryConfig.factor ** (streamAttempt - 1);
5407
+ const cappedBaseDelay = Math.min(baseDelay, this.retryConfig.maxTimeout);
5408
+ const delay = retryAfterMs !== null ? Math.min(retryAfterMs, this.retryConfig.maxRetryAfterMs) : cappedBaseDelay;
5409
+ const finalDelay = this.retryConfig.randomize ? delay * (0.5 + Math.random()) : delay;
5410
+ this.logger.warn(
5411
+ `Stream iteration failed (attempt ${streamAttempt}/${maxStreamAttempts}), retrying...`,
5412
+ {
5413
+ error: error.message,
5414
+ retriesLeft: maxStreamAttempts - streamAttempt,
5415
+ delayMs: Math.round(finalDelay),
5416
+ retryAfterMs
5417
+ }
5418
+ );
5419
+ this.retryConfig.onRetry?.(error, streamAttempt);
5420
+ await safeObserve(async () => {
5421
+ if (this.hooks.observers?.onRetryAttempt) {
5422
+ const subagentContext = getSubagentContextForNode(this.tree, llmNodeId);
5423
+ const hookContext = {
5424
+ iteration,
5425
+ attemptNumber: streamAttempt,
5426
+ retriesLeft: maxStreamAttempts - streamAttempt,
5427
+ error,
5428
+ retryAfterMs: retryAfterMs ?? void 0,
5429
+ logger: this.logger,
5430
+ subagentContext
5431
+ };
5432
+ await this.hooks.observers.onRetryAttempt(hookContext);
5433
+ }
5434
+ }, this.logger);
5435
+ await this.sleep(finalDelay);
5436
+ }
5345
5437
  };
5346
5438
  }
5347
5439
  });
@@ -12483,7 +12575,7 @@ var init_model_registry = __esm({
12483
12575
  * Register a provider and collect its model specifications
12484
12576
  */
12485
12577
  registerProvider(provider) {
12486
- const specs = provider.getModelSpecs?.() ?? [];
12578
+ const specs = [...provider.getModelSpecs?.() ?? []];
12487
12579
  if (specs.length > 0) {
12488
12580
  this.modelSpecs.push(...specs);
12489
12581
  this.providerMap.set(provider.providerId, specs);
@@ -12577,7 +12669,7 @@ var init_model_registry = __esm({
12577
12669
  if (!providerId) {
12578
12670
  return [...this.modelSpecs];
12579
12671
  }
12580
- return this.providerMap.get(providerId) ?? [];
12672
+ return [...this.providerMap.get(providerId) ?? []];
12581
12673
  }
12582
12674
  /**
12583
12675
  * Get context window and output limits for a model
@@ -16772,7 +16864,7 @@ var init_stream_processor_factory = __esm({
16772
16864
 
16773
16865
  // src/mcp/errors.ts
16774
16866
  var McpError, McpUntrustedCommandError, McpConnectError, McpToolCallError, McpTimeoutError, JsonSchemaConversionError;
16775
- var init_errors = __esm({
16867
+ var init_errors2 = __esm({
16776
16868
  "src/mcp/errors.ts"() {
16777
16869
  "use strict";
16778
16870
  McpError = class extends Error {
@@ -16858,7 +16950,7 @@ var init_allowlist = __esm({
16858
16950
  "src/mcp/allowlist.ts"() {
16859
16951
  "use strict";
16860
16952
  import_node_path6 = __toESM(require("path"), 1);
16861
- init_errors();
16953
+ init_errors2();
16862
16954
  DEFAULT_MCP_COMMAND_ALLOWLIST = /* @__PURE__ */ new Set([
16863
16955
  "npx",
16864
16956
  "node",
@@ -16896,7 +16988,7 @@ var init_client2 = __esm({
16896
16988
  "src/mcp/client.ts"() {
16897
16989
  "use strict";
16898
16990
  init_allowlist();
16899
- init_errors();
16991
+ init_errors2();
16900
16992
  cachedSdk = null;
16901
16993
  DEFAULT_CLIENT_INFO = { name: "llmist", version: "0.0.0" };
16902
16994
  McpClient = class {
@@ -17478,7 +17570,7 @@ var init_json_schema_to_zod = __esm({
17478
17570
  "src/mcp/json-schema-to-zod.ts"() {
17479
17571
  "use strict";
17480
17572
  import_zod4 = require("zod");
17481
- init_errors();
17573
+ init_errors2();
17482
17574
  }
17483
17575
  });
17484
17576
 
@@ -18485,6 +18577,7 @@ __export(index_exports, {
18485
18577
  DEFAULT_RATE_LIMIT_CONFIG: () => DEFAULT_RATE_LIMIT_CONFIG,
18486
18578
  DEFAULT_RETRY_CONFIG: () => DEFAULT_RETRY_CONFIG,
18487
18579
  DEFAULT_SUMMARIZATION_PROMPT: () => DEFAULT_SUMMARIZATION_PROMPT,
18580
+ EmptyCompletionError: () => EmptyCompletionError,
18488
18581
  ExecutionTree: () => ExecutionTree,
18489
18582
  FALLBACK_CHARS_PER_TOKEN: () => FALLBACK_CHARS_PER_TOKEN,
18490
18583
  GADGET_ARG_PREFIX: () => GADGET_ARG_PREFIX,
@@ -18770,19 +18863,7 @@ init_stream_processor();
18770
18863
  // src/index.ts
18771
18864
  init_client();
18772
18865
  init_constants();
18773
-
18774
- // src/core/errors.ts
18775
- function isAbortError(error) {
18776
- if (!(error instanceof Error)) return false;
18777
- if (error.name === "AbortError") return true;
18778
- if (error.name === "APIConnectionAbortedError") return true;
18779
- if (error.name === "APIUserAbortError") return true;
18780
- const message = error.message.toLowerCase();
18781
- if (message.includes("abort")) return true;
18782
- if (message.includes("cancelled")) return true;
18783
- if (message.includes("canceled")) return true;
18784
- return false;
18785
- }
18866
+ init_errors();
18786
18867
 
18787
18868
  // src/core/execution-events.ts
18788
18869
  function isLLMEvent(event) {
@@ -18842,7 +18923,7 @@ init_typed_gadget();
18842
18923
  // src/mcp/index.ts
18843
18924
  init_allowlist();
18844
18925
  init_client2();
18845
- init_errors();
18926
+ init_errors2();
18846
18927
 
18847
18928
  // src/mcp/gadget-exporter.ts
18848
18929
  init_schema_to_json();
@@ -19492,6 +19573,7 @@ function getHostExports2(ctx) {
19492
19573
  DEFAULT_RATE_LIMIT_CONFIG,
19493
19574
  DEFAULT_RETRY_CONFIG,
19494
19575
  DEFAULT_SUMMARIZATION_PROMPT,
19576
+ EmptyCompletionError,
19495
19577
  ExecutionTree,
19496
19578
  FALLBACK_CHARS_PER_TOKEN,
19497
19579
  GADGET_ARG_PREFIX,