llmist 6.2.0 → 7.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.cjs CHANGED
@@ -1877,6 +1877,9 @@ function parseEnvBoolean(value) {
1877
1877
  if (normalized === "false" || normalized === "0") return false;
1878
1878
  return void 0;
1879
1879
  }
1880
+ function stripAnsi(str) {
1881
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
1882
+ }
1880
1883
  function createLogger(options = {}) {
1881
1884
  const envMinLevel = parseLogLevel(process.env.LLMIST_LOG_LEVEL);
1882
1885
  const envLogFile = process.env.LLMIST_LOG_FILE?.trim() ?? "";
@@ -1885,36 +1888,62 @@ function createLogger(options = {}) {
1885
1888
  const defaultType = options.type ?? "pretty";
1886
1889
  const name = options.name ?? "llmist";
1887
1890
  const logReset = options.logReset ?? envLogReset ?? false;
1888
- let logFileStream;
1889
- let finalType = defaultType;
1890
- if (envLogFile) {
1891
+ if (envLogFile && (!logFileInitialized || sharedLogFilePath !== envLogFile)) {
1891
1892
  try {
1893
+ if (sharedLogFileStream) {
1894
+ sharedLogFileStream.end();
1895
+ sharedLogFileStream = void 0;
1896
+ }
1892
1897
  (0, import_node_fs.mkdirSync)((0, import_node_path2.dirname)(envLogFile), { recursive: true });
1893
1898
  const flags = logReset ? "w" : "a";
1894
- logFileStream = (0, import_node_fs.createWriteStream)(envLogFile, { flags });
1895
- finalType = "hidden";
1899
+ sharedLogFileStream = (0, import_node_fs.createWriteStream)(envLogFile, { flags });
1900
+ sharedLogFilePath = envLogFile;
1901
+ logFileInitialized = true;
1902
+ writeErrorCount = 0;
1903
+ writeErrorReported = false;
1904
+ sharedLogFileStream.on("error", (error) => {
1905
+ writeErrorCount++;
1906
+ if (!writeErrorReported) {
1907
+ console.error(`[llmist] Log file write error: ${error.message}`);
1908
+ writeErrorReported = true;
1909
+ }
1910
+ if (writeErrorCount >= MAX_WRITE_ERRORS_BEFORE_DISABLE) {
1911
+ console.error(
1912
+ `[llmist] Too many log file errors (${writeErrorCount}), disabling file logging`
1913
+ );
1914
+ sharedLogFileStream?.end();
1915
+ sharedLogFileStream = void 0;
1916
+ }
1917
+ });
1896
1918
  } catch (error) {
1897
1919
  console.error("Failed to initialize LLMIST_LOG_FILE output:", error);
1898
1920
  }
1899
1921
  }
1922
+ const useFileLogging = Boolean(sharedLogFileStream);
1900
1923
  const logger = new import_tslog.Logger({
1901
1924
  name,
1902
1925
  minLevel,
1903
- type: finalType,
1904
- // Optimize for production
1905
- hideLogPositionForProduction: finalType !== "pretty",
1906
- // Pretty output settings
1907
- prettyLogTemplate: finalType === "pretty" ? "{{yyyy}}-{{mm}}-{{dd}} {{hh}}:{{MM}}:{{ss}}:{{ms}} {{logLevelName}} [{{name}}] " : void 0
1926
+ type: useFileLogging ? "pretty" : defaultType,
1927
+ // Hide log position for file logging and non-pretty types
1928
+ hideLogPositionForProduction: useFileLogging || defaultType !== "pretty",
1929
+ prettyLogTemplate: LOG_TEMPLATE,
1930
+ // Use overwrite to redirect tslog's formatted output to file instead of console
1931
+ overwrite: useFileLogging ? {
1932
+ transportFormatted: (logMetaMarkup, logArgs, _logErrors) => {
1933
+ if (!sharedLogFileStream) return;
1934
+ const meta = stripAnsi(logMetaMarkup);
1935
+ const args = logArgs.map(
1936
+ (arg) => typeof arg === "string" ? stripAnsi(arg) : JSON.stringify(arg)
1937
+ );
1938
+ const line = `${meta}${args.join(" ")}
1939
+ `;
1940
+ sharedLogFileStream.write(line);
1941
+ }
1942
+ } : void 0
1908
1943
  });
1909
- if (logFileStream) {
1910
- logger.attachTransport((logObj) => {
1911
- logFileStream?.write(`${JSON.stringify(logObj)}
1912
- `);
1913
- });
1914
- }
1915
1944
  return logger;
1916
1945
  }
1917
- var import_node_fs, import_node_path2, import_tslog, LEVEL_NAME_TO_ID, defaultLogger;
1946
+ var import_node_fs, import_node_path2, import_tslog, LEVEL_NAME_TO_ID, sharedLogFilePath, sharedLogFileStream, logFileInitialized, writeErrorCount, writeErrorReported, MAX_WRITE_ERRORS_BEFORE_DISABLE, LOG_TEMPLATE, defaultLogger;
1918
1947
  var init_logger = __esm({
1919
1948
  "src/logging/logger.ts"() {
1920
1949
  "use strict";
@@ -1930,6 +1959,11 @@ var init_logger = __esm({
1930
1959
  error: 5,
1931
1960
  fatal: 6
1932
1961
  };
1962
+ logFileInitialized = false;
1963
+ writeErrorCount = 0;
1964
+ writeErrorReported = false;
1965
+ MAX_WRITE_ERRORS_BEFORE_DISABLE = 5;
1966
+ LOG_TEMPLATE = "{{yyyy}}-{{mm}}-{{dd}} {{hh}}:{{MM}}:{{ss}}:{{ms}} {{logLevelName}} [{{name}}] ";
1933
1967
  defaultLogger = createLogger();
1934
1968
  }
1935
1969
  });
@@ -5900,11 +5934,13 @@ var init_model_registry = __esm({
5900
5934
  }
5901
5935
  /**
5902
5936
  * Get model specification by model ID
5903
- * @param modelId - Full model identifier (e.g., 'gpt-5', 'claude-sonnet-4-5-20250929')
5937
+ * @param modelId - Full model identifier, optionally with provider prefix
5938
+ * (e.g., 'gpt-5', 'claude-sonnet-4-5-20250929', 'anthropic:claude-sonnet-4-5')
5904
5939
  * @returns ModelSpec if found, undefined otherwise
5905
5940
  */
5906
5941
  getModelSpec(modelId) {
5907
- return this.modelSpecs.find((model) => model.modelId === modelId);
5942
+ const normalizedId = modelId.includes(":") ? modelId.split(":")[1] : modelId;
5943
+ return this.modelSpecs.find((model) => model.modelId === normalizedId);
5908
5944
  }
5909
5945
  /**
5910
5946
  * List all models, optionally filtered by provider
@@ -7146,9 +7182,8 @@ var init_cost_reporting_client = __esm({
7146
7182
  */
7147
7183
  reportCostFromUsage(model, inputTokens, outputTokens, cachedInputTokens = 0, cacheCreationInputTokens = 0) {
7148
7184
  if (inputTokens === 0 && outputTokens === 0) return;
7149
- const modelName = model.includes(":") ? model.split(":")[1] : model;
7150
7185
  const estimate = this.client.modelRegistry.estimateCost(
7151
- modelName,
7186
+ model,
7152
7187
  inputTokens,
7153
7188
  outputTokens,
7154
7189
  cachedInputTokens,
@@ -7471,10 +7506,11 @@ function getHostExportsInternal() {
7471
7506
  }
7472
7507
  return cachedHostExports;
7473
7508
  }
7474
- var import_zod2, cachedHostExports, GadgetExecutor;
7509
+ var import_fast_deep_equal, import_zod2, cachedHostExports, GadgetExecutor;
7475
7510
  var init_executor = __esm({
7476
7511
  "src/gadgets/executor.ts"() {
7477
7512
  "use strict";
7513
+ import_fast_deep_equal = __toESM(require("fast-deep-equal"), 1);
7478
7514
  import_zod2 = require("zod");
7479
7515
  init_builder();
7480
7516
  init_client();
@@ -7585,7 +7621,7 @@ var init_executor = __esm({
7585
7621
  try {
7586
7622
  const cleanedRaw = stripMarkdownFences(call.parametersRaw);
7587
7623
  const initialParse = parseBlockParams(cleanedRaw, { argPrefix: this.argPrefix });
7588
- const parametersWereModified = !this.deepEquals(rawParameters, initialParse);
7624
+ const parametersWereModified = !(0, import_fast_deep_equal.default)(rawParameters, initialParse);
7589
7625
  if (parametersWereModified) {
7590
7626
  this.logger.debug("Parameters modified by interceptor, skipping re-parse", {
7591
7627
  gadgetName: call.gadgetName
@@ -7662,7 +7698,9 @@ var init_executor = __esm({
7662
7698
  nodeId: gadgetNodeId,
7663
7699
  depth: gadgetDepth,
7664
7700
  // Host exports for external gadgets to use host's llmist classes
7665
- hostExports: getHostExportsInternal()
7701
+ hostExports: getHostExportsInternal(),
7702
+ // Logger for structured logging (respects CLI's log level/file config)
7703
+ logger: this.logger
7666
7704
  };
7667
7705
  let rawResult;
7668
7706
  if (timeoutMs && timeoutMs > 0) {
@@ -7832,27 +7870,6 @@ var init_executor = __esm({
7832
7870
  async executeAll(calls) {
7833
7871
  return Promise.all(calls.map((call) => this.execute(call)));
7834
7872
  }
7835
- /**
7836
- * Deep equality check for objects/arrays.
7837
- * Used to detect if parameters were modified by an interceptor.
7838
- */
7839
- deepEquals(a, b) {
7840
- if (a === b) return true;
7841
- if (a === null || b === null) return a === b;
7842
- if (typeof a !== typeof b) return false;
7843
- if (typeof a !== "object") return a === b;
7844
- if (Array.isArray(a) !== Array.isArray(b)) return false;
7845
- if (Array.isArray(a) && Array.isArray(b)) {
7846
- if (a.length !== b.length) return false;
7847
- return a.every((val, i) => this.deepEquals(val, b[i]));
7848
- }
7849
- const aObj = a;
7850
- const bObj = b;
7851
- const aKeys = Object.keys(aObj);
7852
- const bKeys = Object.keys(bObj);
7853
- if (aKeys.length !== bKeys.length) return false;
7854
- return aKeys.every((key) => this.deepEquals(aObj[key], bObj[key]));
7855
- }
7856
7873
  };
7857
7874
  }
7858
7875
  });
@@ -7890,6 +7907,11 @@ var init_stream_processor = __esm({
7890
7907
  inFlightExecutions = /* @__PURE__ */ new Map();
7891
7908
  /** Queue of completed gadget results ready to be yielded (for real-time streaming) */
7892
7909
  completedResultsQueue = [];
7910
+ // Cross-iteration dependency tracking
7911
+ /** Invocation IDs completed in previous iterations (read-only reference from Agent) */
7912
+ priorCompletedInvocations;
7913
+ /** Invocation IDs that failed in previous iterations (read-only reference from Agent) */
7914
+ priorFailedInvocations;
7893
7915
  constructor(options) {
7894
7916
  this.iteration = options.iteration;
7895
7917
  this.registry = options.registry;
@@ -7898,6 +7920,8 @@ var init_stream_processor = __esm({
7898
7920
  this.tree = options.tree;
7899
7921
  this.parentNodeId = options.parentNodeId ?? null;
7900
7922
  this.baseDepth = options.baseDepth ?? 0;
7923
+ this.priorCompletedInvocations = options.priorCompletedInvocations ?? /* @__PURE__ */ new Set();
7924
+ this.priorFailedInvocations = options.priorFailedInvocations ?? /* @__PURE__ */ new Set();
7901
7925
  this.parser = new GadgetCallParser({
7902
7926
  startPrefix: options.gadgetStartPrefix,
7903
7927
  endPrefix: options.gadgetEndPrefix,
@@ -8096,62 +8120,11 @@ var init_stream_processor = __esm({
8096
8120
  }
8097
8121
  return [{ type: "text", content }];
8098
8122
  }
8099
- /**
8100
- * Process a gadget call through the full lifecycle, handling dependencies.
8101
- *
8102
- * Gadgets without dependencies (or with all dependencies satisfied) execute immediately.
8103
- * Gadgets with unsatisfied dependencies are queued for later execution.
8104
- * After each execution, pending gadgets are checked to see if they can now run.
8105
- */
8106
- async processGadgetCall(call) {
8107
- const events = [];
8108
- events.push({ type: "gadget_call", call });
8109
- if (call.dependencies.length > 0) {
8110
- if (call.dependencies.includes(call.invocationId)) {
8111
- this.logger.warn("Gadget has self-referential dependency (depends on itself)", {
8112
- gadgetName: call.gadgetName,
8113
- invocationId: call.invocationId
8114
- });
8115
- this.failedInvocations.add(call.invocationId);
8116
- const skipEvent = {
8117
- type: "gadget_skipped",
8118
- gadgetName: call.gadgetName,
8119
- invocationId: call.invocationId,
8120
- parameters: call.parameters ?? {},
8121
- failedDependency: call.invocationId,
8122
- failedDependencyError: `Gadget "${call.invocationId}" cannot depend on itself (self-referential dependency)`
8123
- };
8124
- events.push(skipEvent);
8125
- return events;
8126
- }
8127
- const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
8128
- if (failedDep) {
8129
- const skipEvents = await this.handleFailedDependency(call, failedDep);
8130
- events.push(...skipEvents);
8131
- return events;
8132
- }
8133
- const unsatisfied = call.dependencies.filter((dep) => !this.completedResults.has(dep));
8134
- if (unsatisfied.length > 0) {
8135
- this.logger.debug("Queueing gadget for later - waiting on dependencies", {
8136
- gadgetName: call.gadgetName,
8137
- invocationId: call.invocationId,
8138
- waitingOn: unsatisfied
8139
- });
8140
- this.gadgetsAwaitingDependencies.set(call.invocationId, call);
8141
- return events;
8142
- }
8143
- }
8144
- const executeEvents = await this.executeGadgetWithHooks(call);
8145
- events.push(...executeEvents);
8146
- const triggeredEvents = await this.processPendingGadgets();
8147
- events.push(...triggeredEvents);
8148
- return events;
8149
- }
8150
8123
  /**
8151
8124
  * Process a gadget call, yielding events in real-time.
8152
8125
  *
8153
- * Key difference from processGadgetCall: yields gadget_call event IMMEDIATELY
8154
- * when parsed (before execution), enabling real-time UI feedback.
8126
+ * Yields gadget_call event IMMEDIATELY when parsed (before execution),
8127
+ * enabling real-time UI feedback.
8155
8128
  */
8156
8129
  async *processGadgetCallGenerator(call) {
8157
8130
  yield { type: "gadget_call", call };
@@ -8182,7 +8155,9 @@ var init_stream_processor = __esm({
8182
8155
  yield skipEvent;
8183
8156
  return;
8184
8157
  }
8185
- const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
8158
+ const failedDep = call.dependencies.find(
8159
+ (dep) => this.failedInvocations.has(dep) || this.priorFailedInvocations.has(dep)
8160
+ );
8186
8161
  if (failedDep) {
8187
8162
  const skipEvents = await this.handleFailedDependency(call, failedDep);
8188
8163
  for (const evt of skipEvents) {
@@ -8190,7 +8165,9 @@ var init_stream_processor = __esm({
8190
8165
  }
8191
8166
  return;
8192
8167
  }
8193
- const unsatisfied = call.dependencies.filter((dep) => !this.completedResults.has(dep));
8168
+ const unsatisfied = call.dependencies.filter(
8169
+ (dep) => !this.completedResults.has(dep) && !this.priorCompletedInvocations.has(dep)
8170
+ );
8194
8171
  if (unsatisfied.length > 0) {
8195
8172
  this.logger.debug("Queueing gadget for later - waiting on dependencies", {
8196
8173
  gadgetName: call.gadgetName,
@@ -8212,143 +8189,9 @@ var init_stream_processor = __esm({
8212
8189
  this.inFlightExecutions.set(call.invocationId, executionPromise);
8213
8190
  }
8214
8191
  /**
8215
- * Execute a gadget through the full hook lifecycle.
8216
- * This is the core execution logic, extracted from processGadgetCall.
8217
- * @deprecated Use executeGadgetGenerator for real-time streaming
8218
- */
8219
- async executeGadgetWithHooks(call) {
8220
- const events = [];
8221
- if (call.parseError) {
8222
- this.logger.warn("Gadget has parse error", {
8223
- gadgetName: call.gadgetName,
8224
- error: call.parseError,
8225
- rawParameters: call.parametersRaw
8226
- });
8227
- }
8228
- let parameters = call.parameters ?? {};
8229
- if (this.hooks.interceptors?.interceptGadgetParameters) {
8230
- const context = {
8231
- iteration: this.iteration,
8232
- gadgetName: call.gadgetName,
8233
- invocationId: call.invocationId,
8234
- logger: this.logger
8235
- };
8236
- parameters = this.hooks.interceptors.interceptGadgetParameters(parameters, context);
8237
- }
8238
- call.parameters = parameters;
8239
- let shouldSkip = false;
8240
- let syntheticResult;
8241
- if (this.hooks.controllers?.beforeGadgetExecution) {
8242
- const context = {
8243
- iteration: this.iteration,
8244
- gadgetName: call.gadgetName,
8245
- invocationId: call.invocationId,
8246
- parameters,
8247
- logger: this.logger
8248
- };
8249
- const action = await this.hooks.controllers.beforeGadgetExecution(context);
8250
- validateBeforeGadgetExecutionAction(action);
8251
- if (action.action === "skip") {
8252
- shouldSkip = true;
8253
- syntheticResult = action.syntheticResult;
8254
- this.logger.info("Controller skipped gadget execution", {
8255
- gadgetName: call.gadgetName
8256
- });
8257
- }
8258
- }
8259
- const startObservers = [];
8260
- if (this.hooks.observers?.onGadgetExecutionStart) {
8261
- startObservers.push(async () => {
8262
- const context = {
8263
- iteration: this.iteration,
8264
- gadgetName: call.gadgetName,
8265
- invocationId: call.invocationId,
8266
- parameters,
8267
- logger: this.logger
8268
- };
8269
- await this.hooks.observers?.onGadgetExecutionStart?.(context);
8270
- });
8271
- }
8272
- await this.runObserversInParallel(startObservers);
8273
- let result;
8274
- if (shouldSkip) {
8275
- result = {
8276
- gadgetName: call.gadgetName,
8277
- invocationId: call.invocationId,
8278
- parameters,
8279
- result: syntheticResult ?? "Execution skipped",
8280
- executionTimeMs: 0
8281
- };
8282
- } else {
8283
- result = await this.executor.execute(call);
8284
- }
8285
- const originalResult = result.result;
8286
- if (result.result && this.hooks.interceptors?.interceptGadgetResult) {
8287
- const context = {
8288
- iteration: this.iteration,
8289
- gadgetName: result.gadgetName,
8290
- invocationId: result.invocationId,
8291
- parameters,
8292
- executionTimeMs: result.executionTimeMs,
8293
- logger: this.logger
8294
- };
8295
- result.result = this.hooks.interceptors.interceptGadgetResult(result.result, context);
8296
- }
8297
- if (this.hooks.controllers?.afterGadgetExecution) {
8298
- const context = {
8299
- iteration: this.iteration,
8300
- gadgetName: result.gadgetName,
8301
- invocationId: result.invocationId,
8302
- parameters,
8303
- result: result.result,
8304
- error: result.error,
8305
- executionTimeMs: result.executionTimeMs,
8306
- logger: this.logger
8307
- };
8308
- const action = await this.hooks.controllers.afterGadgetExecution(context);
8309
- validateAfterGadgetExecutionAction(action);
8310
- if (action.action === "recover" && result.error) {
8311
- this.logger.info("Controller recovered from gadget error", {
8312
- gadgetName: result.gadgetName,
8313
- originalError: result.error
8314
- });
8315
- result = {
8316
- ...result,
8317
- error: void 0,
8318
- result: action.fallbackResult
8319
- };
8320
- }
8321
- }
8322
- const completeObservers = [];
8323
- if (this.hooks.observers?.onGadgetExecutionComplete) {
8324
- completeObservers.push(async () => {
8325
- const context = {
8326
- iteration: this.iteration,
8327
- gadgetName: result.gadgetName,
8328
- invocationId: result.invocationId,
8329
- parameters,
8330
- originalResult,
8331
- finalResult: result.result,
8332
- error: result.error,
8333
- executionTimeMs: result.executionTimeMs,
8334
- breaksLoop: result.breaksLoop,
8335
- cost: result.cost,
8336
- logger: this.logger
8337
- };
8338
- await this.hooks.observers?.onGadgetExecutionComplete?.(context);
8339
- });
8340
- }
8341
- await this.runObserversInParallel(completeObservers);
8342
- this.completedResults.set(result.invocationId, result);
8343
- if (result.error) {
8344
- this.failedInvocations.add(result.invocationId);
8345
- }
8346
- events.push({ type: "gadget_result", result });
8347
- return events;
8348
- }
8349
- /**
8350
- * Execute a gadget and yield the result event.
8351
- * Generator version that yields gadget_result immediately when execution completes.
8192
+ * Execute a gadget through the full hook lifecycle and yield events.
8193
+ * Handles parameter interception, before/after controllers, observers,
8194
+ * execution, result interception, and tree tracking.
8352
8195
  */
8353
8196
  async *executeGadgetGenerator(call) {
8354
8197
  if (call.parseError) {
@@ -8614,8 +8457,9 @@ var init_stream_processor = __esm({
8614
8457
  invocationId: call.invocationId,
8615
8458
  failedDependency: failedDep
8616
8459
  });
8617
- const executeEvents = await this.executeGadgetWithHooks(call);
8618
- events.push(...executeEvents);
8460
+ for await (const evt of this.executeGadgetGenerator(call)) {
8461
+ events.push(evt);
8462
+ }
8619
8463
  } else if (action.action === "use_fallback") {
8620
8464
  const fallbackResult = {
8621
8465
  gadgetName: call.gadgetName,
@@ -8636,90 +8480,9 @@ var init_stream_processor = __esm({
8636
8480
  }
8637
8481
  /**
8638
8482
  * Process pending gadgets whose dependencies are now satisfied.
8639
- * Executes ready gadgets in parallel and continues until no more can be triggered.
8640
- */
8641
- async processPendingGadgets() {
8642
- const events = [];
8643
- let progress = true;
8644
- while (progress && this.gadgetsAwaitingDependencies.size > 0) {
8645
- progress = false;
8646
- const readyToExecute = [];
8647
- const readyToSkip = [];
8648
- for (const [invocationId, call] of this.gadgetsAwaitingDependencies) {
8649
- const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
8650
- if (failedDep) {
8651
- readyToSkip.push({ call, failedDep });
8652
- continue;
8653
- }
8654
- const allSatisfied = call.dependencies.every((dep) => this.completedResults.has(dep));
8655
- if (allSatisfied) {
8656
- readyToExecute.push(call);
8657
- }
8658
- }
8659
- for (const { call, failedDep } of readyToSkip) {
8660
- this.gadgetsAwaitingDependencies.delete(call.invocationId);
8661
- const skipEvents = await this.handleFailedDependency(call, failedDep);
8662
- events.push(...skipEvents);
8663
- progress = true;
8664
- }
8665
- if (readyToExecute.length > 0) {
8666
- this.logger.debug("Executing ready gadgets in parallel", {
8667
- count: readyToExecute.length,
8668
- invocationIds: readyToExecute.map((c) => c.invocationId)
8669
- });
8670
- for (const call of readyToExecute) {
8671
- this.gadgetsAwaitingDependencies.delete(call.invocationId);
8672
- }
8673
- const executePromises = readyToExecute.map((call) => this.executeGadgetWithHooks(call));
8674
- const results = await Promise.all(executePromises);
8675
- for (const executeEvents of results) {
8676
- events.push(...executeEvents);
8677
- }
8678
- progress = true;
8679
- }
8680
- }
8681
- if (this.gadgetsAwaitingDependencies.size > 0) {
8682
- const pendingIds = new Set(this.gadgetsAwaitingDependencies.keys());
8683
- for (const [invocationId, call] of this.gadgetsAwaitingDependencies) {
8684
- const missingDeps = call.dependencies.filter((dep) => !this.completedResults.has(dep));
8685
- const circularDeps = missingDeps.filter((dep) => pendingIds.has(dep));
8686
- const trulyMissingDeps = missingDeps.filter((dep) => !pendingIds.has(dep));
8687
- let errorMessage;
8688
- let logLevel = "warn";
8689
- if (circularDeps.length > 0 && trulyMissingDeps.length > 0) {
8690
- errorMessage = `Dependencies unresolvable: circular=[${circularDeps.join(", ")}], missing=[${trulyMissingDeps.join(", ")}]`;
8691
- logLevel = "error";
8692
- } else if (circularDeps.length > 0) {
8693
- errorMessage = `Circular dependency detected: "${invocationId}" depends on "${circularDeps[0]}" which also depends on "${invocationId}" (directly or indirectly)`;
8694
- } else {
8695
- errorMessage = `Dependency "${missingDeps[0]}" was never executed - check that the invocation ID exists and is spelled correctly`;
8696
- }
8697
- this.logger[logLevel]("Gadget has unresolvable dependencies", {
8698
- gadgetName: call.gadgetName,
8699
- invocationId,
8700
- circularDependencies: circularDeps,
8701
- missingDependencies: trulyMissingDeps
8702
- });
8703
- this.failedInvocations.add(invocationId);
8704
- const skipEvent = {
8705
- type: "gadget_skipped",
8706
- gadgetName: call.gadgetName,
8707
- invocationId,
8708
- parameters: call.parameters ?? {},
8709
- failedDependency: missingDeps[0],
8710
- failedDependencyError: errorMessage
8711
- };
8712
- events.push(skipEvent);
8713
- }
8714
- this.gadgetsAwaitingDependencies.clear();
8715
- }
8716
- return events;
8717
- }
8718
- /**
8719
- * Process pending gadgets, yielding events in real-time.
8720
- * Generator version that yields events as gadgets complete.
8483
+ * Yields events in real-time as gadgets complete.
8721
8484
  *
8722
- * Note: Gadgets are still executed in parallel for efficiency,
8485
+ * Gadgets are executed in parallel for efficiency,
8723
8486
  * but results are yielded as they become available.
8724
8487
  */
8725
8488
  async *processPendingGadgetsGenerator() {
@@ -8729,12 +8492,16 @@ var init_stream_processor = __esm({
8729
8492
  const readyToExecute = [];
8730
8493
  const readyToSkip = [];
8731
8494
  for (const [_invocationId, call] of this.gadgetsAwaitingDependencies) {
8732
- const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
8495
+ const failedDep = call.dependencies.find(
8496
+ (dep) => this.failedInvocations.has(dep) || this.priorFailedInvocations.has(dep)
8497
+ );
8733
8498
  if (failedDep) {
8734
8499
  readyToSkip.push({ call, failedDep });
8735
8500
  continue;
8736
8501
  }
8737
- const allSatisfied = call.dependencies.every((dep) => this.completedResults.has(dep));
8502
+ const allSatisfied = call.dependencies.every(
8503
+ (dep) => this.completedResults.has(dep) || this.priorCompletedInvocations.has(dep)
8504
+ );
8738
8505
  if (allSatisfied) {
8739
8506
  readyToExecute.push(call);
8740
8507
  }
@@ -8775,7 +8542,9 @@ var init_stream_processor = __esm({
8775
8542
  if (this.gadgetsAwaitingDependencies.size > 0) {
8776
8543
  const pendingIds = new Set(this.gadgetsAwaitingDependencies.keys());
8777
8544
  for (const [invocationId, call] of this.gadgetsAwaitingDependencies) {
8778
- const missingDeps = call.dependencies.filter((dep) => !this.completedResults.has(dep));
8545
+ const missingDeps = call.dependencies.filter(
8546
+ (dep) => !this.completedResults.has(dep) && !this.priorCompletedInvocations.has(dep)
8547
+ );
8779
8548
  const circularDeps = missingDeps.filter((dep) => pendingIds.has(dep));
8780
8549
  const trulyMissingDeps = missingDeps.filter((dep) => !pendingIds.has(dep));
8781
8550
  let errorMessage;
@@ -8829,10 +8598,27 @@ var init_stream_processor = __esm({
8829
8598
  */
8830
8599
  async runObserversInParallel(observers) {
8831
8600
  if (observers.length === 0) return;
8832
- const results = await Promise.allSettled(
8601
+ await Promise.allSettled(
8833
8602
  observers.map((observer) => this.safeObserve(observer))
8834
8603
  );
8835
8604
  }
8605
+ // ==========================================================================
8606
+ // Public accessors for cross-iteration dependency tracking
8607
+ // ==========================================================================
8608
+ /**
8609
+ * Get all invocation IDs that completed successfully in this iteration.
8610
+ * Used by Agent to accumulate completed IDs across iterations.
8611
+ */
8612
+ getCompletedInvocationIds() {
8613
+ return new Set(this.completedResults.keys());
8614
+ }
8615
+ /**
8616
+ * Get all invocation IDs that failed in this iteration.
8617
+ * Used by Agent to accumulate failed IDs across iterations.
8618
+ */
8619
+ getFailedInvocationIds() {
8620
+ return new Set(this.failedInvocations);
8621
+ }
8836
8622
  };
8837
8623
  }
8838
8624
  });
@@ -8895,6 +8681,9 @@ var init_agent = __esm({
8895
8681
  onSubagentEvent;
8896
8682
  // Counter for generating synthetic invocation IDs for wrapped text content
8897
8683
  syntheticInvocationCounter = 0;
8684
+ // Cross-iteration dependency tracking - allows gadgets to depend on results from prior iterations
8685
+ completedInvocationIds = /* @__PURE__ */ new Set();
8686
+ failedInvocationIds = /* @__PURE__ */ new Set();
8898
8687
  // Execution Tree - first-class model for nested subagent support
8899
8688
  tree;
8900
8689
  parentNodeId;
@@ -9204,96 +8993,22 @@ var init_agent = __esm({
9204
8993
  maxIterations: this.maxIterations
9205
8994
  });
9206
8995
  while (currentIteration < this.maxIterations) {
9207
- if (this.signal?.aborted) {
9208
- this.logger.info("Agent loop terminated by abort signal", {
9209
- iteration: currentIteration,
9210
- reason: this.signal.reason
9211
- });
9212
- await this.safeObserve(async () => {
9213
- if (this.hooks.observers?.onAbort) {
9214
- const context = {
9215
- iteration: currentIteration,
9216
- reason: this.signal?.reason,
9217
- logger: this.logger
9218
- };
9219
- await this.hooks.observers.onAbort(context);
9220
- }
9221
- });
8996
+ if (await this.checkAbortAndNotify(currentIteration)) {
9222
8997
  return;
9223
8998
  }
9224
8999
  this.logger.debug("Starting iteration", { iteration: currentIteration });
9225
9000
  try {
9226
- if (this.compactionManager) {
9227
- const compactionEvent = await this.compactionManager.checkAndCompact(
9228
- this.conversation,
9229
- currentIteration
9230
- );
9231
- if (compactionEvent) {
9232
- this.logger.info("Context compacted", {
9233
- strategy: compactionEvent.strategy,
9234
- tokensBefore: compactionEvent.tokensBefore,
9235
- tokensAfter: compactionEvent.tokensAfter
9236
- });
9237
- yield { type: "compaction", event: compactionEvent };
9238
- await this.safeObserve(async () => {
9239
- if (this.hooks.observers?.onCompaction) {
9240
- await this.hooks.observers.onCompaction({
9241
- iteration: currentIteration,
9242
- event: compactionEvent,
9243
- // biome-ignore lint/style/noNonNullAssertion: compactionManager exists if compactionEvent is truthy
9244
- stats: this.compactionManager.getStats(),
9245
- logger: this.logger
9246
- });
9247
- }
9248
- });
9249
- }
9001
+ const compactionEvent = await this.checkAndPerformCompaction(currentIteration);
9002
+ if (compactionEvent) {
9003
+ yield compactionEvent;
9250
9004
  }
9251
- let llmOptions = {
9252
- model: this.model,
9253
- messages: this.conversation.getMessages(),
9254
- temperature: this.temperature,
9255
- maxTokens: this.defaultMaxTokens,
9256
- signal: this.signal
9257
- };
9258
- await this.safeObserve(async () => {
9259
- if (this.hooks.observers?.onLLMCallStart) {
9260
- const context = {
9261
- iteration: currentIteration,
9262
- options: llmOptions,
9263
- logger: this.logger
9264
- };
9265
- await this.hooks.observers.onLLMCallStart(context);
9266
- }
9267
- });
9268
- if (this.hooks.controllers?.beforeLLMCall) {
9269
- const context = {
9270
- iteration: currentIteration,
9271
- maxIterations: this.maxIterations,
9272
- options: llmOptions,
9273
- logger: this.logger
9274
- };
9275
- const action = await this.hooks.controllers.beforeLLMCall(context);
9276
- validateBeforeLLMCallAction(action);
9277
- if (action.action === "skip") {
9278
- this.logger.info("Controller skipped LLM call, using synthetic response");
9279
- this.conversation.addAssistantMessage(action.syntheticResponse);
9280
- yield { type: "text", content: action.syntheticResponse };
9281
- break;
9282
- } else if (action.action === "proceed" && action.modifiedOptions) {
9283
- llmOptions = { ...llmOptions, ...action.modifiedOptions };
9284
- }
9005
+ const prepared = await this.prepareLLMCall(currentIteration);
9006
+ const llmOptions = prepared.options;
9007
+ if (prepared.skipWithSynthetic !== void 0) {
9008
+ this.conversation.addAssistantMessage(prepared.skipWithSynthetic);
9009
+ yield { type: "text", content: prepared.skipWithSynthetic };
9010
+ break;
9285
9011
  }
9286
- await this.safeObserve(async () => {
9287
- if (this.hooks.observers?.onLLMCallReady) {
9288
- const context = {
9289
- iteration: currentIteration,
9290
- maxIterations: this.maxIterations,
9291
- options: llmOptions,
9292
- logger: this.logger
9293
- };
9294
- await this.hooks.observers.onLLMCallReady(context);
9295
- }
9296
- });
9297
9012
  this.logger.info("Calling LLM", { model: this.model });
9298
9013
  this.logger.silly("LLM request details", {
9299
9014
  model: llmOptions.model,
@@ -9329,7 +9044,10 @@ var init_agent = __esm({
9329
9044
  tree: this.tree,
9330
9045
  parentNodeId: currentLLMNodeId,
9331
9046
  // Gadgets are children of this LLM call
9332
- baseDepth: this.baseDepth
9047
+ baseDepth: this.baseDepth,
9048
+ // Cross-iteration dependency tracking
9049
+ priorCompletedInvocations: this.completedInvocationIds,
9050
+ priorFailedInvocations: this.failedInvocationIds
9333
9051
  });
9334
9052
  let streamMetadata = null;
9335
9053
  let gadgetCallCount = 0;
@@ -9352,6 +9070,12 @@ var init_agent = __esm({
9352
9070
  if (!streamMetadata) {
9353
9071
  throw new Error("Stream processing completed without metadata event");
9354
9072
  }
9073
+ for (const id of processor.getCompletedInvocationIds()) {
9074
+ this.completedInvocationIds.add(id);
9075
+ }
9076
+ for (const id of processor.getFailedInvocationIds()) {
9077
+ this.failedInvocationIds.add(id);
9078
+ }
9355
9079
  const result = streamMetadata;
9356
9080
  this.logger.info("LLM response completed", {
9357
9081
  finishReason: result.finishReason,
@@ -9375,81 +9099,21 @@ var init_agent = __esm({
9375
9099
  await this.hooks.observers.onLLMCallComplete(context);
9376
9100
  }
9377
9101
  });
9378
- this.tree.completeLLMCall(currentLLMNodeId, {
9379
- response: result.rawResponse,
9380
- usage: result.usage,
9381
- finishReason: result.finishReason
9382
- });
9383
- let finalMessage = result.finalMessage;
9384
- if (this.hooks.controllers?.afterLLMCall) {
9385
- const context = {
9386
- iteration: currentIteration,
9387
- maxIterations: this.maxIterations,
9388
- options: llmOptions,
9389
- finishReason: result.finishReason,
9390
- usage: result.usage,
9391
- finalMessage: result.finalMessage,
9392
- gadgetCallCount,
9393
- logger: this.logger
9394
- };
9395
- const action = await this.hooks.controllers.afterLLMCall(context);
9396
- validateAfterLLMCallAction(action);
9397
- if (action.action === "modify_and_continue" || action.action === "append_and_modify") {
9398
- finalMessage = action.modifiedMessage;
9399
- }
9400
- if (action.action === "append_messages" || action.action === "append_and_modify") {
9401
- for (const msg of action.messages) {
9402
- if (msg.role === "user") {
9403
- this.conversation.addUserMessage(msg.content);
9404
- } else if (msg.role === "assistant") {
9405
- this.conversation.addAssistantMessage(extractMessageText(msg.content));
9406
- } else if (msg.role === "system") {
9407
- this.conversation.addUserMessage(`[System] ${extractMessageText(msg.content)}`);
9408
- }
9409
- }
9410
- }
9411
- }
9412
- if (result.didExecuteGadgets) {
9413
- if (this.textWithGadgetsHandler) {
9414
- const textContent = textOutputs.join("");
9415
- if (textContent.trim()) {
9416
- const { gadgetName, parameterMapping, resultMapping } = this.textWithGadgetsHandler;
9417
- const syntheticId = `gc_text_${++this.syntheticInvocationCounter}`;
9418
- this.conversation.addGadgetCallResult(
9419
- gadgetName,
9420
- parameterMapping(textContent),
9421
- resultMapping ? resultMapping(textContent) : textContent,
9422
- syntheticId
9423
- );
9424
- }
9425
- }
9426
- for (const output of gadgetResults) {
9427
- if (output.type === "gadget_result") {
9428
- const gadgetResult = output.result;
9429
- this.conversation.addGadgetCallResult(
9430
- gadgetResult.gadgetName,
9431
- gadgetResult.parameters,
9432
- gadgetResult.error ?? gadgetResult.result ?? "",
9433
- gadgetResult.invocationId,
9434
- gadgetResult.media,
9435
- gadgetResult.mediaIds
9436
- );
9437
- }
9438
- }
9439
- } else {
9440
- if (finalMessage.trim()) {
9441
- const syntheticId = `gc_tell_${++this.syntheticInvocationCounter}`;
9442
- this.conversation.addGadgetCallResult(
9443
- "TellUser",
9444
- { message: finalMessage, done: false, type: "info" },
9445
- `\u2139\uFE0F ${finalMessage}`,
9446
- syntheticId
9447
- );
9448
- }
9449
- const shouldBreak = await this.handleTextOnlyResponse(finalMessage);
9450
- if (shouldBreak) {
9451
- break;
9452
- }
9102
+ this.completeLLMCallInTree(currentLLMNodeId, result);
9103
+ const finalMessage = await this.processAfterLLMCallController(
9104
+ currentIteration,
9105
+ llmOptions,
9106
+ result,
9107
+ gadgetCallCount
9108
+ );
9109
+ const shouldBreakFromTextOnly = await this.updateConversationWithResults(
9110
+ result.didExecuteGadgets,
9111
+ textOutputs,
9112
+ gadgetResults,
9113
+ finalMessage
9114
+ );
9115
+ if (shouldBreakFromTextOnly) {
9116
+ break;
9453
9117
  }
9454
9118
  if (result.shouldBreakLoop) {
9455
9119
  this.logger.info("Loop terminated by gadget or processor");
@@ -9602,6 +9266,210 @@ var init_agent = __esm({
9602
9266
  }
9603
9267
  };
9604
9268
  }
9269
+ // ==========================================================================
9270
+ // Agent Loop Helper Methods (extracted from run() for readability)
9271
+ // ==========================================================================
9272
+ /**
9273
+ * Check abort signal and notify observers if aborted.
9274
+ * @returns true if agent should terminate
9275
+ */
9276
+ async checkAbortAndNotify(iteration) {
9277
+ if (!this.signal?.aborted) return false;
9278
+ this.logger.info("Agent loop terminated by abort signal", {
9279
+ iteration,
9280
+ reason: this.signal.reason
9281
+ });
9282
+ await this.safeObserve(async () => {
9283
+ if (this.hooks.observers?.onAbort) {
9284
+ const context = {
9285
+ iteration,
9286
+ reason: this.signal?.reason,
9287
+ logger: this.logger
9288
+ };
9289
+ await this.hooks.observers.onAbort(context);
9290
+ }
9291
+ });
9292
+ return true;
9293
+ }
9294
+ /**
9295
+ * Check and perform context compaction if needed.
9296
+ * @returns compaction stream event if compaction occurred, null otherwise
9297
+ */
9298
+ async checkAndPerformCompaction(iteration) {
9299
+ if (!this.compactionManager) return null;
9300
+ const compactionEvent = await this.compactionManager.checkAndCompact(
9301
+ this.conversation,
9302
+ iteration
9303
+ );
9304
+ if (!compactionEvent) return null;
9305
+ this.logger.info("Context compacted", {
9306
+ strategy: compactionEvent.strategy,
9307
+ tokensBefore: compactionEvent.tokensBefore,
9308
+ tokensAfter: compactionEvent.tokensAfter
9309
+ });
9310
+ await this.safeObserve(async () => {
9311
+ if (this.hooks.observers?.onCompaction) {
9312
+ await this.hooks.observers.onCompaction({
9313
+ iteration,
9314
+ event: compactionEvent,
9315
+ // biome-ignore lint/style/noNonNullAssertion: compactionManager exists if compactionEvent is truthy
9316
+ stats: this.compactionManager.getStats(),
9317
+ logger: this.logger
9318
+ });
9319
+ }
9320
+ });
9321
+ return { type: "compaction", event: compactionEvent };
9322
+ }
9323
+ /**
9324
+ * Prepare LLM call options and process beforeLLMCall controller.
9325
+ * @returns options and optional skipWithSynthetic response if controller wants to skip
9326
+ */
9327
+ async prepareLLMCall(iteration) {
9328
+ let llmOptions = {
9329
+ model: this.model,
9330
+ messages: this.conversation.getMessages(),
9331
+ temperature: this.temperature,
9332
+ maxTokens: this.defaultMaxTokens,
9333
+ signal: this.signal
9334
+ };
9335
+ await this.safeObserve(async () => {
9336
+ if (this.hooks.observers?.onLLMCallStart) {
9337
+ const context = {
9338
+ iteration,
9339
+ options: llmOptions,
9340
+ logger: this.logger
9341
+ };
9342
+ await this.hooks.observers.onLLMCallStart(context);
9343
+ }
9344
+ });
9345
+ if (this.hooks.controllers?.beforeLLMCall) {
9346
+ const context = {
9347
+ iteration,
9348
+ maxIterations: this.maxIterations,
9349
+ options: llmOptions,
9350
+ logger: this.logger
9351
+ };
9352
+ const action = await this.hooks.controllers.beforeLLMCall(context);
9353
+ validateBeforeLLMCallAction(action);
9354
+ if (action.action === "skip") {
9355
+ this.logger.info("Controller skipped LLM call, using synthetic response");
9356
+ return { options: llmOptions, skipWithSynthetic: action.syntheticResponse };
9357
+ } else if (action.action === "proceed" && action.modifiedOptions) {
9358
+ llmOptions = { ...llmOptions, ...action.modifiedOptions };
9359
+ }
9360
+ }
9361
+ await this.safeObserve(async () => {
9362
+ if (this.hooks.observers?.onLLMCallReady) {
9363
+ const context = {
9364
+ iteration,
9365
+ maxIterations: this.maxIterations,
9366
+ options: llmOptions,
9367
+ logger: this.logger
9368
+ };
9369
+ await this.hooks.observers.onLLMCallReady(context);
9370
+ }
9371
+ });
9372
+ return { options: llmOptions };
9373
+ }
9374
+ /**
9375
+ * Calculate cost and complete LLM call in execution tree.
9376
+ */
9377
+ completeLLMCallInTree(nodeId, result) {
9378
+ const llmCost = this.client.modelRegistry?.estimateCost?.(
9379
+ this.model,
9380
+ result.usage?.inputTokens ?? 0,
9381
+ result.usage?.outputTokens ?? 0,
9382
+ result.usage?.cachedInputTokens ?? 0,
9383
+ result.usage?.cacheCreationInputTokens ?? 0
9384
+ )?.totalCost;
9385
+ this.tree.completeLLMCall(nodeId, {
9386
+ response: result.rawResponse,
9387
+ usage: result.usage,
9388
+ finishReason: result.finishReason,
9389
+ cost: llmCost
9390
+ });
9391
+ }
9392
+ /**
9393
+ * Process afterLLMCall controller and return modified final message.
9394
+ */
9395
+ async processAfterLLMCallController(iteration, llmOptions, result, gadgetCallCount) {
9396
+ let finalMessage = result.finalMessage;
9397
+ if (!this.hooks.controllers?.afterLLMCall) {
9398
+ return finalMessage;
9399
+ }
9400
+ const context = {
9401
+ iteration,
9402
+ maxIterations: this.maxIterations,
9403
+ options: llmOptions,
9404
+ finishReason: result.finishReason,
9405
+ usage: result.usage,
9406
+ finalMessage: result.finalMessage,
9407
+ gadgetCallCount,
9408
+ logger: this.logger
9409
+ };
9410
+ const action = await this.hooks.controllers.afterLLMCall(context);
9411
+ validateAfterLLMCallAction(action);
9412
+ if (action.action === "modify_and_continue" || action.action === "append_and_modify") {
9413
+ finalMessage = action.modifiedMessage;
9414
+ }
9415
+ if (action.action === "append_messages" || action.action === "append_and_modify") {
9416
+ for (const msg of action.messages) {
9417
+ if (msg.role === "user") {
9418
+ this.conversation.addUserMessage(msg.content);
9419
+ } else if (msg.role === "assistant") {
9420
+ this.conversation.addAssistantMessage(extractMessageText(msg.content));
9421
+ } else if (msg.role === "system") {
9422
+ this.conversation.addUserMessage(`[System] ${extractMessageText(msg.content)}`);
9423
+ }
9424
+ }
9425
+ }
9426
+ return finalMessage;
9427
+ }
9428
+ /**
9429
+ * Update conversation history with gadget results or text-only response.
9430
+ * @returns true if loop should break (text-only handler requested termination)
9431
+ */
9432
+ async updateConversationWithResults(didExecuteGadgets, textOutputs, gadgetResults, finalMessage) {
9433
+ if (didExecuteGadgets) {
9434
+ if (this.textWithGadgetsHandler) {
9435
+ const textContent = textOutputs.join("");
9436
+ if (textContent.trim()) {
9437
+ const { gadgetName, parameterMapping, resultMapping } = this.textWithGadgetsHandler;
9438
+ const syntheticId = `gc_text_${++this.syntheticInvocationCounter}`;
9439
+ this.conversation.addGadgetCallResult(
9440
+ gadgetName,
9441
+ parameterMapping(textContent),
9442
+ resultMapping ? resultMapping(textContent) : textContent,
9443
+ syntheticId
9444
+ );
9445
+ }
9446
+ }
9447
+ for (const output of gadgetResults) {
9448
+ if (output.type === "gadget_result") {
9449
+ const gadgetResult = output.result;
9450
+ this.conversation.addGadgetCallResult(
9451
+ gadgetResult.gadgetName,
9452
+ gadgetResult.parameters,
9453
+ gadgetResult.error ?? gadgetResult.result ?? "",
9454
+ gadgetResult.invocationId,
9455
+ gadgetResult.media,
9456
+ gadgetResult.mediaIds
9457
+ );
9458
+ }
9459
+ }
9460
+ return false;
9461
+ }
9462
+ if (finalMessage.trim()) {
9463
+ const syntheticId = `gc_tell_${++this.syntheticInvocationCounter}`;
9464
+ this.conversation.addGadgetCallResult(
9465
+ "TellUser",
9466
+ { message: finalMessage, done: false, type: "info" },
9467
+ `\u2139\uFE0F ${finalMessage}`,
9468
+ syntheticId
9469
+ );
9470
+ }
9471
+ return await this.handleTextOnlyResponse(finalMessage);
9472
+ }
9605
9473
  /**
9606
9474
  * Run agent with named event handlers (syntactic sugar).
9607
9475
  *