llmist 16.0.2 → 16.0.4

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
@@ -658,11 +658,11 @@ var init_execution_tree = __esm({
658
658
  yield this.eventQueue.shift();
659
659
  }
660
660
  if (this.isCompleted) break;
661
- const event = await new Promise((resolve) => {
661
+ const event = await new Promise((resolve2) => {
662
662
  if (this.eventQueue.length > 0) {
663
- resolve(this.eventQueue.shift());
663
+ resolve2(this.eventQueue.shift());
664
664
  } else {
665
- this.eventWaiters.push(resolve);
665
+ this.eventWaiters.push(resolve2);
666
666
  }
667
667
  });
668
668
  yield event;
@@ -3978,6 +3978,23 @@ var init_hook_validators = __esm({
3978
3978
  }
3979
3979
  });
3980
3980
 
3981
+ // src/agent/safe-observe.ts
3982
+ async function safeObserve(fn, logger2, label) {
3983
+ try {
3984
+ await fn();
3985
+ } catch (error) {
3986
+ const message = label ? `Observer error in ${label}:` : "Observer threw error (ignoring)";
3987
+ logger2.error(message, {
3988
+ error: error instanceof Error ? error.message : String(error)
3989
+ });
3990
+ }
3991
+ }
3992
+ var init_safe_observe = __esm({
3993
+ "src/agent/safe-observe.ts"() {
3994
+ "use strict";
3995
+ }
3996
+ });
3997
+
3981
3998
  // src/gadgets/registry.ts
3982
3999
  var GadgetRegistry;
3983
4000
  var init_registry = __esm({
@@ -4084,6 +4101,62 @@ var init_registry = __esm({
4084
4101
  });
4085
4102
 
4086
4103
  // src/agent/file-logging.ts
4104
+ function createFileLoggingState() {
4105
+ return {
4106
+ counters: /* @__PURE__ */ new Map(),
4107
+ subagentDirectories: /* @__PURE__ */ new Map(),
4108
+ activeDirectoryByContext: /* @__PURE__ */ new Map()
4109
+ };
4110
+ }
4111
+ function getDefaultState() {
4112
+ if (!defaultState) {
4113
+ defaultState = createFileLoggingState();
4114
+ }
4115
+ return defaultState;
4116
+ }
4117
+ function getNextCounter(state, directory) {
4118
+ const current = state.counters.get(directory) ?? 0;
4119
+ const next = current + 1;
4120
+ state.counters.set(directory, next);
4121
+ return next;
4122
+ }
4123
+ function getContextKey(subagentContext) {
4124
+ if (!subagentContext) return "root:0";
4125
+ return `${subagentContext.parentGadgetInvocationId}:${subagentContext.depth}`;
4126
+ }
4127
+ function resetFileLoggingState() {
4128
+ defaultState = void 0;
4129
+ }
4130
+ function findParentContextKey(state, currentDepth) {
4131
+ const parentDepth = currentDepth - 1;
4132
+ if (parentDepth === 0) return "root:0";
4133
+ for (const [key, _path] of state.activeDirectoryByContext) {
4134
+ if (key.endsWith(`:${parentDepth}`)) {
4135
+ return key;
4136
+ }
4137
+ }
4138
+ return "root:0";
4139
+ }
4140
+ function resolveLoggingDirectory(state, baseDirectory, counterPadding, subagentContext) {
4141
+ const contextKey = getContextKey(subagentContext);
4142
+ if (!subagentContext) {
4143
+ state.activeDirectoryByContext.set(contextKey, baseDirectory);
4144
+ return baseDirectory;
4145
+ }
4146
+ const { parentGadgetInvocationId, depth } = subagentContext;
4147
+ const parentContextKey = findParentContextKey(state, depth);
4148
+ const parentDir = state.activeDirectoryByContext.get(parentContextKey) ?? baseDirectory;
4149
+ const subagentKey = `${parentDir}:${parentGadgetInvocationId}`;
4150
+ let fullPath = state.subagentDirectories.get(subagentKey);
4151
+ if (!fullPath) {
4152
+ const chronoNumber = getNextCounter(state, parentDir);
4153
+ const subdirName = `${formatCallNumber(chronoNumber, counterPadding)}-${parentGadgetInvocationId}`;
4154
+ fullPath = (0, import_node_path3.join)(parentDir, subdirName);
4155
+ state.subagentDirectories.set(subagentKey, fullPath);
4156
+ }
4157
+ state.activeDirectoryByContext.set(contextKey, fullPath);
4158
+ return fullPath;
4159
+ }
4087
4160
  function formatLlmRequest(messages) {
4088
4161
  const lines = [];
4089
4162
  for (const msg of messages) {
@@ -4100,16 +4173,20 @@ async function writeLogFile(dir, filename, content) {
4100
4173
  await (0, import_promises2.mkdir)(dir, { recursive: true });
4101
4174
  await (0, import_promises2.writeFile)((0, import_node_path3.join)(dir, filename), content, "utf-8");
4102
4175
  }
4103
- function createFileLoggingHooks(options) {
4176
+ function createFileLoggingHooks(options, state = getDefaultState()) {
4104
4177
  const {
4105
- directory,
4106
4178
  startingCounter = 1,
4107
4179
  counterPadding = 4,
4108
4180
  skipSubagents = true,
4109
4181
  formatRequest = formatLlmRequest,
4110
4182
  onFileWritten
4111
4183
  } = options;
4112
- let callCounter = startingCounter - 1;
4184
+ const baseDirectory = (0, import_node_path3.resolve)(options.directory);
4185
+ if (!state.counters.has(baseDirectory)) {
4186
+ state.counters.set(baseDirectory, startingCounter - 1);
4187
+ }
4188
+ let currentCallNumber = 0;
4189
+ let currentDirectory = baseDirectory;
4113
4190
  return {
4114
4191
  observers: {
4115
4192
  /**
@@ -4119,17 +4196,25 @@ function createFileLoggingHooks(options) {
4119
4196
  if (skipSubagents && context.subagentContext) {
4120
4197
  return;
4121
4198
  }
4122
- callCounter++;
4123
- const filename = `${formatCallNumber(callCounter, counterPadding)}.request`;
4199
+ currentDirectory = resolveLoggingDirectory(
4200
+ state,
4201
+ baseDirectory,
4202
+ counterPadding,
4203
+ context.subagentContext
4204
+ );
4205
+ currentCallNumber = getNextCounter(state, currentDirectory);
4206
+ const filename = `${formatCallNumber(currentCallNumber, counterPadding)}.request`;
4124
4207
  const content = formatRequest(context.options.messages);
4125
4208
  try {
4126
- await writeLogFile(directory, filename, content);
4209
+ await writeLogFile(currentDirectory, filename, content);
4127
4210
  if (onFileWritten) {
4128
4211
  onFileWritten({
4129
- filePath: (0, import_node_path3.join)(directory, filename),
4212
+ filePath: (0, import_node_path3.join)(currentDirectory, filename),
4130
4213
  type: "request",
4131
- callNumber: callCounter,
4132
- contentLength: content.length
4214
+ callNumber: currentCallNumber,
4215
+ contentLength: content.length,
4216
+ parentGadgetInvocationId: context.subagentContext?.parentGadgetInvocationId,
4217
+ depth: context.subagentContext?.depth
4133
4218
  });
4134
4219
  }
4135
4220
  } catch (error) {
@@ -4143,16 +4228,22 @@ function createFileLoggingHooks(options) {
4143
4228
  if (skipSubagents && context.subagentContext) {
4144
4229
  return;
4145
4230
  }
4146
- const filename = `${formatCallNumber(callCounter, counterPadding)}.response`;
4231
+ if (currentCallNumber === 0) {
4232
+ console.warn("[file-logging] Skipping response write: no matching request recorded");
4233
+ return;
4234
+ }
4235
+ const filename = `${formatCallNumber(currentCallNumber, counterPadding)}.response`;
4147
4236
  const content = context.rawResponse;
4148
4237
  try {
4149
- await writeLogFile(directory, filename, content);
4238
+ await writeLogFile(currentDirectory, filename, content);
4150
4239
  if (onFileWritten) {
4151
4240
  onFileWritten({
4152
- filePath: (0, import_node_path3.join)(directory, filename),
4241
+ filePath: (0, import_node_path3.join)(currentDirectory, filename),
4153
4242
  type: "response",
4154
- callNumber: callCounter,
4155
- contentLength: content.length
4243
+ callNumber: currentCallNumber,
4244
+ contentLength: content.length,
4245
+ parentGadgetInvocationId: context.subagentContext?.parentGadgetInvocationId,
4246
+ depth: context.subagentContext?.depth
4156
4247
  });
4157
4248
  }
4158
4249
  } catch (error) {
@@ -4169,7 +4260,7 @@ function getEnvFileLoggingHooks() {
4169
4260
  }
4170
4261
  return createFileLoggingHooks({ directory });
4171
4262
  }
4172
- var import_promises2, import_node_path3, ENV_LOG_RAW_DIRECTORY;
4263
+ var import_promises2, import_node_path3, defaultState, ENV_LOG_RAW_DIRECTORY;
4173
4264
  var init_file_logging = __esm({
4174
4265
  "src/agent/file-logging.ts"() {
4175
4266
  "use strict";
@@ -4358,6 +4449,8 @@ var init_hook_presets = __esm({
4358
4449
  /**
4359
4450
  * Tracks cumulative token usage across all LLM calls.
4360
4451
  *
4452
+ * @public
4453
+ *
4361
4454
  * **Output:**
4362
4455
  * - Per-call token count with 📊 emoji
4363
4456
  * - Cumulative total across all calls
@@ -4620,6 +4713,8 @@ var init_hook_presets = __esm({
4620
4713
  /**
4621
4714
  * Logs detailed error information for debugging and troubleshooting.
4622
4715
  *
4716
+ * @public
4717
+ *
4623
4718
  * **Output:**
4624
4719
  * - LLM errors with ❌ emoji, including model and recovery status
4625
4720
  * - Gadget errors with full context (parameters, error message)
@@ -4698,6 +4793,8 @@ var init_hook_presets = __esm({
4698
4793
  /**
4699
4794
  * Tracks context compaction events.
4700
4795
  *
4796
+ * @public
4797
+ *
4701
4798
  * **Output:**
4702
4799
  * - Compaction events with 🗜️ emoji
4703
4800
  * - Strategy name, tokens before/after, and savings
@@ -4811,6 +4908,8 @@ var init_hook_presets = __esm({
4811
4908
  /**
4812
4909
  * Returns empty hook configuration for clean output without any logging.
4813
4910
  *
4911
+ * @public
4912
+ *
4814
4913
  * **Output:**
4815
4914
  * - None. Returns {} (empty object).
4816
4915
  *
@@ -4968,6 +5067,8 @@ var init_hook_presets = __esm({
4968
5067
  /**
4969
5068
  * Composite preset combining logging, timing, tokenTracking, and errorLogging.
4970
5069
  *
5070
+ * @public
5071
+ *
4971
5072
  * This is the recommended preset for development and initial production deployments,
4972
5073
  * providing comprehensive observability with a single method call.
4973
5074
  *
@@ -11918,7 +12019,9 @@ ${endPrefix}`
11918
12019
  */
11919
12020
  /**
11920
12021
  * Build AgentOptions with the given user prompt.
11921
- * Centralizes options construction for ask(), askWithImage(), and askWithContent().
12022
+ * Centralizes options construction for ask(), askWithImage(), askWithContent(), and build().
12023
+ *
12024
+ * @param userPrompt - Optional user prompt (omitted for build() which has no prompt)
11922
12025
  */
11923
12026
  buildAgentOptions(userPrompt) {
11924
12027
  if (!this.client) {
@@ -11960,7 +12063,12 @@ ${endPrefix}`
11960
12063
  // Tree context for shared tree model (subagents share parent's tree)
11961
12064
  parentTree: this.parentContext?.tree,
11962
12065
  parentNodeId: this.parentContext?.nodeId,
11963
- baseDepth: this.parentContext ? (this.parentContext.depth ?? 0) + 1 : 0
12066
+ baseDepth: this.parentContext ? (this.parentContext.depth ?? 0) + 1 : 0,
12067
+ // Parent observer hooks for subagent visibility
12068
+ parentObservers: this.parentObservers,
12069
+ // Shared rate limit tracker and retry config (for coordinated limits across subagents)
12070
+ sharedRateLimitTracker: this.sharedRateLimitTracker,
12071
+ sharedRetryConfig: this.sharedRetryConfig
11964
12072
  };
11965
12073
  }
11966
12074
  ask(userPrompt) {
@@ -12112,52 +12220,7 @@ ${endPrefix}`
12112
12220
  * ```
12113
12221
  */
12114
12222
  build() {
12115
- if (!this.client) {
12116
- const { LLMist: LLMistClass } = (init_client(), __toCommonJS(client_exports));
12117
- this.client = new LLMistClass();
12118
- }
12119
- const registry = GadgetRegistry.from(this.gadgets);
12120
- const options = {
12121
- client: this.client,
12122
- model: this.model ?? "openai:gpt-5-nano",
12123
- systemPrompt: this.systemPrompt,
12124
- // No userPrompt - agent.run() will throw if called directly
12125
- registry,
12126
- maxIterations: this.maxIterations,
12127
- budget: this.budget,
12128
- temperature: this.temperature,
12129
- logger: this.logger,
12130
- hooks: this.composeHooks(),
12131
- promptConfig: this.promptConfig,
12132
- initialMessages: this.initialMessages,
12133
- requestHumanInput: this.requestHumanInput,
12134
- gadgetStartPrefix: this.gadgetStartPrefix,
12135
- gadgetEndPrefix: this.gadgetEndPrefix,
12136
- gadgetArgPrefix: this.gadgetArgPrefix,
12137
- textOnlyHandler: this.textOnlyHandler,
12138
- textWithGadgetsHandler: this.textWithGadgetsHandler,
12139
- defaultGadgetTimeoutMs: this.defaultGadgetTimeoutMs,
12140
- gadgetExecutionMode: this.gadgetExecutionMode,
12141
- maxGadgetsPerResponse: this.maxGadgetsPerResponse,
12142
- gadgetOutputLimit: this.gadgetOutputLimit,
12143
- gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
12144
- compactionConfig: this.compactionConfig,
12145
- retryConfig: this.retryConfig,
12146
- rateLimitConfig: this.rateLimitConfig,
12147
- signal: this.signal,
12148
- reasoning: this.reasoningConfig,
12149
- caching: this.cachingConfig,
12150
- subagentConfig: this.subagentConfig,
12151
- // Tree context for shared tree model (subagents share parent's tree)
12152
- parentTree: this.parentContext?.tree,
12153
- parentNodeId: this.parentContext?.nodeId,
12154
- baseDepth: this.parentContext ? (this.parentContext.depth ?? 0) + 1 : 0,
12155
- // Parent observer hooks for subagent visibility
12156
- parentObservers: this.parentObservers,
12157
- // Shared rate limit tracker and retry config (for coordinated limits across subagents)
12158
- sharedRateLimitTracker: this.sharedRateLimitTracker,
12159
- sharedRetryConfig: this.sharedRetryConfig
12160
- };
12223
+ const options = this.buildAgentOptions();
12161
12224
  return new Agent(AGENT_INTERNAL_KEY, options);
12162
12225
  }
12163
12226
  };
@@ -13445,137 +13508,549 @@ var init_executor = __esm({
13445
13508
  }
13446
13509
  });
13447
13510
 
13448
- // src/agent/tree-hook-bridge.ts
13449
- function findParentGadgetInvocationId(tree, nodeId) {
13450
- let currentId = nodeId;
13451
- while (currentId) {
13452
- const node = tree.getNode(currentId);
13453
- if (!node) break;
13454
- currentId = node.parentId;
13455
- if (!currentId) break;
13456
- const parentNode = tree.getNode(currentId);
13457
- if (parentNode?.type === "gadget") {
13458
- return parentNode.invocationId;
13459
- }
13460
- }
13461
- return void 0;
13462
- }
13463
- function getIterationFromTree(tree, nodeId) {
13464
- let currentId = nodeId;
13465
- while (currentId) {
13466
- const node = tree.getNode(currentId);
13467
- if (!node) break;
13468
- if (node.type === "llm_call") {
13469
- return node.iteration;
13470
- }
13471
- currentId = node.parentId;
13472
- }
13473
- return 0;
13474
- }
13475
- function buildSubagentContext(tree, event) {
13476
- const parentGadgetInvocationId = findParentGadgetInvocationId(tree, event.nodeId);
13477
- if (!parentGadgetInvocationId) {
13478
- return void 0;
13479
- }
13480
- return {
13481
- parentGadgetInvocationId,
13482
- depth: event.depth
13483
- };
13484
- }
13485
- function getSubagentContextForNode(tree, nodeId) {
13486
- const node = tree.getNode(nodeId);
13487
- if (!node) return void 0;
13488
- const parentGadgetInvocationId = findParentGadgetInvocationId(tree, nodeId);
13489
- if (!parentGadgetInvocationId) {
13490
- return void 0;
13491
- }
13492
- return {
13493
- parentGadgetInvocationId,
13494
- depth: node.depth
13495
- };
13496
- }
13497
- async function safeObserve(fn, logger2, eventType) {
13498
- try {
13499
- await fn();
13500
- } catch (error) {
13501
- logger2.warn(`Observer error in ${eventType}:`, error);
13502
- }
13503
- }
13504
- function chainObserverCall(chainMap, key, fn, logger2, eventType, cleanup = false) {
13505
- const previousPromise = chainMap.get(key) ?? Promise.resolve();
13506
- const newPromise = previousPromise.then(() => safeObserve(fn, logger2, eventType));
13507
- chainMap.set(key, newPromise);
13508
- if (cleanup) {
13509
- newPromise.finally(() => chainMap.delete(key));
13510
- }
13511
- }
13512
- function bridgeTreeToHooks(tree, hooks, logger2) {
13513
- const gadgetPromiseChains = /* @__PURE__ */ new Map();
13514
- const llmPromiseChains = /* @__PURE__ */ new Map();
13515
- return tree.onAll((event) => {
13516
- const subagentContext = buildSubagentContext(tree, event);
13517
- switch (event.type) {
13518
- // =================================================================
13519
- // GADGET EVENTS - Bridged for subagent visibility
13520
- // =================================================================
13521
- // When a subagent executes gadgets, these events propagate through
13522
- // the shared tree to the parent's hooks.
13523
- // Only bridged for subagent events (depth > 0) to avoid double-calling
13524
- // root agent events which are handled directly in stream-processor.ts
13525
- case "gadget_start": {
13526
- if (subagentContext && hooks.observers?.onGadgetExecutionStart) {
13527
- const gadgetEvent = event;
13528
- const gadgetNode = tree.getNode(event.nodeId);
13529
- const context = {
13530
- iteration: getIterationFromTree(tree, event.nodeId),
13531
- gadgetName: gadgetEvent.name,
13532
- invocationId: gadgetEvent.invocationId,
13533
- parameters: gadgetNode?.parameters ?? {},
13534
- logger: logger2,
13535
- subagentContext
13536
- };
13537
- chainObserverCall(
13538
- gadgetPromiseChains,
13539
- gadgetEvent.invocationId,
13540
- () => hooks.observers?.onGadgetExecutionStart?.(context),
13541
- logger2,
13542
- "onGadgetExecutionStart",
13543
- false
13544
- // Don't cleanup - wait for completion event
13545
- );
13546
- }
13547
- break;
13511
+ // src/agent/gadget-concurrency-manager.ts
13512
+ var GadgetConcurrencyManager;
13513
+ var init_gadget_concurrency_manager = __esm({
13514
+ "src/agent/gadget-concurrency-manager.ts"() {
13515
+ "use strict";
13516
+ init_logger();
13517
+ GadgetConcurrencyManager = class {
13518
+ registry;
13519
+ subagentConfig;
13520
+ logger;
13521
+ /** Track active execution count per gadget name */
13522
+ activeCountByGadget = /* @__PURE__ */ new Map();
13523
+ /** Queue of gadgets waiting for a concurrency slot (per gadget name) */
13524
+ concurrencyQueue = /* @__PURE__ */ new Map();
13525
+ /** All active gadget promises, keyed by invocationId */
13526
+ inFlightExecutions = /* @__PURE__ */ new Map();
13527
+ /** Queue of exclusive gadgets deferred until in-flight gadgets complete */
13528
+ exclusiveQueue = [];
13529
+ constructor(options) {
13530
+ this.registry = options.registry;
13531
+ this.subagentConfig = options.subagentConfig;
13532
+ this.logger = options.logger ?? createLogger({ name: "llmist:gadget-concurrency-manager" });
13548
13533
  }
13549
- case "gadget_complete": {
13550
- if (subagentContext && hooks.observers?.onGadgetExecutionComplete) {
13551
- const gadgetEvent = event;
13552
- const gadgetNode = tree.getNode(event.nodeId);
13553
- const context = {
13554
- iteration: getIterationFromTree(tree, event.nodeId),
13555
- gadgetName: gadgetEvent.name,
13556
- invocationId: gadgetEvent.invocationId,
13557
- parameters: gadgetNode?.parameters ?? {},
13558
- finalResult: gadgetEvent.result,
13559
- executionTimeMs: gadgetEvent.executionTimeMs,
13560
- cost: gadgetEvent.cost,
13561
- logger: logger2,
13562
- subagentContext
13563
- };
13564
- chainObserverCall(
13565
- gadgetPromiseChains,
13566
- gadgetEvent.invocationId,
13567
- () => hooks.observers?.onGadgetExecutionComplete?.(context),
13568
- logger2,
13569
- "onGadgetExecutionComplete",
13570
- true
13571
- // Cleanup after completion
13572
- );
13534
+ // ==========================================================================
13535
+ // Concurrency limit resolution
13536
+ // ==========================================================================
13537
+ /**
13538
+ * Get the effective concurrency limit for a gadget.
13539
+ * Uses "most restrictive wins" strategy: the lowest non-zero value from
13540
+ * external config (SubagentConfig) and gadget's intrinsic maxConcurrent.
13541
+ *
13542
+ * This ensures gadget authors can set safety floors (e.g., maxConcurrent: 1
13543
+ * for file writers) that cannot be weakened by external configuration.
13544
+ *
13545
+ * @returns 0 if unlimited, otherwise the effective limit
13546
+ */
13547
+ getConcurrencyLimit(gadgetName) {
13548
+ const configLimit = this.subagentConfig?.[gadgetName]?.maxConcurrent;
13549
+ const gadget = this.registry.get(gadgetName);
13550
+ const gadgetLimit = gadget?.maxConcurrent;
13551
+ const config = configLimit || Number.POSITIVE_INFINITY;
13552
+ const intrinsic = gadgetLimit || Number.POSITIVE_INFINITY;
13553
+ const effective = Math.min(config, intrinsic);
13554
+ return effective === Number.POSITIVE_INFINITY ? 0 : effective;
13555
+ }
13556
+ // ==========================================================================
13557
+ // Concurrency checks
13558
+ // ==========================================================================
13559
+ /**
13560
+ * Check whether a gadget call can start immediately given current concurrency state.
13561
+ * Returns false if:
13562
+ * - The gadget is exclusive and other gadgets are in-flight
13563
+ * - The gadget has a concurrency limit and it is already reached
13564
+ *
13565
+ * Does NOT modify any state.
13566
+ */
13567
+ canStart(call) {
13568
+ const gadget = this.registry.get(call.gadgetName);
13569
+ if (gadget?.exclusive && this.inFlightExecutions.size > 0) {
13570
+ return false;
13573
13571
  }
13574
- break;
13572
+ const limit = this.getConcurrencyLimit(call.gadgetName);
13573
+ if (limit > 0) {
13574
+ const activeCount = this.activeCountByGadget.get(call.gadgetName) ?? 0;
13575
+ if (activeCount >= limit) {
13576
+ return false;
13577
+ }
13578
+ }
13579
+ return true;
13575
13580
  }
13576
- case "gadget_error": {
13577
- if (subagentContext && hooks.observers?.onGadgetExecutionComplete) {
13578
- const gadgetEvent = event;
13581
+ /**
13582
+ * Check whether a gadget is marked as exclusive.
13583
+ */
13584
+ isExclusive(gadgetName) {
13585
+ const gadget = this.registry.get(gadgetName);
13586
+ return gadget?.exclusive === true;
13587
+ }
13588
+ /**
13589
+ * Get the current count of in-flight (actively executing) gadgets.
13590
+ */
13591
+ get inFlightCount() {
13592
+ return this.inFlightExecutions.size;
13593
+ }
13594
+ /**
13595
+ * Get the total count of actively executing gadgets across all gadget types.
13596
+ * Used to know when all work is truly complete.
13597
+ */
13598
+ getTotalActiveGadgetCount() {
13599
+ let total = 0;
13600
+ for (const count of this.activeCountByGadget.values()) {
13601
+ total += count;
13602
+ }
13603
+ return total;
13604
+ }
13605
+ /**
13606
+ * Check if there are any gadgets waiting in concurrency queues.
13607
+ */
13608
+ hasQueuedGadgets() {
13609
+ for (const queue of this.concurrencyQueue.values()) {
13610
+ if (queue.length > 0) return true;
13611
+ }
13612
+ return false;
13613
+ }
13614
+ /**
13615
+ * Get total count of queued gadgets across all queues.
13616
+ */
13617
+ getQueuedGadgetCount() {
13618
+ let count = 0;
13619
+ for (const queue of this.concurrencyQueue.values()) {
13620
+ count += queue.length;
13621
+ }
13622
+ return count;
13623
+ }
13624
+ /**
13625
+ * Check if there are exclusive gadgets waiting.
13626
+ */
13627
+ get hasExclusiveQueued() {
13628
+ return this.exclusiveQueue.length > 0;
13629
+ }
13630
+ /**
13631
+ * Drain and return all exclusive gadgets from the queue.
13632
+ * The caller is responsible for executing them.
13633
+ */
13634
+ drainExclusiveQueue() {
13635
+ const queue = this.exclusiveQueue;
13636
+ this.exclusiveQueue = [];
13637
+ return queue;
13638
+ }
13639
+ // ==========================================================================
13640
+ // State mutation
13641
+ // ==========================================================================
13642
+ /**
13643
+ * Track an in-flight gadget execution.
13644
+ * Increments the active count for the gadget name and registers the promise.
13645
+ *
13646
+ * @param invocationId - Unique ID for this execution
13647
+ * @param gadgetName - Name of the gadget being executed
13648
+ * @param promise - The execution promise to track
13649
+ */
13650
+ trackExecution(invocationId, gadgetName, promise) {
13651
+ const currentCount = this.activeCountByGadget.get(gadgetName) ?? 0;
13652
+ this.activeCountByGadget.set(gadgetName, currentCount + 1);
13653
+ this.inFlightExecutions.set(invocationId, promise);
13654
+ }
13655
+ /**
13656
+ * Called when a gadget execution completes.
13657
+ * Decrements active count and triggers queue processing if a slot opened up.
13658
+ *
13659
+ * @param gadgetName - Name of the gadget that completed
13660
+ * @returns The next queued gadget call for this gadget, if one was promoted, otherwise null
13661
+ */
13662
+ onComplete(gadgetName) {
13663
+ const newCount = (this.activeCountByGadget.get(gadgetName) ?? 1) - 1;
13664
+ this.activeCountByGadget.set(gadgetName, newCount);
13665
+ return this.promoteFromQueue(gadgetName);
13666
+ }
13667
+ /**
13668
+ * Queue a gadget for later execution due to a concurrency limit being reached.
13669
+ *
13670
+ * @param call - The gadget call to defer
13671
+ */
13672
+ queueForLater(call) {
13673
+ this.logger.debug("Gadget queued due to concurrency limit", {
13674
+ gadgetName: call.gadgetName,
13675
+ invocationId: call.invocationId,
13676
+ activeCount: this.activeCountByGadget.get(call.gadgetName) ?? 0,
13677
+ limit: this.getConcurrencyLimit(call.gadgetName)
13678
+ });
13679
+ const queue = this.concurrencyQueue.get(call.gadgetName) ?? [];
13680
+ queue.push(call);
13681
+ this.concurrencyQueue.set(call.gadgetName, queue);
13682
+ }
13683
+ /**
13684
+ * Queue a gadget for exclusive execution (after all in-flight complete).
13685
+ *
13686
+ * @param call - The exclusive gadget call to defer
13687
+ */
13688
+ queueExclusive(call) {
13689
+ this.logger.debug("Deferring exclusive gadget until in-flight gadgets complete", {
13690
+ gadgetName: call.gadgetName,
13691
+ invocationId: call.invocationId,
13692
+ inFlightCount: this.inFlightExecutions.size
13693
+ });
13694
+ this.exclusiveQueue.push(call);
13695
+ }
13696
+ /**
13697
+ * Clear the inFlightExecutions map after all promises have completed.
13698
+ * Called after waitForAll resolves.
13699
+ */
13700
+ clearInFlight() {
13701
+ this.inFlightExecutions.clear();
13702
+ }
13703
+ // ==========================================================================
13704
+ // Waiting
13705
+ // ==========================================================================
13706
+ /**
13707
+ * Wait for all currently in-flight gadget executions to complete.
13708
+ * Resolves when the Promise.all of all tracked promises resolves.
13709
+ *
13710
+ * Note: new executions may be started during waiting (from the queue).
13711
+ * Callers should loop until inFlightCount === 0 AND hasQueuedGadgets() === false.
13712
+ */
13713
+ async waitForAll() {
13714
+ if (this.inFlightExecutions.size === 0) return;
13715
+ await Promise.all(this.inFlightExecutions.values());
13716
+ }
13717
+ /**
13718
+ * Get a promise that resolves when all current in-flight executions complete.
13719
+ * Returns a resolved promise if no executions are in-flight.
13720
+ */
13721
+ getAllDonePromise() {
13722
+ if (this.inFlightExecutions.size === 0) {
13723
+ return Promise.resolve("done");
13724
+ }
13725
+ return Promise.all(this.inFlightExecutions.values()).then(() => "done");
13726
+ }
13727
+ // ==========================================================================
13728
+ // Private helpers
13729
+ // ==========================================================================
13730
+ /**
13731
+ * Check the concurrency queue for a gadget name and promote the next queued
13732
+ * call if a slot is available.
13733
+ *
13734
+ * @param gadgetName - The gadget name to check
13735
+ * @returns The promoted call, or null if queue is empty or limit still reached
13736
+ */
13737
+ promoteFromQueue(gadgetName) {
13738
+ const queue = this.concurrencyQueue.get(gadgetName);
13739
+ if (!queue || queue.length === 0) return null;
13740
+ const limit = this.getConcurrencyLimit(gadgetName);
13741
+ const activeCount = this.activeCountByGadget.get(gadgetName) ?? 0;
13742
+ if (limit === 0 || activeCount < limit) {
13743
+ const nextCall = queue.shift();
13744
+ this.logger.debug("Processing queued gadget", {
13745
+ gadgetName,
13746
+ invocationId: nextCall.invocationId,
13747
+ remainingInQueue: queue.length
13748
+ });
13749
+ return nextCall;
13750
+ }
13751
+ return null;
13752
+ }
13753
+ };
13754
+ }
13755
+ });
13756
+
13757
+ // src/agent/gadget-dependency-resolver.ts
13758
+ var GadgetDependencyResolver;
13759
+ var init_gadget_dependency_resolver = __esm({
13760
+ "src/agent/gadget-dependency-resolver.ts"() {
13761
+ "use strict";
13762
+ GadgetDependencyResolver = class {
13763
+ /** Gadgets waiting for their dependencies to complete */
13764
+ gadgetsAwaitingDependencies = /* @__PURE__ */ new Map();
13765
+ /** Completed gadget results, keyed by invocation ID */
13766
+ completedResults = /* @__PURE__ */ new Map();
13767
+ /** Invocation IDs of gadgets that have failed (error or skipped due to dependency) */
13768
+ failedInvocations = /* @__PURE__ */ new Set();
13769
+ /** Invocation IDs completed in previous iterations (read-only) */
13770
+ priorCompletedInvocations;
13771
+ /** Invocation IDs that failed in previous iterations (read-only) */
13772
+ priorFailedInvocations;
13773
+ constructor(options = {}) {
13774
+ this.priorCompletedInvocations = options.priorCompletedInvocations ?? /* @__PURE__ */ new Set();
13775
+ this.priorFailedInvocations = options.priorFailedInvocations ?? /* @__PURE__ */ new Set();
13776
+ }
13777
+ // ==========================================================================
13778
+ // State mutation
13779
+ // ==========================================================================
13780
+ /**
13781
+ * Queue a gadget call that is waiting for one or more dependencies to complete.
13782
+ * Call this when a gadget's dependencies are not yet all satisfied.
13783
+ *
13784
+ * @param call - The parsed gadget call to defer
13785
+ */
13786
+ addPending(call) {
13787
+ this.gadgetsAwaitingDependencies.set(call.invocationId, call);
13788
+ }
13789
+ /**
13790
+ * Record that a gadget completed successfully.
13791
+ * This may unblock other gadgets that depend on this invocation.
13792
+ *
13793
+ * Also marks as failed if the result contains an error.
13794
+ *
13795
+ * @param result - The completed gadget execution result
13796
+ */
13797
+ markComplete(result) {
13798
+ this.completedResults.set(result.invocationId, result);
13799
+ if (result.error) {
13800
+ this.failedInvocations.add(result.invocationId);
13801
+ }
13802
+ }
13803
+ /**
13804
+ * Mark an invocation ID as failed without recording a full result.
13805
+ * Use this for gadgets that are skipped before execution (e.g., limit exceeded,
13806
+ * self-referential dependency, dependency skip).
13807
+ *
13808
+ * @param invocationId - The invocation ID to mark as failed
13809
+ */
13810
+ markFailed(invocationId) {
13811
+ this.failedInvocations.add(invocationId);
13812
+ }
13813
+ /**
13814
+ * Remove a gadget from the pending queue (called just before execution).
13815
+ *
13816
+ * @param invocationId - The invocation ID to remove from pending
13817
+ */
13818
+ removePending(invocationId) {
13819
+ this.gadgetsAwaitingDependencies.delete(invocationId);
13820
+ }
13821
+ /**
13822
+ * Clear all remaining pending gadgets (e.g., after handling unresolvable deps).
13823
+ */
13824
+ clearPending() {
13825
+ this.gadgetsAwaitingDependencies.clear();
13826
+ }
13827
+ // ==========================================================================
13828
+ // Queries
13829
+ // ==========================================================================
13830
+ /**
13831
+ * Get all gadget calls currently waiting for dependencies.
13832
+ * Returns entries as [invocationId, call] pairs.
13833
+ */
13834
+ getPendingEntries() {
13835
+ return Array.from(this.gadgetsAwaitingDependencies.entries());
13836
+ }
13837
+ /**
13838
+ * Get the number of gadgets currently waiting for dependencies.
13839
+ */
13840
+ get pendingCount() {
13841
+ return this.gadgetsAwaitingDependencies.size;
13842
+ }
13843
+ /**
13844
+ * Check whether a given invocation ID has been completed successfully
13845
+ * (either in this iteration or a prior one).
13846
+ */
13847
+ isCompleted(invocationId) {
13848
+ return this.completedResults.has(invocationId) || this.priorCompletedInvocations.has(invocationId);
13849
+ }
13850
+ /**
13851
+ * Check whether a given invocation ID has failed
13852
+ * (either in this iteration or a prior one).
13853
+ */
13854
+ isFailed(invocationId) {
13855
+ return this.failedInvocations.has(invocationId) || this.priorFailedInvocations.has(invocationId);
13856
+ }
13857
+ /**
13858
+ * Get the execution result for a completed invocation, if available.
13859
+ * Only returns results from the current iteration; prior iterations
13860
+ * are tracked by ID only.
13861
+ */
13862
+ getCompletedResult(invocationId) {
13863
+ return this.completedResults.get(invocationId);
13864
+ }
13865
+ /**
13866
+ * Check if all dependencies for a gadget call are satisfied.
13867
+ * A dependency is satisfied if it completed in this or a prior iteration.
13868
+ *
13869
+ * @param call - The gadget call whose dependencies to check
13870
+ * @returns true if all deps are satisfied, false if any are still pending
13871
+ */
13872
+ isAllSatisfied(call) {
13873
+ return call.dependencies.every((dep) => this.isCompleted(dep));
13874
+ }
13875
+ /**
13876
+ * Find the first failed dependency for a gadget call, if any.
13877
+ * A dependency is considered failed if it failed in this or a prior iteration.
13878
+ *
13879
+ * @param call - The gadget call to check
13880
+ * @returns The invocation ID of the failed dependency, or undefined if none
13881
+ */
13882
+ getFailedDependency(call) {
13883
+ return call.dependencies.find((dep) => this.isFailed(dep));
13884
+ }
13885
+ /**
13886
+ * Separate the pending gadgets into two groups:
13887
+ * - Those ready to execute (all deps satisfied)
13888
+ * - Those ready to skip (at least one dep has failed)
13889
+ *
13890
+ * Gadgets that are neither ready nor skippable remain pending.
13891
+ *
13892
+ * @returns Object with `readyToExecute` and `readyToSkip` arrays
13893
+ */
13894
+ getReadyCalls() {
13895
+ const readyToExecute = [];
13896
+ const readyToSkip = [];
13897
+ for (const [_invocationId, call] of this.gadgetsAwaitingDependencies) {
13898
+ const failedDep = this.getFailedDependency(call);
13899
+ if (failedDep) {
13900
+ readyToSkip.push({ call, failedDep });
13901
+ continue;
13902
+ }
13903
+ if (this.isAllSatisfied(call)) {
13904
+ readyToExecute.push(call);
13905
+ }
13906
+ }
13907
+ return { readyToExecute, readyToSkip };
13908
+ }
13909
+ // ==========================================================================
13910
+ // Cross-iteration accessors (for Agent to accumulate state)
13911
+ // ==========================================================================
13912
+ /**
13913
+ * Get all invocation IDs that completed successfully in this iteration.
13914
+ * Used by Agent to accumulate completed IDs across iterations.
13915
+ */
13916
+ getCompletedInvocationIds() {
13917
+ return new Set(this.completedResults.keys());
13918
+ }
13919
+ /**
13920
+ * Get all invocation IDs that failed in this iteration.
13921
+ * Used by Agent to accumulate failed IDs across iterations.
13922
+ */
13923
+ getFailedInvocationIds() {
13924
+ return new Set(this.failedInvocations);
13925
+ }
13926
+ };
13927
+ }
13928
+ });
13929
+
13930
+ // src/agent/tree-hook-bridge.ts
13931
+ function findParentGadgetInvocationId(tree, nodeId) {
13932
+ let currentId = nodeId;
13933
+ while (currentId) {
13934
+ const node = tree.getNode(currentId);
13935
+ if (!node) break;
13936
+ currentId = node.parentId;
13937
+ if (!currentId) break;
13938
+ const parentNode = tree.getNode(currentId);
13939
+ if (parentNode?.type === "gadget") {
13940
+ return parentNode.invocationId;
13941
+ }
13942
+ }
13943
+ return void 0;
13944
+ }
13945
+ function getIterationFromTree(tree, nodeId) {
13946
+ let currentId = nodeId;
13947
+ while (currentId) {
13948
+ const node = tree.getNode(currentId);
13949
+ if (!node) break;
13950
+ if (node.type === "llm_call") {
13951
+ return node.iteration;
13952
+ }
13953
+ currentId = node.parentId;
13954
+ }
13955
+ return 0;
13956
+ }
13957
+ function buildSubagentContext(tree, event) {
13958
+ const parentGadgetInvocationId = findParentGadgetInvocationId(tree, event.nodeId);
13959
+ if (!parentGadgetInvocationId) {
13960
+ return void 0;
13961
+ }
13962
+ return {
13963
+ parentGadgetInvocationId,
13964
+ depth: event.depth
13965
+ };
13966
+ }
13967
+ function getSubagentContextForNode(tree, nodeId) {
13968
+ const node = tree.getNode(nodeId);
13969
+ if (!node) return void 0;
13970
+ const parentGadgetInvocationId = findParentGadgetInvocationId(tree, nodeId);
13971
+ if (!parentGadgetInvocationId) {
13972
+ return void 0;
13973
+ }
13974
+ return {
13975
+ parentGadgetInvocationId,
13976
+ depth: node.depth
13977
+ };
13978
+ }
13979
+ function chainObserverCall(chainMap, key, fn, logger2, eventType, cleanup = false) {
13980
+ const previousPromise = chainMap.get(key) ?? Promise.resolve();
13981
+ const newPromise = previousPromise.then(() => safeObserve(fn, logger2, eventType));
13982
+ chainMap.set(key, newPromise);
13983
+ if (cleanup) {
13984
+ newPromise.finally(() => chainMap.delete(key));
13985
+ }
13986
+ }
13987
+ function bridgeTreeToHooks(tree, hooks, logger2) {
13988
+ const gadgetPromiseChains = /* @__PURE__ */ new Map();
13989
+ const llmPromiseChains = /* @__PURE__ */ new Map();
13990
+ return tree.onAll((event) => {
13991
+ const subagentContext = buildSubagentContext(tree, event);
13992
+ switch (event.type) {
13993
+ // =================================================================
13994
+ // GADGET EVENTS - Bridged for subagent visibility
13995
+ // =================================================================
13996
+ // When a subagent executes gadgets, these events propagate through
13997
+ // the shared tree to the parent's hooks.
13998
+ // Only bridged for subagent events (depth > 0) to avoid double-calling
13999
+ // root agent events which are handled directly in stream-processor.ts
14000
+ case "gadget_start": {
14001
+ if (subagentContext && hooks.observers?.onGadgetExecutionStart) {
14002
+ const gadgetEvent = event;
14003
+ const gadgetNode = tree.getNode(event.nodeId);
14004
+ const context = {
14005
+ iteration: getIterationFromTree(tree, event.nodeId),
14006
+ gadgetName: gadgetEvent.name,
14007
+ invocationId: gadgetEvent.invocationId,
14008
+ parameters: gadgetNode?.parameters ?? {},
14009
+ logger: logger2,
14010
+ subagentContext
14011
+ };
14012
+ chainObserverCall(
14013
+ gadgetPromiseChains,
14014
+ gadgetEvent.invocationId,
14015
+ () => hooks.observers?.onGadgetExecutionStart?.(context),
14016
+ logger2,
14017
+ "onGadgetExecutionStart",
14018
+ false
14019
+ // Don't cleanup - wait for completion event
14020
+ );
14021
+ }
14022
+ break;
14023
+ }
14024
+ case "gadget_complete": {
14025
+ if (subagentContext && hooks.observers?.onGadgetExecutionComplete) {
14026
+ const gadgetEvent = event;
14027
+ const gadgetNode = tree.getNode(event.nodeId);
14028
+ const context = {
14029
+ iteration: getIterationFromTree(tree, event.nodeId),
14030
+ gadgetName: gadgetEvent.name,
14031
+ invocationId: gadgetEvent.invocationId,
14032
+ parameters: gadgetNode?.parameters ?? {},
14033
+ finalResult: gadgetEvent.result,
14034
+ executionTimeMs: gadgetEvent.executionTimeMs,
14035
+ cost: gadgetEvent.cost,
14036
+ logger: logger2,
14037
+ subagentContext
14038
+ };
14039
+ chainObserverCall(
14040
+ gadgetPromiseChains,
14041
+ gadgetEvent.invocationId,
14042
+ () => hooks.observers?.onGadgetExecutionComplete?.(context),
14043
+ logger2,
14044
+ "onGadgetExecutionComplete",
14045
+ true
14046
+ // Cleanup after completion
14047
+ );
14048
+ }
14049
+ break;
14050
+ }
14051
+ case "gadget_error": {
14052
+ if (subagentContext && hooks.observers?.onGadgetExecutionComplete) {
14053
+ const gadgetEvent = event;
13579
14054
  const gadgetNode = tree.getNode(event.nodeId);
13580
14055
  const context = {
13581
14056
  iteration: getIterationFromTree(tree, event.nodeId),
@@ -13724,6 +14199,82 @@ function bridgeTreeToHooks(tree, hooks, logger2) {
13724
14199
  var init_tree_hook_bridge = __esm({
13725
14200
  "src/agent/tree-hook-bridge.ts"() {
13726
14201
  "use strict";
14202
+ init_safe_observe();
14203
+ }
14204
+ });
14205
+
14206
+ // src/agent/observer-notifier.ts
14207
+ async function notifyGadgetSkipped(ctx) {
14208
+ const gadgetNode = ctx.tree?.getNodeByInvocationId(ctx.invocationId);
14209
+ const subagentContext = ctx.tree && gadgetNode ? getSubagentContextForNode(ctx.tree, gadgetNode.id) : void 0;
14210
+ const context = {
14211
+ iteration: ctx.iteration,
14212
+ gadgetName: ctx.gadgetName,
14213
+ invocationId: ctx.invocationId,
14214
+ parameters: ctx.parameters,
14215
+ failedDependency: ctx.failedDependency,
14216
+ failedDependencyError: ctx.failedDependencyError,
14217
+ logger: ctx.logger,
14218
+ subagentContext
14219
+ };
14220
+ if (ctx.hooks?.onGadgetSkipped) {
14221
+ const hookFn = ctx.hooks.onGadgetSkipped;
14222
+ await safeObserve(() => hookFn(context), ctx.logger);
14223
+ }
14224
+ if (ctx.parentObservers?.onGadgetSkipped) {
14225
+ const hookFn = ctx.parentObservers.onGadgetSkipped;
14226
+ await safeObserve(() => hookFn(context), ctx.logger);
14227
+ }
14228
+ }
14229
+ async function notifyGadgetStart(ctx) {
14230
+ const gadgetNode = ctx.tree?.getNodeByInvocationId(ctx.invocationId);
14231
+ const subagentContext = ctx.tree && gadgetNode ? getSubagentContextForNode(ctx.tree, gadgetNode.id) : void 0;
14232
+ const context = {
14233
+ iteration: ctx.iteration,
14234
+ gadgetName: ctx.gadgetName,
14235
+ invocationId: ctx.invocationId,
14236
+ parameters: ctx.parameters,
14237
+ logger: ctx.logger,
14238
+ subagentContext
14239
+ };
14240
+ if (ctx.hooks?.onGadgetExecutionStart) {
14241
+ const hookFn = ctx.hooks.onGadgetExecutionStart;
14242
+ await safeObserve(() => hookFn(context), ctx.logger);
14243
+ }
14244
+ if (ctx.parentObservers?.onGadgetExecutionStart) {
14245
+ const hookFn = ctx.parentObservers.onGadgetExecutionStart;
14246
+ await safeObserve(() => hookFn(context), ctx.logger);
14247
+ }
14248
+ }
14249
+ async function notifyGadgetComplete(ctx) {
14250
+ const gadgetNode = ctx.tree?.getNodeByInvocationId(ctx.invocationId);
14251
+ const subagentContext = ctx.tree && gadgetNode ? getSubagentContextForNode(ctx.tree, gadgetNode.id) : void 0;
14252
+ const context = {
14253
+ iteration: ctx.iteration,
14254
+ gadgetName: ctx.gadgetName,
14255
+ invocationId: ctx.invocationId,
14256
+ parameters: ctx.parameters,
14257
+ finalResult: ctx.finalResult,
14258
+ error: ctx.error,
14259
+ executionTimeMs: ctx.executionTimeMs,
14260
+ cost: ctx.cost,
14261
+ logger: ctx.logger,
14262
+ subagentContext
14263
+ };
14264
+ if (ctx.hooks?.onGadgetExecutionComplete) {
14265
+ const hookFn = ctx.hooks.onGadgetExecutionComplete;
14266
+ await safeObserve(() => hookFn(context), ctx.logger);
14267
+ }
14268
+ if (ctx.parentObservers?.onGadgetExecutionComplete) {
14269
+ const hookFn = ctx.parentObservers.onGadgetExecutionComplete;
14270
+ await safeObserve(() => hookFn(context), ctx.logger);
14271
+ }
14272
+ }
14273
+ var init_observer_notifier = __esm({
14274
+ "src/agent/observer-notifier.ts"() {
14275
+ "use strict";
14276
+ init_safe_observe();
14277
+ init_tree_hook_bridge();
13727
14278
  }
13728
14279
  });
13729
14280
 
@@ -13735,11 +14286,13 @@ var init_stream_processor = __esm({
13735
14286
  init_executor();
13736
14287
  init_parser();
13737
14288
  init_logger();
14289
+ init_gadget_concurrency_manager();
14290
+ init_gadget_dependency_resolver();
13738
14291
  init_hook_validators();
13739
- init_tree_hook_bridge();
14292
+ init_observer_notifier();
14293
+ init_safe_observe();
13740
14294
  StreamProcessor = class {
13741
14295
  iteration;
13742
- registry;
13743
14296
  hooks;
13744
14297
  logger;
13745
14298
  parser;
@@ -13751,33 +14304,12 @@ var init_stream_processor = __esm({
13751
14304
  // Gadget execution mode
13752
14305
  gadgetExecutionMode;
13753
14306
  responseText = "";
13754
- observerFailureCount = 0;
13755
- // Dependency tracking for gadget execution DAG
13756
- /** Gadgets waiting for their dependencies to complete */
13757
- gadgetsAwaitingDependencies = /* @__PURE__ */ new Map();
13758
- /** Completed gadget results, keyed by invocation ID */
13759
- completedResults = /* @__PURE__ */ new Map();
13760
- /** Invocation IDs of gadgets that have failed (error or skipped due to dependency) */
13761
- failedInvocations = /* @__PURE__ */ new Set();
13762
- /** Promises for independent gadgets currently executing (fire-and-forget) */
13763
- inFlightExecutions = /* @__PURE__ */ new Map();
14307
+ // Dependency resolution is delegated to GadgetDependencyResolver
14308
+ dependencyResolver;
14309
+ // Concurrency management is delegated to GadgetConcurrencyManager
14310
+ concurrencyManager;
13764
14311
  /** Queue of completed gadget results ready to be yielded (for real-time streaming) */
13765
14312
  completedResultsQueue = [];
13766
- // Concurrency limiting
13767
- /** Subagent configuration map for checking maxConcurrent limits */
13768
- subagentConfig;
13769
- /** Track active execution count per gadget name */
13770
- activeCountByGadget = /* @__PURE__ */ new Map();
13771
- /** Queue of gadgets waiting for a concurrency slot (per gadget name) */
13772
- concurrencyQueue = /* @__PURE__ */ new Map();
13773
- // Exclusive gadget support
13774
- /** Queue of exclusive gadgets deferred until in-flight gadgets complete */
13775
- exclusiveQueue = [];
13776
- // Cross-iteration dependency tracking
13777
- /** Invocation IDs completed in previous iterations (read-only reference from Agent) */
13778
- priorCompletedInvocations;
13779
- /** Invocation IDs that failed in previous iterations (read-only reference from Agent) */
13780
- priorFailedInvocations;
13781
14313
  // Parent observer hooks for subagent visibility
13782
14314
  parentObservers;
13783
14315
  // Gadget limiting per response
@@ -13786,16 +14318,21 @@ var init_stream_processor = __esm({
13786
14318
  limitExceeded = false;
13787
14319
  constructor(options) {
13788
14320
  this.iteration = options.iteration;
13789
- this.registry = options.registry;
13790
14321
  this.hooks = options.hooks ?? {};
13791
14322
  this.logger = options.logger ?? createLogger({ name: "llmist:stream-processor" });
13792
14323
  this.tree = options.tree;
13793
14324
  this.parentNodeId = options.parentNodeId ?? null;
13794
14325
  this.baseDepth = options.baseDepth ?? 0;
13795
14326
  this.gadgetExecutionMode = options.gadgetExecutionMode ?? "parallel";
13796
- this.priorCompletedInvocations = options.priorCompletedInvocations ?? /* @__PURE__ */ new Set();
13797
- this.priorFailedInvocations = options.priorFailedInvocations ?? /* @__PURE__ */ new Set();
13798
- this.subagentConfig = options.subagentConfig;
14327
+ this.dependencyResolver = new GadgetDependencyResolver({
14328
+ priorCompletedInvocations: options.priorCompletedInvocations,
14329
+ priorFailedInvocations: options.priorFailedInvocations
14330
+ });
14331
+ this.concurrencyManager = new GadgetConcurrencyManager({
14332
+ registry: options.registry,
14333
+ subagentConfig: options.subagentConfig,
14334
+ logger: this.logger.getSubLogger({ name: "concurrency" })
14335
+ });
13799
14336
  this.parentObservers = options.parentObservers;
13800
14337
  this.maxGadgetsPerResponse = options.maxGadgetsPerResponse ?? 0;
13801
14338
  this.parser = new GadgetCallParser({
@@ -14037,7 +14574,7 @@ var init_stream_processor = __esm({
14037
14574
  gadgetName: call.gadgetName,
14038
14575
  invocationId: call.invocationId
14039
14576
  });
14040
- this.failedInvocations.add(call.invocationId);
14577
+ this.dependencyResolver.markFailed(call.invocationId);
14041
14578
  const errorMessage = `Gadget "${call.invocationId}" cannot depend on itself (self-referential dependency)`;
14042
14579
  const skipEvent = {
14043
14580
  type: "gadget_skipped",
@@ -14048,39 +14585,21 @@ var init_stream_processor = __esm({
14048
14585
  failedDependencyError: errorMessage
14049
14586
  };
14050
14587
  yield skipEvent;
14051
- const skippedGadgetNode = this.tree?.getNodeByInvocationId(call.invocationId);
14052
- const skippedSubagentContext = this.tree && skippedGadgetNode ? getSubagentContextForNode(this.tree, skippedGadgetNode.id) : void 0;
14053
- if (this.hooks.observers?.onGadgetSkipped) {
14054
- const context = {
14055
- iteration: this.iteration,
14056
- gadgetName: call.gadgetName,
14057
- invocationId: call.invocationId,
14058
- parameters: call.parameters ?? {},
14059
- failedDependency: call.invocationId,
14060
- failedDependencyError: errorMessage,
14061
- logger: this.logger,
14062
- subagentContext: skippedSubagentContext
14063
- };
14064
- await this.safeObserve(() => this.hooks.observers.onGadgetSkipped(context));
14065
- }
14066
- if (this.parentObservers?.onGadgetSkipped) {
14067
- const context = {
14068
- iteration: this.iteration,
14069
- gadgetName: call.gadgetName,
14070
- invocationId: call.invocationId,
14071
- parameters: call.parameters ?? {},
14072
- failedDependency: call.invocationId,
14073
- failedDependencyError: errorMessage,
14074
- logger: this.logger,
14075
- subagentContext: skippedSubagentContext
14076
- };
14077
- await this.safeObserve(() => this.parentObservers.onGadgetSkipped(context));
14078
- }
14588
+ await notifyGadgetSkipped({
14589
+ tree: this.tree,
14590
+ hooks: this.hooks.observers,
14591
+ parentObservers: this.parentObservers,
14592
+ logger: this.logger,
14593
+ iteration: this.iteration,
14594
+ gadgetName: call.gadgetName,
14595
+ invocationId: call.invocationId,
14596
+ parameters: call.parameters ?? {},
14597
+ failedDependency: call.invocationId,
14598
+ failedDependencyError: errorMessage
14599
+ });
14079
14600
  return;
14080
14601
  }
14081
- const failedDep = call.dependencies.find(
14082
- (dep) => this.failedInvocations.has(dep) || this.priorFailedInvocations.has(dep)
14083
- );
14602
+ const failedDep = this.dependencyResolver.getFailedDependency(call);
14084
14603
  if (failedDep) {
14085
14604
  const skipEvents = await this.handleFailedDependency(call, failedDep);
14086
14605
  for (const evt of skipEvents) {
@@ -14088,16 +14607,16 @@ var init_stream_processor = __esm({
14088
14607
  }
14089
14608
  return;
14090
14609
  }
14091
- const unsatisfied = call.dependencies.filter(
14092
- (dep) => !this.completedResults.has(dep) && !this.priorCompletedInvocations.has(dep)
14093
- );
14094
- if (unsatisfied.length > 0) {
14610
+ if (!this.dependencyResolver.isAllSatisfied(call)) {
14611
+ const unsatisfied = call.dependencies.filter(
14612
+ (dep) => !this.dependencyResolver.isCompleted(dep)
14613
+ );
14095
14614
  this.logger.debug("Queueing gadget for later - waiting on dependencies", {
14096
14615
  gadgetName: call.gadgetName,
14097
14616
  invocationId: call.invocationId,
14098
14617
  waitingOn: unsatisfied
14099
14618
  });
14100
- this.gadgetsAwaitingDependencies.set(call.invocationId, call);
14619
+ this.dependencyResolver.addPending(call);
14101
14620
  return;
14102
14621
  }
14103
14622
  const limitCheckGen2 = this.checkGadgetLimitExceeded(call);
@@ -14126,28 +14645,12 @@ var init_stream_processor = __esm({
14126
14645
  if (limitResult.value === true) {
14127
14646
  return;
14128
14647
  }
14129
- const gadget = this.registry.get(call.gadgetName);
14130
- if (gadget?.exclusive && this.inFlightExecutions.size > 0) {
14131
- this.logger.debug("Deferring exclusive gadget until in-flight gadgets complete", {
14132
- gadgetName: call.gadgetName,
14133
- invocationId: call.invocationId,
14134
- inFlightCount: this.inFlightExecutions.size
14135
- });
14136
- this.exclusiveQueue.push(call);
14648
+ if (this.concurrencyManager.isExclusive(call.gadgetName) && this.concurrencyManager.inFlightCount > 0) {
14649
+ this.concurrencyManager.queueExclusive(call);
14137
14650
  return;
14138
14651
  }
14139
- const limit = this.getConcurrencyLimit(call.gadgetName);
14140
- const activeCount = this.activeCountByGadget.get(call.gadgetName) ?? 0;
14141
- if (limit > 0 && activeCount >= limit) {
14142
- this.logger.debug("Gadget queued due to concurrency limit", {
14143
- gadgetName: call.gadgetName,
14144
- invocationId: call.invocationId,
14145
- activeCount,
14146
- limit
14147
- });
14148
- const queue = this.concurrencyQueue.get(call.gadgetName) ?? [];
14149
- queue.push(call);
14150
- this.concurrencyQueue.set(call.gadgetName, queue);
14652
+ if (!this.concurrencyManager.canStart(call)) {
14653
+ this.concurrencyManager.queueForLater(call);
14151
14654
  return;
14152
14655
  }
14153
14656
  if (this.gadgetExecutionMode === "sequential") {
@@ -14158,57 +14661,19 @@ var init_stream_processor = __esm({
14158
14661
  this.startGadgetWithConcurrencyTracking(call);
14159
14662
  }
14160
14663
  }
14161
- /**
14162
- * Get the effective concurrency limit for a gadget.
14163
- * Uses "most restrictive wins" strategy: the lowest non-zero value from
14164
- * external config (SubagentConfig) and gadget's intrinsic maxConcurrent.
14165
- *
14166
- * This ensures gadget authors can set safety floors (e.g., maxConcurrent: 1
14167
- * for file writers) that cannot be weakened by external configuration.
14168
- *
14169
- * @returns 0 if unlimited, otherwise the effective limit
14170
- */
14171
- getConcurrencyLimit(gadgetName) {
14172
- const configLimit = this.subagentConfig?.[gadgetName]?.maxConcurrent;
14173
- const gadget = this.registry.get(gadgetName);
14174
- const gadgetLimit = gadget?.maxConcurrent;
14175
- const config = configLimit || Number.POSITIVE_INFINITY;
14176
- const intrinsic = gadgetLimit || Number.POSITIVE_INFINITY;
14177
- const effective = Math.min(config, intrinsic);
14178
- return effective === Number.POSITIVE_INFINITY ? 0 : effective;
14179
- }
14180
14664
  /**
14181
14665
  * Start a gadget execution with concurrency tracking.
14182
- * Increments active count, starts execution, and schedules queue processing on completion.
14666
+ * Delegates tracking to GadgetConcurrencyManager; schedules queue processing on completion.
14183
14667
  */
14184
14668
  startGadgetWithConcurrencyTracking(call) {
14185
14669
  const gadgetName = call.gadgetName;
14186
- const currentCount = this.activeCountByGadget.get(gadgetName) ?? 0;
14187
- this.activeCountByGadget.set(gadgetName, currentCount + 1);
14188
14670
  const executionPromise = this.executeGadgetAndCollect(call).finally(() => {
14189
- const newCount = (this.activeCountByGadget.get(gadgetName) ?? 1) - 1;
14190
- this.activeCountByGadget.set(gadgetName, newCount);
14191
- this.processQueuedGadget(gadgetName);
14671
+ const nextCall = this.concurrencyManager.onComplete(gadgetName);
14672
+ if (nextCall) {
14673
+ this.startGadgetWithConcurrencyTracking(nextCall);
14674
+ }
14192
14675
  });
14193
- this.inFlightExecutions.set(call.invocationId, executionPromise);
14194
- }
14195
- /**
14196
- * Process the next queued gadget for a given gadget name if a slot is available.
14197
- */
14198
- processQueuedGadget(gadgetName) {
14199
- const queue = this.concurrencyQueue.get(gadgetName);
14200
- if (!queue || queue.length === 0) return;
14201
- const limit = this.getConcurrencyLimit(gadgetName);
14202
- const activeCount = this.activeCountByGadget.get(gadgetName) ?? 0;
14203
- if (limit === 0 || activeCount < limit) {
14204
- const nextCall = queue.shift();
14205
- this.logger.debug("Processing queued gadget", {
14206
- gadgetName,
14207
- invocationId: nextCall.invocationId,
14208
- remainingInQueue: queue.length
14209
- });
14210
- this.startGadgetWithConcurrencyTracking(nextCall);
14211
- }
14676
+ this.concurrencyManager.trackExecution(call.invocationId, gadgetName, executionPromise);
14212
14677
  }
14213
14678
  /**
14214
14679
  * Execute a gadget through the full hook lifecycle and yield events.
@@ -14263,30 +14728,16 @@ var init_stream_processor = __esm({
14263
14728
  this.tree.startGadget(gadgetNode.id);
14264
14729
  }
14265
14730
  }
14266
- const gadgetStartNode = this.tree?.getNodeByInvocationId(call.invocationId);
14267
- const gadgetStartSubagentContext = this.tree && gadgetStartNode ? getSubagentContextForNode(this.tree, gadgetStartNode.id) : void 0;
14268
- if (this.hooks.observers?.onGadgetExecutionStart) {
14269
- const context = {
14270
- iteration: this.iteration,
14271
- gadgetName: call.gadgetName,
14272
- invocationId: call.invocationId,
14273
- parameters,
14274
- logger: this.logger,
14275
- subagentContext: gadgetStartSubagentContext
14276
- };
14277
- await this.safeObserve(() => this.hooks.observers.onGadgetExecutionStart(context));
14278
- }
14279
- if (this.parentObservers?.onGadgetExecutionStart) {
14280
- const context = {
14281
- iteration: this.iteration,
14282
- gadgetName: call.gadgetName,
14283
- invocationId: call.invocationId,
14284
- parameters,
14285
- logger: this.logger,
14286
- subagentContext: gadgetStartSubagentContext
14287
- };
14288
- await this.safeObserve(() => this.parentObservers.onGadgetExecutionStart(context));
14289
- }
14731
+ await notifyGadgetStart({
14732
+ tree: this.tree,
14733
+ hooks: this.hooks.observers,
14734
+ parentObservers: this.parentObservers,
14735
+ logger: this.logger,
14736
+ iteration: this.iteration,
14737
+ gadgetName: call.gadgetName,
14738
+ invocationId: call.invocationId,
14739
+ parameters
14740
+ });
14290
14741
  let result;
14291
14742
  if (shouldSkip) {
14292
14743
  result = {
@@ -14299,7 +14750,7 @@ var init_stream_processor = __esm({
14299
14750
  } else {
14300
14751
  result = await this.executor.execute(call);
14301
14752
  }
14302
- if (result.result && this.hooks.interceptors?.interceptGadgetResult) {
14753
+ if ((result.result || result.error) && this.hooks.interceptors?.interceptGadgetResult) {
14303
14754
  const context = {
14304
14755
  iteration: this.iteration,
14305
14756
  gadgetName: result.gadgetName,
@@ -14308,7 +14759,12 @@ var init_stream_processor = __esm({
14308
14759
  executionTimeMs: result.executionTimeMs,
14309
14760
  logger: this.logger
14310
14761
  };
14311
- result.result = this.hooks.interceptors.interceptGadgetResult(result.result, context);
14762
+ if (result.result) {
14763
+ result.result = this.hooks.interceptors.interceptGadgetResult(result.result, context);
14764
+ }
14765
+ if (result.error) {
14766
+ result.error = this.hooks.interceptors.interceptGadgetResult(result.error, context);
14767
+ }
14312
14768
  }
14313
14769
  if (this.hooks.controllers?.afterGadgetExecution) {
14314
14770
  const context = {
@@ -14355,42 +14811,21 @@ var init_stream_processor = __esm({
14355
14811
  }
14356
14812
  }
14357
14813
  }
14358
- const gadgetCompleteNode = this.tree?.getNodeByInvocationId(result.invocationId);
14359
- const gadgetCompleteSubagentContext = this.tree && gadgetCompleteNode ? getSubagentContextForNode(this.tree, gadgetCompleteNode.id) : void 0;
14360
- if (this.hooks.observers?.onGadgetExecutionComplete) {
14361
- const context = {
14362
- iteration: this.iteration,
14363
- gadgetName: result.gadgetName,
14364
- invocationId: result.invocationId,
14365
- parameters,
14366
- finalResult: result.result,
14367
- error: result.error,
14368
- executionTimeMs: result.executionTimeMs,
14369
- cost: result.cost,
14370
- logger: this.logger,
14371
- subagentContext: gadgetCompleteSubagentContext
14372
- };
14373
- await this.safeObserve(() => this.hooks.observers.onGadgetExecutionComplete(context));
14374
- }
14375
- if (this.parentObservers?.onGadgetExecutionComplete) {
14376
- const context = {
14377
- iteration: this.iteration,
14378
- gadgetName: result.gadgetName,
14379
- invocationId: result.invocationId,
14380
- parameters,
14381
- finalResult: result.result,
14382
- error: result.error,
14383
- executionTimeMs: result.executionTimeMs,
14384
- cost: result.cost,
14385
- logger: this.logger,
14386
- subagentContext: gadgetCompleteSubagentContext
14387
- };
14388
- await this.safeObserve(() => this.parentObservers.onGadgetExecutionComplete(context));
14389
- }
14390
- this.completedResults.set(result.invocationId, result);
14391
- if (result.error) {
14392
- this.failedInvocations.add(result.invocationId);
14393
- }
14814
+ await notifyGadgetComplete({
14815
+ tree: this.tree,
14816
+ hooks: this.hooks.observers,
14817
+ parentObservers: this.parentObservers,
14818
+ logger: this.logger,
14819
+ iteration: this.iteration,
14820
+ gadgetName: result.gadgetName,
14821
+ invocationId: result.invocationId,
14822
+ parameters,
14823
+ finalResult: result.result,
14824
+ error: result.error,
14825
+ executionTimeMs: result.executionTimeMs,
14826
+ cost: result.cost
14827
+ });
14828
+ this.dependencyResolver.markComplete(result);
14394
14829
  yield { type: "gadget_result", result };
14395
14830
  }
14396
14831
  /**
@@ -14422,77 +14857,45 @@ var init_stream_processor = __esm({
14422
14857
  * Clears the inFlightExecutions map after all gadgets complete.
14423
14858
  */
14424
14859
  async *waitForInFlightExecutions() {
14425
- if (this.inFlightExecutions.size === 0 && !this.hasQueuedGadgets() && this.exclusiveQueue.length === 0) {
14860
+ if (this.concurrencyManager.inFlightCount === 0 && !this.concurrencyManager.hasQueuedGadgets() && !this.concurrencyManager.hasExclusiveQueued) {
14426
14861
  return;
14427
14862
  }
14428
14863
  this.logger.debug("Waiting for in-flight gadget executions", {
14429
- count: this.inFlightExecutions.size,
14430
- invocationIds: Array.from(this.inFlightExecutions.keys()),
14431
- queuedCount: this.getQueuedGadgetCount()
14864
+ count: this.concurrencyManager.inFlightCount,
14865
+ queuedCount: this.concurrencyManager.getQueuedGadgetCount()
14432
14866
  });
14433
14867
  const POLL_INTERVAL_MS = 100;
14434
- while (this.inFlightExecutions.size > 0 || this.hasQueuedGadgets()) {
14435
- const allDone = this.inFlightExecutions.size > 0 ? Promise.all(this.inFlightExecutions.values()).then(() => "done") : Promise.resolve("done");
14868
+ while (this.concurrencyManager.inFlightCount > 0 || this.concurrencyManager.hasQueuedGadgets()) {
14869
+ const allDone = this.concurrencyManager.getAllDonePromise();
14436
14870
  const result = await Promise.race([
14437
14871
  allDone,
14438
- new Promise((resolve) => setTimeout(() => resolve("poll"), POLL_INTERVAL_MS))
14872
+ new Promise((resolve2) => setTimeout(() => resolve2("poll"), POLL_INTERVAL_MS))
14439
14873
  ]);
14440
14874
  yield* this.drainCompletedResults();
14441
- if (result === "done" && this.getTotalActiveGadgetCount() === 0 && !this.hasQueuedGadgets()) {
14875
+ if (result === "done" && this.concurrencyManager.getTotalActiveGadgetCount() === 0 && !this.concurrencyManager.hasQueuedGadgets()) {
14442
14876
  break;
14443
14877
  }
14444
14878
  }
14445
- this.inFlightExecutions.clear();
14446
- if (this.exclusiveQueue.length > 0) {
14879
+ this.concurrencyManager.clearInFlight();
14880
+ if (this.concurrencyManager.hasExclusiveQueued) {
14881
+ const exclusiveQueue = this.concurrencyManager.drainExclusiveQueue();
14447
14882
  this.logger.debug("Processing deferred exclusive gadgets", {
14448
- count: this.exclusiveQueue.length
14883
+ count: exclusiveQueue.length
14449
14884
  });
14450
- const queue = this.exclusiveQueue;
14451
- this.exclusiveQueue = [];
14452
- for (const call of queue) {
14885
+ for (const call of exclusiveQueue) {
14453
14886
  for await (const evt of this.executeGadgetGenerator(call)) {
14454
14887
  yield evt;
14455
14888
  }
14456
14889
  }
14457
14890
  }
14458
14891
  }
14459
- /**
14460
- * Check if there are any gadgets waiting in concurrency queues.
14461
- */
14462
- hasQueuedGadgets() {
14463
- for (const queue of this.concurrencyQueue.values()) {
14464
- if (queue.length > 0) return true;
14465
- }
14466
- return false;
14467
- }
14468
- /**
14469
- * Get total count of queued gadgets across all queues.
14470
- */
14471
- getQueuedGadgetCount() {
14472
- let count = 0;
14473
- for (const queue of this.concurrencyQueue.values()) {
14474
- count += queue.length;
14475
- }
14476
- return count;
14477
- }
14478
- /**
14479
- * Get total count of actively executing gadgets across all types.
14480
- * Used to know when all work is truly complete (not just when allDone resolves).
14481
- */
14482
- getTotalActiveGadgetCount() {
14483
- let total = 0;
14484
- for (const count of this.activeCountByGadget.values()) {
14485
- total += count;
14486
- }
14487
- return total;
14488
- }
14489
14892
  /**
14490
14893
  * Handle a gadget that cannot execute because a dependency failed.
14491
14894
  * Calls the onDependencySkipped controller to allow customization.
14492
14895
  */
14493
14896
  async handleFailedDependency(call, failedDep) {
14494
14897
  const events = [];
14495
- const depResult = this.completedResults.get(failedDep);
14898
+ const depResult = this.dependencyResolver.getCompletedResult(failedDep);
14496
14899
  const depError = depResult?.error ?? "Dependency failed";
14497
14900
  let action = { action: "skip" };
14498
14901
  if (this.hooks.controllers?.onDependencySkipped) {
@@ -14508,7 +14911,7 @@ var init_stream_processor = __esm({
14508
14911
  action = await this.hooks.controllers.onDependencySkipped(context);
14509
14912
  }
14510
14913
  if (action.action === "skip") {
14511
- this.failedInvocations.add(call.invocationId);
14914
+ this.dependencyResolver.markFailed(call.invocationId);
14512
14915
  if (this.tree) {
14513
14916
  const gadgetNode = this.tree.getNodeByInvocationId(call.invocationId);
14514
14917
  if (gadgetNode) {
@@ -14524,34 +14927,18 @@ var init_stream_processor = __esm({
14524
14927
  failedDependencyError: depError
14525
14928
  };
14526
14929
  events.push(skipEvent);
14527
- const gadgetNodeForSkip = this.tree?.getNodeByInvocationId(call.invocationId);
14528
- const skipSubagentContext = this.tree && gadgetNodeForSkip ? getSubagentContextForNode(this.tree, gadgetNodeForSkip.id) : void 0;
14529
- if (this.hooks.observers?.onGadgetSkipped) {
14530
- const context = {
14531
- iteration: this.iteration,
14532
- gadgetName: call.gadgetName,
14533
- invocationId: call.invocationId,
14534
- parameters: call.parameters ?? {},
14535
- failedDependency: failedDep,
14536
- failedDependencyError: depError,
14537
- logger: this.logger,
14538
- subagentContext: skipSubagentContext
14539
- };
14540
- await this.safeObserve(() => this.hooks.observers.onGadgetSkipped(context));
14541
- }
14542
- if (this.parentObservers?.onGadgetSkipped) {
14543
- const context = {
14544
- iteration: this.iteration,
14545
- gadgetName: call.gadgetName,
14546
- invocationId: call.invocationId,
14547
- parameters: call.parameters ?? {},
14548
- failedDependency: failedDep,
14549
- failedDependencyError: depError,
14550
- logger: this.logger,
14551
- subagentContext: skipSubagentContext
14552
- };
14553
- await this.safeObserve(() => this.parentObservers.onGadgetSkipped(context));
14554
- }
14930
+ await notifyGadgetSkipped({
14931
+ tree: this.tree,
14932
+ hooks: this.hooks.observers,
14933
+ parentObservers: this.parentObservers,
14934
+ logger: this.logger,
14935
+ iteration: this.iteration,
14936
+ gadgetName: call.gadgetName,
14937
+ invocationId: call.invocationId,
14938
+ parameters: call.parameters ?? {},
14939
+ failedDependency: failedDep,
14940
+ failedDependencyError: depError
14941
+ });
14555
14942
  this.logger.info("Gadget skipped due to failed dependency", {
14556
14943
  gadgetName: call.gadgetName,
14557
14944
  invocationId: call.invocationId,
@@ -14574,7 +14961,7 @@ var init_stream_processor = __esm({
14574
14961
  result: action.fallbackResult,
14575
14962
  executionTimeMs: 0
14576
14963
  };
14577
- this.completedResults.set(call.invocationId, fallbackResult);
14964
+ this.dependencyResolver.markComplete(fallbackResult);
14578
14965
  events.push({ type: "gadget_result", result: fallbackResult });
14579
14966
  this.logger.info("Using fallback result for gadget with failed dependency", {
14580
14967
  gadgetName: call.gadgetName,
@@ -14605,7 +14992,7 @@ var init_stream_processor = __esm({
14605
14992
  limit: this.maxGadgetsPerResponse,
14606
14993
  currentCount: this.gadgetStartedCount
14607
14994
  });
14608
- this.failedInvocations.add(call.invocationId);
14995
+ this.dependencyResolver.markFailed(call.invocationId);
14609
14996
  if (this.tree) {
14610
14997
  const gadgetNode = this.tree.getNodeByInvocationId(call.invocationId);
14611
14998
  if (gadgetNode) {
@@ -14626,34 +15013,18 @@ var init_stream_processor = __esm({
14626
15013
  failedDependencyError: errorMessage
14627
15014
  };
14628
15015
  yield skipEvent;
14629
- const limitSkipNode = this.tree?.getNodeByInvocationId(call.invocationId);
14630
- const limitSkipSubagentContext = this.tree && limitSkipNode ? getSubagentContextForNode(this.tree, limitSkipNode.id) : void 0;
14631
- if (this.hooks.observers?.onGadgetSkipped) {
14632
- const context = {
14633
- iteration: this.iteration,
14634
- gadgetName: call.gadgetName,
14635
- invocationId: call.invocationId,
14636
- parameters: call.parameters ?? {},
14637
- failedDependency: "maxGadgetsPerResponse",
14638
- failedDependencyError: errorMessage,
14639
- logger: this.logger,
14640
- subagentContext: limitSkipSubagentContext
14641
- };
14642
- await this.safeObserve(() => this.hooks.observers.onGadgetSkipped(context));
14643
- }
14644
- if (this.parentObservers?.onGadgetSkipped) {
14645
- const context = {
14646
- iteration: this.iteration,
14647
- gadgetName: call.gadgetName,
14648
- invocationId: call.invocationId,
14649
- parameters: call.parameters ?? {},
14650
- failedDependency: "maxGadgetsPerResponse",
14651
- failedDependencyError: errorMessage,
14652
- logger: this.logger,
14653
- subagentContext: limitSkipSubagentContext
14654
- };
14655
- await this.safeObserve(() => this.parentObservers.onGadgetSkipped(context));
14656
- }
15016
+ await notifyGadgetSkipped({
15017
+ tree: this.tree,
15018
+ hooks: this.hooks.observers,
15019
+ parentObservers: this.parentObservers,
15020
+ logger: this.logger,
15021
+ iteration: this.iteration,
15022
+ gadgetName: call.gadgetName,
15023
+ invocationId: call.invocationId,
15024
+ parameters: call.parameters ?? {},
15025
+ failedDependency: "maxGadgetsPerResponse",
15026
+ failedDependencyError: errorMessage
15027
+ });
14657
15028
  return true;
14658
15029
  }
14659
15030
  this.gadgetStartedCount++;
@@ -14671,27 +15042,11 @@ var init_stream_processor = __esm({
14671
15042
  return;
14672
15043
  }
14673
15044
  let progress = true;
14674
- while (progress && this.gadgetsAwaitingDependencies.size > 0) {
15045
+ while (progress && this.dependencyResolver.pendingCount > 0) {
14675
15046
  progress = false;
14676
- const readyToExecute = [];
14677
- const readyToSkip = [];
14678
- for (const [_invocationId, call] of this.gadgetsAwaitingDependencies) {
14679
- const failedDep = call.dependencies.find(
14680
- (dep) => this.failedInvocations.has(dep) || this.priorFailedInvocations.has(dep)
14681
- );
14682
- if (failedDep) {
14683
- readyToSkip.push({ call, failedDep });
14684
- continue;
14685
- }
14686
- const allSatisfied = call.dependencies.every(
14687
- (dep) => this.completedResults.has(dep) || this.priorCompletedInvocations.has(dep)
14688
- );
14689
- if (allSatisfied) {
14690
- readyToExecute.push(call);
14691
- }
14692
- }
15047
+ const { readyToExecute, readyToSkip } = this.dependencyResolver.getReadyCalls();
14693
15048
  for (const { call, failedDep } of readyToSkip) {
14694
- this.gadgetsAwaitingDependencies.delete(call.invocationId);
15049
+ this.dependencyResolver.removePending(call.invocationId);
14695
15050
  const skipEvents = await this.handleFailedDependency(call, failedDep);
14696
15051
  for (const evt of skipEvents) {
14697
15052
  yield evt;
@@ -14700,7 +15055,7 @@ var init_stream_processor = __esm({
14700
15055
  }
14701
15056
  if (readyToExecute.length > 0) {
14702
15057
  for (const call of readyToExecute) {
14703
- this.gadgetsAwaitingDependencies.delete(call.invocationId);
15058
+ this.dependencyResolver.removePending(call.invocationId);
14704
15059
  }
14705
15060
  if (this.gadgetExecutionMode === "sequential") {
14706
15061
  this.logger.debug("Executing ready gadgets sequentially", {
@@ -14753,11 +15108,12 @@ var init_stream_processor = __esm({
14753
15108
  progress = true;
14754
15109
  }
14755
15110
  }
14756
- if (this.gadgetsAwaitingDependencies.size > 0) {
14757
- const pendingIds = new Set(this.gadgetsAwaitingDependencies.keys());
14758
- for (const [invocationId, call] of this.gadgetsAwaitingDependencies) {
15111
+ if (this.dependencyResolver.pendingCount > 0) {
15112
+ const pendingEntries = this.dependencyResolver.getPendingEntries();
15113
+ const pendingIds = new Set(pendingEntries.map(([id]) => id));
15114
+ for (const [invocationId, call] of pendingEntries) {
14759
15115
  const missingDeps = call.dependencies.filter(
14760
- (dep) => !this.completedResults.has(dep) && !this.priorCompletedInvocations.has(dep)
15116
+ (dep) => !this.dependencyResolver.isCompleted(dep)
14761
15117
  );
14762
15118
  const circularDeps = missingDeps.filter((dep) => pendingIds.has(dep));
14763
15119
  const trulyMissingDeps = missingDeps.filter((dep) => !pendingIds.has(dep));
@@ -14777,7 +15133,7 @@ var init_stream_processor = __esm({
14777
15133
  circularDependencies: circularDeps,
14778
15134
  missingDependencies: trulyMissingDeps
14779
15135
  });
14780
- this.failedInvocations.add(invocationId);
15136
+ this.dependencyResolver.markFailed(invocationId);
14781
15137
  const skipEvent = {
14782
15138
  type: "gadget_skipped",
14783
15139
  gadgetName: call.gadgetName,
@@ -14787,51 +15143,20 @@ var init_stream_processor = __esm({
14787
15143
  failedDependencyError: errorMessage
14788
15144
  };
14789
15145
  yield skipEvent;
14790
- const gadgetNodeForTimeout = this.tree?.getNodeByInvocationId(invocationId);
14791
- const timeoutSubagentContext = this.tree && gadgetNodeForTimeout ? getSubagentContextForNode(this.tree, gadgetNodeForTimeout.id) : void 0;
14792
- if (this.hooks.observers?.onGadgetSkipped) {
14793
- const context = {
14794
- iteration: this.iteration,
14795
- gadgetName: call.gadgetName,
14796
- invocationId,
14797
- parameters: call.parameters ?? {},
14798
- failedDependency: missingDeps[0],
14799
- failedDependencyError: errorMessage,
14800
- logger: this.logger,
14801
- subagentContext: timeoutSubagentContext
14802
- };
14803
- await this.safeObserve(() => this.hooks.observers.onGadgetSkipped(context));
14804
- }
14805
- if (this.parentObservers?.onGadgetSkipped) {
14806
- const context = {
14807
- iteration: this.iteration,
14808
- gadgetName: call.gadgetName,
14809
- invocationId,
14810
- parameters: call.parameters ?? {},
14811
- failedDependency: missingDeps[0],
14812
- failedDependencyError: errorMessage,
14813
- logger: this.logger,
14814
- subagentContext: timeoutSubagentContext
14815
- };
14816
- await this.safeObserve(() => this.parentObservers.onGadgetSkipped(context));
14817
- }
15146
+ await notifyGadgetSkipped({
15147
+ tree: this.tree,
15148
+ hooks: this.hooks.observers,
15149
+ parentObservers: this.parentObservers,
15150
+ logger: this.logger,
15151
+ iteration: this.iteration,
15152
+ gadgetName: call.gadgetName,
15153
+ invocationId,
15154
+ parameters: call.parameters ?? {},
15155
+ failedDependency: missingDeps[0],
15156
+ failedDependencyError: errorMessage
15157
+ });
14818
15158
  }
14819
- this.gadgetsAwaitingDependencies.clear();
14820
- }
14821
- }
14822
- /**
14823
- * Safely execute an observer, catching and logging any errors.
14824
- * Observers are non-critical, so errors are logged but don't crash the system.
14825
- */
14826
- async safeObserve(fn) {
14827
- try {
14828
- await fn();
14829
- } catch (error) {
14830
- this.observerFailureCount++;
14831
- this.logger.error("Observer threw error (ignoring)", {
14832
- error: error instanceof Error ? error.message : String(error),
14833
- failureCount: this.observerFailureCount
14834
- });
15159
+ this.dependencyResolver.clearPending();
14835
15160
  }
14836
15161
  }
14837
15162
  /**
@@ -14840,7 +15165,7 @@ var init_stream_processor = __esm({
14840
15165
  */
14841
15166
  async runObserversInParallel(observers) {
14842
15167
  if (observers.length === 0) return;
14843
- await Promise.allSettled(observers.map((observer) => this.safeObserve(observer)));
15168
+ await Promise.allSettled(observers.map((observer) => safeObserve(observer, this.logger)));
14844
15169
  }
14845
15170
  // ==========================================================================
14846
15171
  // Public accessors for cross-iteration dependency tracking
@@ -14850,14 +15175,14 @@ var init_stream_processor = __esm({
14850
15175
  * Used by Agent to accumulate completed IDs across iterations.
14851
15176
  */
14852
15177
  getCompletedInvocationIds() {
14853
- return new Set(this.completedResults.keys());
15178
+ return this.dependencyResolver.getCompletedInvocationIds();
14854
15179
  }
14855
15180
  /**
14856
15181
  * Get all invocation IDs that failed in this iteration.
14857
15182
  * Used by Agent to accumulate failed IDs across iterations.
14858
15183
  */
14859
15184
  getFailedInvocationIds() {
14860
- return new Set(this.failedInvocations);
15185
+ return this.dependencyResolver.getFailedInvocationIds();
14861
15186
  }
14862
15187
  };
14863
15188
  }
@@ -14884,6 +15209,7 @@ var init_agent = __esm({
14884
15209
  init_event_handlers();
14885
15210
  init_gadget_output_store();
14886
15211
  init_hook_validators();
15212
+ init_safe_observe();
14887
15213
  init_stream_processor();
14888
15214
  init_tree_hook_bridge();
14889
15215
  Agent = class {
@@ -15372,7 +15698,7 @@ var init_agent = __esm({
15372
15698
  }
15373
15699
  );
15374
15700
  this.retryConfig.onRetry?.(error, streamAttempt);
15375
- await this.safeObserve(async () => {
15701
+ await safeObserve(async () => {
15376
15702
  if (this.hooks.observers?.onRetryAttempt) {
15377
15703
  const subagentContext = getSubagentContextForNode(this.tree, currentLLMNodeId);
15378
15704
  const hookContext = {
@@ -15386,7 +15712,7 @@ var init_agent = __esm({
15386
15712
  };
15387
15713
  await this.hooks.observers.onRetryAttempt(hookContext);
15388
15714
  }
15389
- });
15715
+ }, this.logger);
15390
15716
  await this.sleep(finalDelay);
15391
15717
  streamMetadata = null;
15392
15718
  gadgetCallCount = 0;
@@ -15416,7 +15742,7 @@ var init_agent = __esm({
15416
15742
  this.logger.silly("LLM response details", {
15417
15743
  rawResponse: result.rawResponse
15418
15744
  });
15419
- await this.safeObserve(async () => {
15745
+ await safeObserve(async () => {
15420
15746
  if (this.hooks.observers?.onLLMCallComplete) {
15421
15747
  const subagentContext = getSubagentContextForNode(this.tree, currentLLMNodeId);
15422
15748
  const context = {
@@ -15432,7 +15758,7 @@ var init_agent = __esm({
15432
15758
  };
15433
15759
  await this.hooks.observers.onLLMCallComplete(context);
15434
15760
  }
15435
- });
15761
+ }, this.logger);
15436
15762
  this.completeLLMCallInTree(currentLLMNodeId, result);
15437
15763
  const finalMessage = await this.processAfterLLMCallController(
15438
15764
  currentIteration,
@@ -15466,7 +15792,7 @@ var init_agent = __esm({
15466
15792
  }
15467
15793
  } catch (error) {
15468
15794
  const errorHandled = await this.handleLLMError(error, currentIteration);
15469
- await this.safeObserve(async () => {
15795
+ await safeObserve(async () => {
15470
15796
  if (this.hooks.observers?.onLLMCallError) {
15471
15797
  const options = llmOptions ?? {
15472
15798
  model: this.model,
@@ -15485,7 +15811,7 @@ var init_agent = __esm({
15485
15811
  };
15486
15812
  await this.hooks.observers.onLLMCallError(context);
15487
15813
  }
15488
- });
15814
+ }, this.logger);
15489
15815
  if (!errorHandled) {
15490
15816
  throw error;
15491
15817
  }
@@ -15512,7 +15838,7 @@ var init_agent = __esm({
15512
15838
  if (currentLLMNodeId) {
15513
15839
  const node = this.tree.getNode(currentLLMNodeId);
15514
15840
  if (node && node.type === "llm_call" && !node.completedAt) {
15515
- await this.safeObserve(async () => {
15841
+ await safeObserve(async () => {
15516
15842
  if (this.hooks.observers?.onLLMCallComplete) {
15517
15843
  const subagentContext = getSubagentContextForNode(this.tree, currentLLMNodeId);
15518
15844
  const context = {
@@ -15534,7 +15860,7 @@ var init_agent = __esm({
15534
15860
  };
15535
15861
  await this.hooks.observers.onLLMCallComplete(context);
15536
15862
  }
15537
- });
15863
+ }, this.logger);
15538
15864
  this.tree.completeLLMCall(currentLLMNodeId, {
15539
15865
  finishReason: "interrupted"
15540
15866
  });
@@ -15554,7 +15880,7 @@ var init_agent = __esm({
15554
15880
  const throttleDelay = this.rateLimitTracker.getRequiredDelayMs();
15555
15881
  if (throttleDelay > 0) {
15556
15882
  this.logger.debug("Rate limit throttling", { delayMs: throttleDelay });
15557
- await this.safeObserve(async () => {
15883
+ await safeObserve(async () => {
15558
15884
  if (this.hooks.observers?.onRateLimitThrottle) {
15559
15885
  const subagentContext = getSubagentContextForNode(this.tree, llmNodeId);
15560
15886
  const context = {
@@ -15566,7 +15892,7 @@ var init_agent = __esm({
15566
15892
  };
15567
15893
  await this.hooks.observers.onRateLimitThrottle(context);
15568
15894
  }
15569
- });
15895
+ }, this.logger);
15570
15896
  await this.sleep(throttleDelay);
15571
15897
  }
15572
15898
  this.rateLimitTracker.reserveRequest();
@@ -15577,7 +15903,7 @@ var init_agent = __esm({
15577
15903
  * Simple sleep utility for rate limit delays.
15578
15904
  */
15579
15905
  sleep(ms) {
15580
- return new Promise((resolve) => setTimeout(resolve, ms));
15906
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
15581
15907
  }
15582
15908
  /**
15583
15909
  * Handle LLM error through controller.
@@ -15629,18 +15955,6 @@ var init_agent = __esm({
15629
15955
  }
15630
15956
  return true;
15631
15957
  }
15632
- /**
15633
- * Safely execute an observer, catching and logging any errors.
15634
- */
15635
- async safeObserve(fn) {
15636
- try {
15637
- await fn();
15638
- } catch (error) {
15639
- this.logger.error("Observer threw error (ignoring)", {
15640
- error: error instanceof Error ? error.message : String(error)
15641
- });
15642
- }
15643
- }
15644
15958
  /**
15645
15959
  * Resolve max tokens from model catalog.
15646
15960
  */
@@ -15709,7 +16023,7 @@ var init_agent = __esm({
15709
16023
  iteration,
15710
16024
  reason: this.signal.reason
15711
16025
  });
15712
- await this.safeObserve(async () => {
16026
+ await safeObserve(async () => {
15713
16027
  if (this.hooks.observers?.onAbort) {
15714
16028
  const context = {
15715
16029
  iteration,
@@ -15718,7 +16032,7 @@ var init_agent = __esm({
15718
16032
  };
15719
16033
  await this.hooks.observers.onAbort(context);
15720
16034
  }
15721
- });
16035
+ }, this.logger);
15722
16036
  return true;
15723
16037
  }
15724
16038
  /**
@@ -15737,7 +16051,7 @@ var init_agent = __esm({
15737
16051
  tokensBefore: compactionEvent.tokensBefore,
15738
16052
  tokensAfter: compactionEvent.tokensAfter
15739
16053
  });
15740
- await this.safeObserve(async () => {
16054
+ await safeObserve(async () => {
15741
16055
  if (this.hooks.observers?.onCompaction) {
15742
16056
  await this.hooks.observers.onCompaction({
15743
16057
  iteration,
@@ -15747,7 +16061,7 @@ var init_agent = __esm({
15747
16061
  logger: this.logger
15748
16062
  });
15749
16063
  }
15750
- });
16064
+ }, this.logger);
15751
16065
  return { type: "compaction", event: compactionEvent };
15752
16066
  }
15753
16067
  /**
@@ -15800,7 +16114,7 @@ var init_agent = __esm({
15800
16114
  parentId: this.parentNodeId,
15801
16115
  request: llmOptions.messages
15802
16116
  });
15803
- await this.safeObserve(async () => {
16117
+ await safeObserve(async () => {
15804
16118
  if (this.hooks.observers?.onLLMCallStart) {
15805
16119
  const subagentContext = getSubagentContextForNode(this.tree, llmNode.id);
15806
16120
  const context = {
@@ -15811,7 +16125,7 @@ var init_agent = __esm({
15811
16125
  };
15812
16126
  await this.hooks.observers.onLLMCallStart(context);
15813
16127
  }
15814
- });
16128
+ }, this.logger);
15815
16129
  if (this.hooks.controllers?.beforeLLMCall) {
15816
16130
  const context = {
15817
16131
  iteration,
@@ -15834,7 +16148,7 @@ var init_agent = __esm({
15834
16148
  llmOptions = { ...llmOptions, ...action.modifiedOptions };
15835
16149
  }
15836
16150
  }
15837
- await this.safeObserve(async () => {
16151
+ await safeObserve(async () => {
15838
16152
  if (this.hooks.observers?.onLLMCallReady) {
15839
16153
  const subagentContext = getSubagentContextForNode(this.tree, llmNode.id);
15840
16154
  const context = {
@@ -15848,7 +16162,7 @@ var init_agent = __esm({
15848
16162
  };
15849
16163
  await this.hooks.observers.onLLMCallReady(context);
15850
16164
  }
15851
- });
16165
+ }, this.logger);
15852
16166
  return { options: llmOptions, llmNodeId: llmNode.id };
15853
16167
  }
15854
16168
  /**
@@ -16033,6 +16347,7 @@ __export(index_exports, {
16033
16347
  collectText: () => collectText,
16034
16348
  complete: () => complete,
16035
16349
  createAnthropicProviderFromEnv: () => createAnthropicProviderFromEnv,
16350
+ createFileLoggingState: () => createFileLoggingState,
16036
16351
  createGadget: () => createGadget,
16037
16352
  createGadgetOutputViewer: () => createGadgetOutputViewer,
16038
16353
  createGeminiProviderFromEnv: () => createGeminiProviderFromEnv,
@@ -16095,6 +16410,7 @@ __export(index_exports, {
16095
16410
  parseManifest: () => parseManifest,
16096
16411
  parseRetryAfterHeader: () => parseRetryAfterHeader,
16097
16412
  randomDelay: () => randomDelay,
16413
+ resetFileLoggingState: () => resetFileLoggingState,
16098
16414
  resolveConfig: () => resolveConfig,
16099
16415
  resolveHintTemplate: () => resolveHintTemplate,
16100
16416
  resolveModel: () => resolveModel,
@@ -16772,10 +17088,10 @@ function randomDelay(min, max) {
16772
17088
  }
16773
17089
  async function humanDelay(min = 50, max = 150) {
16774
17090
  const delay = randomDelay(min, max);
16775
- return new Promise((resolve) => setTimeout(resolve, delay));
17091
+ return new Promise((resolve2) => setTimeout(resolve2, delay));
16776
17092
  }
16777
17093
  async function withTimeout(fn, timeoutMs, signal) {
16778
- return new Promise((resolve, reject) => {
17094
+ return new Promise((resolve2, reject) => {
16779
17095
  if (signal?.aborted) {
16780
17096
  reject(new Error("Operation aborted"));
16781
17097
  return;
@@ -16801,7 +17117,7 @@ async function withTimeout(fn, timeoutMs, signal) {
16801
17117
  settled = true;
16802
17118
  clearTimeout(timeoutId);
16803
17119
  signal?.removeEventListener("abort", abortHandler);
16804
- resolve(result);
17120
+ resolve2(result);
16805
17121
  }
16806
17122
  }).catch((error) => {
16807
17123
  if (!settled) {
@@ -16834,7 +17150,7 @@ async function withRetry(fn, options = {}) {
16834
17150
  }
16835
17151
  const waitTime = Math.min(currentDelay, maxDelay);
16836
17152
  onRetry?.(error, attempt + 1, waitTime);
16837
- await new Promise((resolve) => setTimeout(resolve, waitTime));
17153
+ await new Promise((resolve2) => setTimeout(resolve2, waitTime));
16838
17154
  if (backoff === "exponential") {
16839
17155
  currentDelay *= 2;
16840
17156
  } else {
@@ -16914,6 +17230,7 @@ function getHostExports2(ctx) {
16914
17230
  collectText,
16915
17231
  complete,
16916
17232
  createAnthropicProviderFromEnv,
17233
+ createFileLoggingState,
16917
17234
  createGadget,
16918
17235
  createGadgetOutputViewer,
16919
17236
  createGeminiProviderFromEnv,
@@ -16976,6 +17293,7 @@ function getHostExports2(ctx) {
16976
17293
  parseManifest,
16977
17294
  parseRetryAfterHeader,
16978
17295
  randomDelay,
17296
+ resetFileLoggingState,
16979
17297
  resolveConfig,
16980
17298
  resolveHintTemplate,
16981
17299
  resolveModel,