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.
@@ -52,6 +52,9 @@ function parseEnvBoolean(value) {
52
52
  if (normalized === "false" || normalized === "0") return false;
53
53
  return void 0;
54
54
  }
55
+ function stripAnsi(str) {
56
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
57
+ }
55
58
  function createLogger(options = {}) {
56
59
  const envMinLevel = parseLogLevel(process.env.LLMIST_LOG_LEVEL);
57
60
  const envLogFile = process.env.LLMIST_LOG_FILE?.trim() ?? "";
@@ -60,36 +63,62 @@ function createLogger(options = {}) {
60
63
  const defaultType = options.type ?? "pretty";
61
64
  const name = options.name ?? "llmist";
62
65
  const logReset = options.logReset ?? envLogReset ?? false;
63
- let logFileStream;
64
- let finalType = defaultType;
65
- if (envLogFile) {
66
+ if (envLogFile && (!logFileInitialized || sharedLogFilePath !== envLogFile)) {
66
67
  try {
68
+ if (sharedLogFileStream) {
69
+ sharedLogFileStream.end();
70
+ sharedLogFileStream = void 0;
71
+ }
67
72
  (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(envLogFile), { recursive: true });
68
73
  const flags = logReset ? "w" : "a";
69
- logFileStream = (0, import_node_fs.createWriteStream)(envLogFile, { flags });
70
- finalType = "hidden";
74
+ sharedLogFileStream = (0, import_node_fs.createWriteStream)(envLogFile, { flags });
75
+ sharedLogFilePath = envLogFile;
76
+ logFileInitialized = true;
77
+ writeErrorCount = 0;
78
+ writeErrorReported = false;
79
+ sharedLogFileStream.on("error", (error) => {
80
+ writeErrorCount++;
81
+ if (!writeErrorReported) {
82
+ console.error(`[llmist] Log file write error: ${error.message}`);
83
+ writeErrorReported = true;
84
+ }
85
+ if (writeErrorCount >= MAX_WRITE_ERRORS_BEFORE_DISABLE) {
86
+ console.error(
87
+ `[llmist] Too many log file errors (${writeErrorCount}), disabling file logging`
88
+ );
89
+ sharedLogFileStream?.end();
90
+ sharedLogFileStream = void 0;
91
+ }
92
+ });
71
93
  } catch (error) {
72
94
  console.error("Failed to initialize LLMIST_LOG_FILE output:", error);
73
95
  }
74
96
  }
97
+ const useFileLogging = Boolean(sharedLogFileStream);
75
98
  const logger = new import_tslog.Logger({
76
99
  name,
77
100
  minLevel,
78
- type: finalType,
79
- // Optimize for production
80
- hideLogPositionForProduction: finalType !== "pretty",
81
- // Pretty output settings
82
- prettyLogTemplate: finalType === "pretty" ? "{{yyyy}}-{{mm}}-{{dd}} {{hh}}:{{MM}}:{{ss}}:{{ms}} {{logLevelName}} [{{name}}] " : void 0
101
+ type: useFileLogging ? "pretty" : defaultType,
102
+ // Hide log position for file logging and non-pretty types
103
+ hideLogPositionForProduction: useFileLogging || defaultType !== "pretty",
104
+ prettyLogTemplate: LOG_TEMPLATE,
105
+ // Use overwrite to redirect tslog's formatted output to file instead of console
106
+ overwrite: useFileLogging ? {
107
+ transportFormatted: (logMetaMarkup, logArgs, _logErrors) => {
108
+ if (!sharedLogFileStream) return;
109
+ const meta = stripAnsi(logMetaMarkup);
110
+ const args = logArgs.map(
111
+ (arg) => typeof arg === "string" ? stripAnsi(arg) : JSON.stringify(arg)
112
+ );
113
+ const line = `${meta}${args.join(" ")}
114
+ `;
115
+ sharedLogFileStream.write(line);
116
+ }
117
+ } : void 0
83
118
  });
84
- if (logFileStream) {
85
- logger.attachTransport((logObj) => {
86
- logFileStream?.write(`${JSON.stringify(logObj)}
87
- `);
88
- });
89
- }
90
119
  return logger;
91
120
  }
92
- var import_node_fs, import_node_path, import_tslog, LEVEL_NAME_TO_ID, defaultLogger;
121
+ var import_node_fs, import_node_path, import_tslog, LEVEL_NAME_TO_ID, sharedLogFilePath, sharedLogFileStream, logFileInitialized, writeErrorCount, writeErrorReported, MAX_WRITE_ERRORS_BEFORE_DISABLE, LOG_TEMPLATE, defaultLogger;
93
122
  var init_logger = __esm({
94
123
  "src/logging/logger.ts"() {
95
124
  "use strict";
@@ -105,6 +134,11 @@ var init_logger = __esm({
105
134
  error: 5,
106
135
  fatal: 6
107
136
  };
137
+ logFileInitialized = false;
138
+ writeErrorCount = 0;
139
+ writeErrorReported = false;
140
+ MAX_WRITE_ERRORS_BEFORE_DISABLE = 5;
141
+ LOG_TEMPLATE = "{{yyyy}}-{{mm}}-{{dd}} {{hh}}:{{MM}}:{{ss}}:{{ms}} {{logLevelName}} [{{name}}] ";
108
142
  defaultLogger = createLogger();
109
143
  }
110
144
  });
@@ -3795,9 +3829,8 @@ var init_cost_reporting_client = __esm({
3795
3829
  */
3796
3830
  reportCostFromUsage(model, inputTokens, outputTokens, cachedInputTokens = 0, cacheCreationInputTokens = 0) {
3797
3831
  if (inputTokens === 0 && outputTokens === 0) return;
3798
- const modelName = model.includes(":") ? model.split(":")[1] : model;
3799
3832
  const estimate = this.client.modelRegistry.estimateCost(
3800
- modelName,
3833
+ model,
3801
3834
  inputTokens,
3802
3835
  outputTokens,
3803
3836
  cachedInputTokens,
@@ -4120,10 +4153,11 @@ function getHostExportsInternal() {
4120
4153
  }
4121
4154
  return cachedHostExports;
4122
4155
  }
4123
- var import_zod2, cachedHostExports, GadgetExecutor;
4156
+ var import_fast_deep_equal, import_zod2, cachedHostExports, GadgetExecutor;
4124
4157
  var init_executor = __esm({
4125
4158
  "src/gadgets/executor.ts"() {
4126
4159
  "use strict";
4160
+ import_fast_deep_equal = __toESM(require("fast-deep-equal"), 1);
4127
4161
  import_zod2 = require("zod");
4128
4162
  init_builder();
4129
4163
  init_client();
@@ -4234,7 +4268,7 @@ var init_executor = __esm({
4234
4268
  try {
4235
4269
  const cleanedRaw = stripMarkdownFences(call.parametersRaw);
4236
4270
  const initialParse = parseBlockParams(cleanedRaw, { argPrefix: this.argPrefix });
4237
- const parametersWereModified = !this.deepEquals(rawParameters, initialParse);
4271
+ const parametersWereModified = !(0, import_fast_deep_equal.default)(rawParameters, initialParse);
4238
4272
  if (parametersWereModified) {
4239
4273
  this.logger.debug("Parameters modified by interceptor, skipping re-parse", {
4240
4274
  gadgetName: call.gadgetName
@@ -4311,7 +4345,9 @@ var init_executor = __esm({
4311
4345
  nodeId: gadgetNodeId,
4312
4346
  depth: gadgetDepth,
4313
4347
  // Host exports for external gadgets to use host's llmist classes
4314
- hostExports: getHostExportsInternal()
4348
+ hostExports: getHostExportsInternal(),
4349
+ // Logger for structured logging (respects CLI's log level/file config)
4350
+ logger: this.logger
4315
4351
  };
4316
4352
  let rawResult;
4317
4353
  if (timeoutMs && timeoutMs > 0) {
@@ -4481,27 +4517,6 @@ var init_executor = __esm({
4481
4517
  async executeAll(calls) {
4482
4518
  return Promise.all(calls.map((call) => this.execute(call)));
4483
4519
  }
4484
- /**
4485
- * Deep equality check for objects/arrays.
4486
- * Used to detect if parameters were modified by an interceptor.
4487
- */
4488
- deepEquals(a, b) {
4489
- if (a === b) return true;
4490
- if (a === null || b === null) return a === b;
4491
- if (typeof a !== typeof b) return false;
4492
- if (typeof a !== "object") return a === b;
4493
- if (Array.isArray(a) !== Array.isArray(b)) return false;
4494
- if (Array.isArray(a) && Array.isArray(b)) {
4495
- if (a.length !== b.length) return false;
4496
- return a.every((val, i) => this.deepEquals(val, b[i]));
4497
- }
4498
- const aObj = a;
4499
- const bObj = b;
4500
- const aKeys = Object.keys(aObj);
4501
- const bKeys = Object.keys(bObj);
4502
- if (aKeys.length !== bKeys.length) return false;
4503
- return aKeys.every((key) => this.deepEquals(aObj[key], bObj[key]));
4504
- }
4505
4520
  };
4506
4521
  }
4507
4522
  });
@@ -4539,6 +4554,11 @@ var init_stream_processor = __esm({
4539
4554
  inFlightExecutions = /* @__PURE__ */ new Map();
4540
4555
  /** Queue of completed gadget results ready to be yielded (for real-time streaming) */
4541
4556
  completedResultsQueue = [];
4557
+ // Cross-iteration dependency tracking
4558
+ /** Invocation IDs completed in previous iterations (read-only reference from Agent) */
4559
+ priorCompletedInvocations;
4560
+ /** Invocation IDs that failed in previous iterations (read-only reference from Agent) */
4561
+ priorFailedInvocations;
4542
4562
  constructor(options) {
4543
4563
  this.iteration = options.iteration;
4544
4564
  this.registry = options.registry;
@@ -4547,6 +4567,8 @@ var init_stream_processor = __esm({
4547
4567
  this.tree = options.tree;
4548
4568
  this.parentNodeId = options.parentNodeId ?? null;
4549
4569
  this.baseDepth = options.baseDepth ?? 0;
4570
+ this.priorCompletedInvocations = options.priorCompletedInvocations ?? /* @__PURE__ */ new Set();
4571
+ this.priorFailedInvocations = options.priorFailedInvocations ?? /* @__PURE__ */ new Set();
4550
4572
  this.parser = new GadgetCallParser({
4551
4573
  startPrefix: options.gadgetStartPrefix,
4552
4574
  endPrefix: options.gadgetEndPrefix,
@@ -4745,62 +4767,11 @@ var init_stream_processor = __esm({
4745
4767
  }
4746
4768
  return [{ type: "text", content }];
4747
4769
  }
4748
- /**
4749
- * Process a gadget call through the full lifecycle, handling dependencies.
4750
- *
4751
- * Gadgets without dependencies (or with all dependencies satisfied) execute immediately.
4752
- * Gadgets with unsatisfied dependencies are queued for later execution.
4753
- * After each execution, pending gadgets are checked to see if they can now run.
4754
- */
4755
- async processGadgetCall(call) {
4756
- const events = [];
4757
- events.push({ type: "gadget_call", call });
4758
- if (call.dependencies.length > 0) {
4759
- if (call.dependencies.includes(call.invocationId)) {
4760
- this.logger.warn("Gadget has self-referential dependency (depends on itself)", {
4761
- gadgetName: call.gadgetName,
4762
- invocationId: call.invocationId
4763
- });
4764
- this.failedInvocations.add(call.invocationId);
4765
- const skipEvent = {
4766
- type: "gadget_skipped",
4767
- gadgetName: call.gadgetName,
4768
- invocationId: call.invocationId,
4769
- parameters: call.parameters ?? {},
4770
- failedDependency: call.invocationId,
4771
- failedDependencyError: `Gadget "${call.invocationId}" cannot depend on itself (self-referential dependency)`
4772
- };
4773
- events.push(skipEvent);
4774
- return events;
4775
- }
4776
- const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
4777
- if (failedDep) {
4778
- const skipEvents = await this.handleFailedDependency(call, failedDep);
4779
- events.push(...skipEvents);
4780
- return events;
4781
- }
4782
- const unsatisfied = call.dependencies.filter((dep) => !this.completedResults.has(dep));
4783
- if (unsatisfied.length > 0) {
4784
- this.logger.debug("Queueing gadget for later - waiting on dependencies", {
4785
- gadgetName: call.gadgetName,
4786
- invocationId: call.invocationId,
4787
- waitingOn: unsatisfied
4788
- });
4789
- this.gadgetsAwaitingDependencies.set(call.invocationId, call);
4790
- return events;
4791
- }
4792
- }
4793
- const executeEvents = await this.executeGadgetWithHooks(call);
4794
- events.push(...executeEvents);
4795
- const triggeredEvents = await this.processPendingGadgets();
4796
- events.push(...triggeredEvents);
4797
- return events;
4798
- }
4799
4770
  /**
4800
4771
  * Process a gadget call, yielding events in real-time.
4801
4772
  *
4802
- * Key difference from processGadgetCall: yields gadget_call event IMMEDIATELY
4803
- * when parsed (before execution), enabling real-time UI feedback.
4773
+ * Yields gadget_call event IMMEDIATELY when parsed (before execution),
4774
+ * enabling real-time UI feedback.
4804
4775
  */
4805
4776
  async *processGadgetCallGenerator(call) {
4806
4777
  yield { type: "gadget_call", call };
@@ -4831,7 +4802,9 @@ var init_stream_processor = __esm({
4831
4802
  yield skipEvent;
4832
4803
  return;
4833
4804
  }
4834
- const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
4805
+ const failedDep = call.dependencies.find(
4806
+ (dep) => this.failedInvocations.has(dep) || this.priorFailedInvocations.has(dep)
4807
+ );
4835
4808
  if (failedDep) {
4836
4809
  const skipEvents = await this.handleFailedDependency(call, failedDep);
4837
4810
  for (const evt of skipEvents) {
@@ -4839,7 +4812,9 @@ var init_stream_processor = __esm({
4839
4812
  }
4840
4813
  return;
4841
4814
  }
4842
- const unsatisfied = call.dependencies.filter((dep) => !this.completedResults.has(dep));
4815
+ const unsatisfied = call.dependencies.filter(
4816
+ (dep) => !this.completedResults.has(dep) && !this.priorCompletedInvocations.has(dep)
4817
+ );
4843
4818
  if (unsatisfied.length > 0) {
4844
4819
  this.logger.debug("Queueing gadget for later - waiting on dependencies", {
4845
4820
  gadgetName: call.gadgetName,
@@ -4861,143 +4836,9 @@ var init_stream_processor = __esm({
4861
4836
  this.inFlightExecutions.set(call.invocationId, executionPromise);
4862
4837
  }
4863
4838
  /**
4864
- * Execute a gadget through the full hook lifecycle.
4865
- * This is the core execution logic, extracted from processGadgetCall.
4866
- * @deprecated Use executeGadgetGenerator for real-time streaming
4867
- */
4868
- async executeGadgetWithHooks(call) {
4869
- const events = [];
4870
- if (call.parseError) {
4871
- this.logger.warn("Gadget has parse error", {
4872
- gadgetName: call.gadgetName,
4873
- error: call.parseError,
4874
- rawParameters: call.parametersRaw
4875
- });
4876
- }
4877
- let parameters = call.parameters ?? {};
4878
- if (this.hooks.interceptors?.interceptGadgetParameters) {
4879
- const context = {
4880
- iteration: this.iteration,
4881
- gadgetName: call.gadgetName,
4882
- invocationId: call.invocationId,
4883
- logger: this.logger
4884
- };
4885
- parameters = this.hooks.interceptors.interceptGadgetParameters(parameters, context);
4886
- }
4887
- call.parameters = parameters;
4888
- let shouldSkip = false;
4889
- let syntheticResult;
4890
- if (this.hooks.controllers?.beforeGadgetExecution) {
4891
- const context = {
4892
- iteration: this.iteration,
4893
- gadgetName: call.gadgetName,
4894
- invocationId: call.invocationId,
4895
- parameters,
4896
- logger: this.logger
4897
- };
4898
- const action = await this.hooks.controllers.beforeGadgetExecution(context);
4899
- validateBeforeGadgetExecutionAction(action);
4900
- if (action.action === "skip") {
4901
- shouldSkip = true;
4902
- syntheticResult = action.syntheticResult;
4903
- this.logger.info("Controller skipped gadget execution", {
4904
- gadgetName: call.gadgetName
4905
- });
4906
- }
4907
- }
4908
- const startObservers = [];
4909
- if (this.hooks.observers?.onGadgetExecutionStart) {
4910
- startObservers.push(async () => {
4911
- const context = {
4912
- iteration: this.iteration,
4913
- gadgetName: call.gadgetName,
4914
- invocationId: call.invocationId,
4915
- parameters,
4916
- logger: this.logger
4917
- };
4918
- await this.hooks.observers?.onGadgetExecutionStart?.(context);
4919
- });
4920
- }
4921
- await this.runObserversInParallel(startObservers);
4922
- let result;
4923
- if (shouldSkip) {
4924
- result = {
4925
- gadgetName: call.gadgetName,
4926
- invocationId: call.invocationId,
4927
- parameters,
4928
- result: syntheticResult ?? "Execution skipped",
4929
- executionTimeMs: 0
4930
- };
4931
- } else {
4932
- result = await this.executor.execute(call);
4933
- }
4934
- const originalResult = result.result;
4935
- if (result.result && this.hooks.interceptors?.interceptGadgetResult) {
4936
- const context = {
4937
- iteration: this.iteration,
4938
- gadgetName: result.gadgetName,
4939
- invocationId: result.invocationId,
4940
- parameters,
4941
- executionTimeMs: result.executionTimeMs,
4942
- logger: this.logger
4943
- };
4944
- result.result = this.hooks.interceptors.interceptGadgetResult(result.result, context);
4945
- }
4946
- if (this.hooks.controllers?.afterGadgetExecution) {
4947
- const context = {
4948
- iteration: this.iteration,
4949
- gadgetName: result.gadgetName,
4950
- invocationId: result.invocationId,
4951
- parameters,
4952
- result: result.result,
4953
- error: result.error,
4954
- executionTimeMs: result.executionTimeMs,
4955
- logger: this.logger
4956
- };
4957
- const action = await this.hooks.controllers.afterGadgetExecution(context);
4958
- validateAfterGadgetExecutionAction(action);
4959
- if (action.action === "recover" && result.error) {
4960
- this.logger.info("Controller recovered from gadget error", {
4961
- gadgetName: result.gadgetName,
4962
- originalError: result.error
4963
- });
4964
- result = {
4965
- ...result,
4966
- error: void 0,
4967
- result: action.fallbackResult
4968
- };
4969
- }
4970
- }
4971
- const completeObservers = [];
4972
- if (this.hooks.observers?.onGadgetExecutionComplete) {
4973
- completeObservers.push(async () => {
4974
- const context = {
4975
- iteration: this.iteration,
4976
- gadgetName: result.gadgetName,
4977
- invocationId: result.invocationId,
4978
- parameters,
4979
- originalResult,
4980
- finalResult: result.result,
4981
- error: result.error,
4982
- executionTimeMs: result.executionTimeMs,
4983
- breaksLoop: result.breaksLoop,
4984
- cost: result.cost,
4985
- logger: this.logger
4986
- };
4987
- await this.hooks.observers?.onGadgetExecutionComplete?.(context);
4988
- });
4989
- }
4990
- await this.runObserversInParallel(completeObservers);
4991
- this.completedResults.set(result.invocationId, result);
4992
- if (result.error) {
4993
- this.failedInvocations.add(result.invocationId);
4994
- }
4995
- events.push({ type: "gadget_result", result });
4996
- return events;
4997
- }
4998
- /**
4999
- * Execute a gadget and yield the result event.
5000
- * Generator version that yields gadget_result immediately when execution completes.
4839
+ * Execute a gadget through the full hook lifecycle and yield events.
4840
+ * Handles parameter interception, before/after controllers, observers,
4841
+ * execution, result interception, and tree tracking.
5001
4842
  */
5002
4843
  async *executeGadgetGenerator(call) {
5003
4844
  if (call.parseError) {
@@ -5263,8 +5104,9 @@ var init_stream_processor = __esm({
5263
5104
  invocationId: call.invocationId,
5264
5105
  failedDependency: failedDep
5265
5106
  });
5266
- const executeEvents = await this.executeGadgetWithHooks(call);
5267
- events.push(...executeEvents);
5107
+ for await (const evt of this.executeGadgetGenerator(call)) {
5108
+ events.push(evt);
5109
+ }
5268
5110
  } else if (action.action === "use_fallback") {
5269
5111
  const fallbackResult = {
5270
5112
  gadgetName: call.gadgetName,
@@ -5285,90 +5127,9 @@ var init_stream_processor = __esm({
5285
5127
  }
5286
5128
  /**
5287
5129
  * Process pending gadgets whose dependencies are now satisfied.
5288
- * Executes ready gadgets in parallel and continues until no more can be triggered.
5289
- */
5290
- async processPendingGadgets() {
5291
- const events = [];
5292
- let progress = true;
5293
- while (progress && this.gadgetsAwaitingDependencies.size > 0) {
5294
- progress = false;
5295
- const readyToExecute = [];
5296
- const readyToSkip = [];
5297
- for (const [invocationId, call] of this.gadgetsAwaitingDependencies) {
5298
- const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
5299
- if (failedDep) {
5300
- readyToSkip.push({ call, failedDep });
5301
- continue;
5302
- }
5303
- const allSatisfied = call.dependencies.every((dep) => this.completedResults.has(dep));
5304
- if (allSatisfied) {
5305
- readyToExecute.push(call);
5306
- }
5307
- }
5308
- for (const { call, failedDep } of readyToSkip) {
5309
- this.gadgetsAwaitingDependencies.delete(call.invocationId);
5310
- const skipEvents = await this.handleFailedDependency(call, failedDep);
5311
- events.push(...skipEvents);
5312
- progress = true;
5313
- }
5314
- if (readyToExecute.length > 0) {
5315
- this.logger.debug("Executing ready gadgets in parallel", {
5316
- count: readyToExecute.length,
5317
- invocationIds: readyToExecute.map((c) => c.invocationId)
5318
- });
5319
- for (const call of readyToExecute) {
5320
- this.gadgetsAwaitingDependencies.delete(call.invocationId);
5321
- }
5322
- const executePromises = readyToExecute.map((call) => this.executeGadgetWithHooks(call));
5323
- const results = await Promise.all(executePromises);
5324
- for (const executeEvents of results) {
5325
- events.push(...executeEvents);
5326
- }
5327
- progress = true;
5328
- }
5329
- }
5330
- if (this.gadgetsAwaitingDependencies.size > 0) {
5331
- const pendingIds = new Set(this.gadgetsAwaitingDependencies.keys());
5332
- for (const [invocationId, call] of this.gadgetsAwaitingDependencies) {
5333
- const missingDeps = call.dependencies.filter((dep) => !this.completedResults.has(dep));
5334
- const circularDeps = missingDeps.filter((dep) => pendingIds.has(dep));
5335
- const trulyMissingDeps = missingDeps.filter((dep) => !pendingIds.has(dep));
5336
- let errorMessage;
5337
- let logLevel = "warn";
5338
- if (circularDeps.length > 0 && trulyMissingDeps.length > 0) {
5339
- errorMessage = `Dependencies unresolvable: circular=[${circularDeps.join(", ")}], missing=[${trulyMissingDeps.join(", ")}]`;
5340
- logLevel = "error";
5341
- } else if (circularDeps.length > 0) {
5342
- errorMessage = `Circular dependency detected: "${invocationId}" depends on "${circularDeps[0]}" which also depends on "${invocationId}" (directly or indirectly)`;
5343
- } else {
5344
- errorMessage = `Dependency "${missingDeps[0]}" was never executed - check that the invocation ID exists and is spelled correctly`;
5345
- }
5346
- this.logger[logLevel]("Gadget has unresolvable dependencies", {
5347
- gadgetName: call.gadgetName,
5348
- invocationId,
5349
- circularDependencies: circularDeps,
5350
- missingDependencies: trulyMissingDeps
5351
- });
5352
- this.failedInvocations.add(invocationId);
5353
- const skipEvent = {
5354
- type: "gadget_skipped",
5355
- gadgetName: call.gadgetName,
5356
- invocationId,
5357
- parameters: call.parameters ?? {},
5358
- failedDependency: missingDeps[0],
5359
- failedDependencyError: errorMessage
5360
- };
5361
- events.push(skipEvent);
5362
- }
5363
- this.gadgetsAwaitingDependencies.clear();
5364
- }
5365
- return events;
5366
- }
5367
- /**
5368
- * Process pending gadgets, yielding events in real-time.
5369
- * Generator version that yields events as gadgets complete.
5130
+ * Yields events in real-time as gadgets complete.
5370
5131
  *
5371
- * Note: Gadgets are still executed in parallel for efficiency,
5132
+ * Gadgets are executed in parallel for efficiency,
5372
5133
  * but results are yielded as they become available.
5373
5134
  */
5374
5135
  async *processPendingGadgetsGenerator() {
@@ -5378,12 +5139,16 @@ var init_stream_processor = __esm({
5378
5139
  const readyToExecute = [];
5379
5140
  const readyToSkip = [];
5380
5141
  for (const [_invocationId, call] of this.gadgetsAwaitingDependencies) {
5381
- const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
5142
+ const failedDep = call.dependencies.find(
5143
+ (dep) => this.failedInvocations.has(dep) || this.priorFailedInvocations.has(dep)
5144
+ );
5382
5145
  if (failedDep) {
5383
5146
  readyToSkip.push({ call, failedDep });
5384
5147
  continue;
5385
5148
  }
5386
- const allSatisfied = call.dependencies.every((dep) => this.completedResults.has(dep));
5149
+ const allSatisfied = call.dependencies.every(
5150
+ (dep) => this.completedResults.has(dep) || this.priorCompletedInvocations.has(dep)
5151
+ );
5387
5152
  if (allSatisfied) {
5388
5153
  readyToExecute.push(call);
5389
5154
  }
@@ -5424,7 +5189,9 @@ var init_stream_processor = __esm({
5424
5189
  if (this.gadgetsAwaitingDependencies.size > 0) {
5425
5190
  const pendingIds = new Set(this.gadgetsAwaitingDependencies.keys());
5426
5191
  for (const [invocationId, call] of this.gadgetsAwaitingDependencies) {
5427
- const missingDeps = call.dependencies.filter((dep) => !this.completedResults.has(dep));
5192
+ const missingDeps = call.dependencies.filter(
5193
+ (dep) => !this.completedResults.has(dep) && !this.priorCompletedInvocations.has(dep)
5194
+ );
5428
5195
  const circularDeps = missingDeps.filter((dep) => pendingIds.has(dep));
5429
5196
  const trulyMissingDeps = missingDeps.filter((dep) => !pendingIds.has(dep));
5430
5197
  let errorMessage;
@@ -5478,10 +5245,27 @@ var init_stream_processor = __esm({
5478
5245
  */
5479
5246
  async runObserversInParallel(observers) {
5480
5247
  if (observers.length === 0) return;
5481
- const results = await Promise.allSettled(
5248
+ await Promise.allSettled(
5482
5249
  observers.map((observer) => this.safeObserve(observer))
5483
5250
  );
5484
5251
  }
5252
+ // ==========================================================================
5253
+ // Public accessors for cross-iteration dependency tracking
5254
+ // ==========================================================================
5255
+ /**
5256
+ * Get all invocation IDs that completed successfully in this iteration.
5257
+ * Used by Agent to accumulate completed IDs across iterations.
5258
+ */
5259
+ getCompletedInvocationIds() {
5260
+ return new Set(this.completedResults.keys());
5261
+ }
5262
+ /**
5263
+ * Get all invocation IDs that failed in this iteration.
5264
+ * Used by Agent to accumulate failed IDs across iterations.
5265
+ */
5266
+ getFailedInvocationIds() {
5267
+ return new Set(this.failedInvocations);
5268
+ }
5485
5269
  };
5486
5270
  }
5487
5271
  });
@@ -5544,6 +5328,9 @@ var init_agent = __esm({
5544
5328
  onSubagentEvent;
5545
5329
  // Counter for generating synthetic invocation IDs for wrapped text content
5546
5330
  syntheticInvocationCounter = 0;
5331
+ // Cross-iteration dependency tracking - allows gadgets to depend on results from prior iterations
5332
+ completedInvocationIds = /* @__PURE__ */ new Set();
5333
+ failedInvocationIds = /* @__PURE__ */ new Set();
5547
5334
  // Execution Tree - first-class model for nested subagent support
5548
5335
  tree;
5549
5336
  parentNodeId;
@@ -5853,96 +5640,22 @@ var init_agent = __esm({
5853
5640
  maxIterations: this.maxIterations
5854
5641
  });
5855
5642
  while (currentIteration < this.maxIterations) {
5856
- if (this.signal?.aborted) {
5857
- this.logger.info("Agent loop terminated by abort signal", {
5858
- iteration: currentIteration,
5859
- reason: this.signal.reason
5860
- });
5861
- await this.safeObserve(async () => {
5862
- if (this.hooks.observers?.onAbort) {
5863
- const context = {
5864
- iteration: currentIteration,
5865
- reason: this.signal?.reason,
5866
- logger: this.logger
5867
- };
5868
- await this.hooks.observers.onAbort(context);
5869
- }
5870
- });
5643
+ if (await this.checkAbortAndNotify(currentIteration)) {
5871
5644
  return;
5872
5645
  }
5873
5646
  this.logger.debug("Starting iteration", { iteration: currentIteration });
5874
5647
  try {
5875
- if (this.compactionManager) {
5876
- const compactionEvent = await this.compactionManager.checkAndCompact(
5877
- this.conversation,
5878
- currentIteration
5879
- );
5880
- if (compactionEvent) {
5881
- this.logger.info("Context compacted", {
5882
- strategy: compactionEvent.strategy,
5883
- tokensBefore: compactionEvent.tokensBefore,
5884
- tokensAfter: compactionEvent.tokensAfter
5885
- });
5886
- yield { type: "compaction", event: compactionEvent };
5887
- await this.safeObserve(async () => {
5888
- if (this.hooks.observers?.onCompaction) {
5889
- await this.hooks.observers.onCompaction({
5890
- iteration: currentIteration,
5891
- event: compactionEvent,
5892
- // biome-ignore lint/style/noNonNullAssertion: compactionManager exists if compactionEvent is truthy
5893
- stats: this.compactionManager.getStats(),
5894
- logger: this.logger
5895
- });
5896
- }
5897
- });
5898
- }
5648
+ const compactionEvent = await this.checkAndPerformCompaction(currentIteration);
5649
+ if (compactionEvent) {
5650
+ yield compactionEvent;
5899
5651
  }
5900
- let llmOptions = {
5901
- model: this.model,
5902
- messages: this.conversation.getMessages(),
5903
- temperature: this.temperature,
5904
- maxTokens: this.defaultMaxTokens,
5905
- signal: this.signal
5906
- };
5907
- await this.safeObserve(async () => {
5908
- if (this.hooks.observers?.onLLMCallStart) {
5909
- const context = {
5910
- iteration: currentIteration,
5911
- options: llmOptions,
5912
- logger: this.logger
5913
- };
5914
- await this.hooks.observers.onLLMCallStart(context);
5915
- }
5916
- });
5917
- if (this.hooks.controllers?.beforeLLMCall) {
5918
- const context = {
5919
- iteration: currentIteration,
5920
- maxIterations: this.maxIterations,
5921
- options: llmOptions,
5922
- logger: this.logger
5923
- };
5924
- const action = await this.hooks.controllers.beforeLLMCall(context);
5925
- validateBeforeLLMCallAction(action);
5926
- if (action.action === "skip") {
5927
- this.logger.info("Controller skipped LLM call, using synthetic response");
5928
- this.conversation.addAssistantMessage(action.syntheticResponse);
5929
- yield { type: "text", content: action.syntheticResponse };
5930
- break;
5931
- } else if (action.action === "proceed" && action.modifiedOptions) {
5932
- llmOptions = { ...llmOptions, ...action.modifiedOptions };
5933
- }
5652
+ const prepared = await this.prepareLLMCall(currentIteration);
5653
+ const llmOptions = prepared.options;
5654
+ if (prepared.skipWithSynthetic !== void 0) {
5655
+ this.conversation.addAssistantMessage(prepared.skipWithSynthetic);
5656
+ yield { type: "text", content: prepared.skipWithSynthetic };
5657
+ break;
5934
5658
  }
5935
- await this.safeObserve(async () => {
5936
- if (this.hooks.observers?.onLLMCallReady) {
5937
- const context = {
5938
- iteration: currentIteration,
5939
- maxIterations: this.maxIterations,
5940
- options: llmOptions,
5941
- logger: this.logger
5942
- };
5943
- await this.hooks.observers.onLLMCallReady(context);
5944
- }
5945
- });
5946
5659
  this.logger.info("Calling LLM", { model: this.model });
5947
5660
  this.logger.silly("LLM request details", {
5948
5661
  model: llmOptions.model,
@@ -5978,7 +5691,10 @@ var init_agent = __esm({
5978
5691
  tree: this.tree,
5979
5692
  parentNodeId: currentLLMNodeId,
5980
5693
  // Gadgets are children of this LLM call
5981
- baseDepth: this.baseDepth
5694
+ baseDepth: this.baseDepth,
5695
+ // Cross-iteration dependency tracking
5696
+ priorCompletedInvocations: this.completedInvocationIds,
5697
+ priorFailedInvocations: this.failedInvocationIds
5982
5698
  });
5983
5699
  let streamMetadata = null;
5984
5700
  let gadgetCallCount = 0;
@@ -6001,6 +5717,12 @@ var init_agent = __esm({
6001
5717
  if (!streamMetadata) {
6002
5718
  throw new Error("Stream processing completed without metadata event");
6003
5719
  }
5720
+ for (const id of processor.getCompletedInvocationIds()) {
5721
+ this.completedInvocationIds.add(id);
5722
+ }
5723
+ for (const id of processor.getFailedInvocationIds()) {
5724
+ this.failedInvocationIds.add(id);
5725
+ }
6004
5726
  const result = streamMetadata;
6005
5727
  this.logger.info("LLM response completed", {
6006
5728
  finishReason: result.finishReason,
@@ -6024,81 +5746,21 @@ var init_agent = __esm({
6024
5746
  await this.hooks.observers.onLLMCallComplete(context);
6025
5747
  }
6026
5748
  });
6027
- this.tree.completeLLMCall(currentLLMNodeId, {
6028
- response: result.rawResponse,
6029
- usage: result.usage,
6030
- finishReason: result.finishReason
6031
- });
6032
- let finalMessage = result.finalMessage;
6033
- if (this.hooks.controllers?.afterLLMCall) {
6034
- const context = {
6035
- iteration: currentIteration,
6036
- maxIterations: this.maxIterations,
6037
- options: llmOptions,
6038
- finishReason: result.finishReason,
6039
- usage: result.usage,
6040
- finalMessage: result.finalMessage,
6041
- gadgetCallCount,
6042
- logger: this.logger
6043
- };
6044
- const action = await this.hooks.controllers.afterLLMCall(context);
6045
- validateAfterLLMCallAction(action);
6046
- if (action.action === "modify_and_continue" || action.action === "append_and_modify") {
6047
- finalMessage = action.modifiedMessage;
6048
- }
6049
- if (action.action === "append_messages" || action.action === "append_and_modify") {
6050
- for (const msg of action.messages) {
6051
- if (msg.role === "user") {
6052
- this.conversation.addUserMessage(msg.content);
6053
- } else if (msg.role === "assistant") {
6054
- this.conversation.addAssistantMessage(extractMessageText(msg.content));
6055
- } else if (msg.role === "system") {
6056
- this.conversation.addUserMessage(`[System] ${extractMessageText(msg.content)}`);
6057
- }
6058
- }
6059
- }
6060
- }
6061
- if (result.didExecuteGadgets) {
6062
- if (this.textWithGadgetsHandler) {
6063
- const textContent = textOutputs.join("");
6064
- if (textContent.trim()) {
6065
- const { gadgetName, parameterMapping, resultMapping } = this.textWithGadgetsHandler;
6066
- const syntheticId = `gc_text_${++this.syntheticInvocationCounter}`;
6067
- this.conversation.addGadgetCallResult(
6068
- gadgetName,
6069
- parameterMapping(textContent),
6070
- resultMapping ? resultMapping(textContent) : textContent,
6071
- syntheticId
6072
- );
6073
- }
6074
- }
6075
- for (const output of gadgetResults) {
6076
- if (output.type === "gadget_result") {
6077
- const gadgetResult = output.result;
6078
- this.conversation.addGadgetCallResult(
6079
- gadgetResult.gadgetName,
6080
- gadgetResult.parameters,
6081
- gadgetResult.error ?? gadgetResult.result ?? "",
6082
- gadgetResult.invocationId,
6083
- gadgetResult.media,
6084
- gadgetResult.mediaIds
6085
- );
6086
- }
6087
- }
6088
- } else {
6089
- if (finalMessage.trim()) {
6090
- const syntheticId = `gc_tell_${++this.syntheticInvocationCounter}`;
6091
- this.conversation.addGadgetCallResult(
6092
- "TellUser",
6093
- { message: finalMessage, done: false, type: "info" },
6094
- `\u2139\uFE0F ${finalMessage}`,
6095
- syntheticId
6096
- );
6097
- }
6098
- const shouldBreak = await this.handleTextOnlyResponse(finalMessage);
6099
- if (shouldBreak) {
6100
- break;
6101
- }
5749
+ this.completeLLMCallInTree(currentLLMNodeId, result);
5750
+ const finalMessage = await this.processAfterLLMCallController(
5751
+ currentIteration,
5752
+ llmOptions,
5753
+ result,
5754
+ gadgetCallCount
5755
+ );
5756
+ const shouldBreakFromTextOnly = await this.updateConversationWithResults(
5757
+ result.didExecuteGadgets,
5758
+ textOutputs,
5759
+ gadgetResults,
5760
+ finalMessage
5761
+ );
5762
+ if (shouldBreakFromTextOnly) {
5763
+ break;
6102
5764
  }
6103
5765
  if (result.shouldBreakLoop) {
6104
5766
  this.logger.info("Loop terminated by gadget or processor");
@@ -6251,6 +5913,210 @@ var init_agent = __esm({
6251
5913
  }
6252
5914
  };
6253
5915
  }
5916
+ // ==========================================================================
5917
+ // Agent Loop Helper Methods (extracted from run() for readability)
5918
+ // ==========================================================================
5919
+ /**
5920
+ * Check abort signal and notify observers if aborted.
5921
+ * @returns true if agent should terminate
5922
+ */
5923
+ async checkAbortAndNotify(iteration) {
5924
+ if (!this.signal?.aborted) return false;
5925
+ this.logger.info("Agent loop terminated by abort signal", {
5926
+ iteration,
5927
+ reason: this.signal.reason
5928
+ });
5929
+ await this.safeObserve(async () => {
5930
+ if (this.hooks.observers?.onAbort) {
5931
+ const context = {
5932
+ iteration,
5933
+ reason: this.signal?.reason,
5934
+ logger: this.logger
5935
+ };
5936
+ await this.hooks.observers.onAbort(context);
5937
+ }
5938
+ });
5939
+ return true;
5940
+ }
5941
+ /**
5942
+ * Check and perform context compaction if needed.
5943
+ * @returns compaction stream event if compaction occurred, null otherwise
5944
+ */
5945
+ async checkAndPerformCompaction(iteration) {
5946
+ if (!this.compactionManager) return null;
5947
+ const compactionEvent = await this.compactionManager.checkAndCompact(
5948
+ this.conversation,
5949
+ iteration
5950
+ );
5951
+ if (!compactionEvent) return null;
5952
+ this.logger.info("Context compacted", {
5953
+ strategy: compactionEvent.strategy,
5954
+ tokensBefore: compactionEvent.tokensBefore,
5955
+ tokensAfter: compactionEvent.tokensAfter
5956
+ });
5957
+ await this.safeObserve(async () => {
5958
+ if (this.hooks.observers?.onCompaction) {
5959
+ await this.hooks.observers.onCompaction({
5960
+ iteration,
5961
+ event: compactionEvent,
5962
+ // biome-ignore lint/style/noNonNullAssertion: compactionManager exists if compactionEvent is truthy
5963
+ stats: this.compactionManager.getStats(),
5964
+ logger: this.logger
5965
+ });
5966
+ }
5967
+ });
5968
+ return { type: "compaction", event: compactionEvent };
5969
+ }
5970
+ /**
5971
+ * Prepare LLM call options and process beforeLLMCall controller.
5972
+ * @returns options and optional skipWithSynthetic response if controller wants to skip
5973
+ */
5974
+ async prepareLLMCall(iteration) {
5975
+ let llmOptions = {
5976
+ model: this.model,
5977
+ messages: this.conversation.getMessages(),
5978
+ temperature: this.temperature,
5979
+ maxTokens: this.defaultMaxTokens,
5980
+ signal: this.signal
5981
+ };
5982
+ await this.safeObserve(async () => {
5983
+ if (this.hooks.observers?.onLLMCallStart) {
5984
+ const context = {
5985
+ iteration,
5986
+ options: llmOptions,
5987
+ logger: this.logger
5988
+ };
5989
+ await this.hooks.observers.onLLMCallStart(context);
5990
+ }
5991
+ });
5992
+ if (this.hooks.controllers?.beforeLLMCall) {
5993
+ const context = {
5994
+ iteration,
5995
+ maxIterations: this.maxIterations,
5996
+ options: llmOptions,
5997
+ logger: this.logger
5998
+ };
5999
+ const action = await this.hooks.controllers.beforeLLMCall(context);
6000
+ validateBeforeLLMCallAction(action);
6001
+ if (action.action === "skip") {
6002
+ this.logger.info("Controller skipped LLM call, using synthetic response");
6003
+ return { options: llmOptions, skipWithSynthetic: action.syntheticResponse };
6004
+ } else if (action.action === "proceed" && action.modifiedOptions) {
6005
+ llmOptions = { ...llmOptions, ...action.modifiedOptions };
6006
+ }
6007
+ }
6008
+ await this.safeObserve(async () => {
6009
+ if (this.hooks.observers?.onLLMCallReady) {
6010
+ const context = {
6011
+ iteration,
6012
+ maxIterations: this.maxIterations,
6013
+ options: llmOptions,
6014
+ logger: this.logger
6015
+ };
6016
+ await this.hooks.observers.onLLMCallReady(context);
6017
+ }
6018
+ });
6019
+ return { options: llmOptions };
6020
+ }
6021
+ /**
6022
+ * Calculate cost and complete LLM call in execution tree.
6023
+ */
6024
+ completeLLMCallInTree(nodeId, result) {
6025
+ const llmCost = this.client.modelRegistry?.estimateCost?.(
6026
+ this.model,
6027
+ result.usage?.inputTokens ?? 0,
6028
+ result.usage?.outputTokens ?? 0,
6029
+ result.usage?.cachedInputTokens ?? 0,
6030
+ result.usage?.cacheCreationInputTokens ?? 0
6031
+ )?.totalCost;
6032
+ this.tree.completeLLMCall(nodeId, {
6033
+ response: result.rawResponse,
6034
+ usage: result.usage,
6035
+ finishReason: result.finishReason,
6036
+ cost: llmCost
6037
+ });
6038
+ }
6039
+ /**
6040
+ * Process afterLLMCall controller and return modified final message.
6041
+ */
6042
+ async processAfterLLMCallController(iteration, llmOptions, result, gadgetCallCount) {
6043
+ let finalMessage = result.finalMessage;
6044
+ if (!this.hooks.controllers?.afterLLMCall) {
6045
+ return finalMessage;
6046
+ }
6047
+ const context = {
6048
+ iteration,
6049
+ maxIterations: this.maxIterations,
6050
+ options: llmOptions,
6051
+ finishReason: result.finishReason,
6052
+ usage: result.usage,
6053
+ finalMessage: result.finalMessage,
6054
+ gadgetCallCount,
6055
+ logger: this.logger
6056
+ };
6057
+ const action = await this.hooks.controllers.afterLLMCall(context);
6058
+ validateAfterLLMCallAction(action);
6059
+ if (action.action === "modify_and_continue" || action.action === "append_and_modify") {
6060
+ finalMessage = action.modifiedMessage;
6061
+ }
6062
+ if (action.action === "append_messages" || action.action === "append_and_modify") {
6063
+ for (const msg of action.messages) {
6064
+ if (msg.role === "user") {
6065
+ this.conversation.addUserMessage(msg.content);
6066
+ } else if (msg.role === "assistant") {
6067
+ this.conversation.addAssistantMessage(extractMessageText(msg.content));
6068
+ } else if (msg.role === "system") {
6069
+ this.conversation.addUserMessage(`[System] ${extractMessageText(msg.content)}`);
6070
+ }
6071
+ }
6072
+ }
6073
+ return finalMessage;
6074
+ }
6075
+ /**
6076
+ * Update conversation history with gadget results or text-only response.
6077
+ * @returns true if loop should break (text-only handler requested termination)
6078
+ */
6079
+ async updateConversationWithResults(didExecuteGadgets, textOutputs, gadgetResults, finalMessage) {
6080
+ if (didExecuteGadgets) {
6081
+ if (this.textWithGadgetsHandler) {
6082
+ const textContent = textOutputs.join("");
6083
+ if (textContent.trim()) {
6084
+ const { gadgetName, parameterMapping, resultMapping } = this.textWithGadgetsHandler;
6085
+ const syntheticId = `gc_text_${++this.syntheticInvocationCounter}`;
6086
+ this.conversation.addGadgetCallResult(
6087
+ gadgetName,
6088
+ parameterMapping(textContent),
6089
+ resultMapping ? resultMapping(textContent) : textContent,
6090
+ syntheticId
6091
+ );
6092
+ }
6093
+ }
6094
+ for (const output of gadgetResults) {
6095
+ if (output.type === "gadget_result") {
6096
+ const gadgetResult = output.result;
6097
+ this.conversation.addGadgetCallResult(
6098
+ gadgetResult.gadgetName,
6099
+ gadgetResult.parameters,
6100
+ gadgetResult.error ?? gadgetResult.result ?? "",
6101
+ gadgetResult.invocationId,
6102
+ gadgetResult.media,
6103
+ gadgetResult.mediaIds
6104
+ );
6105
+ }
6106
+ }
6107
+ return false;
6108
+ }
6109
+ if (finalMessage.trim()) {
6110
+ const syntheticId = `gc_tell_${++this.syntheticInvocationCounter}`;
6111
+ this.conversation.addGadgetCallResult(
6112
+ "TellUser",
6113
+ { message: finalMessage, done: false, type: "info" },
6114
+ `\u2139\uFE0F ${finalMessage}`,
6115
+ syntheticId
6116
+ );
6117
+ }
6118
+ return await this.handleTextOnlyResponse(finalMessage);
6119
+ }
6254
6120
  /**
6255
6121
  * Run agent with named event handlers (syntactic sugar).
6256
6122
  *
@@ -9783,11 +9649,13 @@ var init_model_registry = __esm({
9783
9649
  }
9784
9650
  /**
9785
9651
  * Get model specification by model ID
9786
- * @param modelId - Full model identifier (e.g., 'gpt-5', 'claude-sonnet-4-5-20250929')
9652
+ * @param modelId - Full model identifier, optionally with provider prefix
9653
+ * (e.g., 'gpt-5', 'claude-sonnet-4-5-20250929', 'anthropic:claude-sonnet-4-5')
9787
9654
  * @returns ModelSpec if found, undefined otherwise
9788
9655
  */
9789
9656
  getModelSpec(modelId) {
9790
- return this.modelSpecs.find((model) => model.modelId === modelId);
9657
+ const normalizedId = modelId.includes(":") ? modelId.split(":")[1] : modelId;
9658
+ return this.modelSpecs.find((model) => model.modelId === normalizedId);
9791
9659
  }
9792
9660
  /**
9793
9661
  * List all models, optionally filtered by provider