llmist 16.0.3 → 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.js CHANGED
@@ -3965,6 +3965,23 @@ var init_hook_validators = __esm({
3965
3965
  }
3966
3966
  });
3967
3967
 
3968
+ // src/agent/safe-observe.ts
3969
+ async function safeObserve(fn, logger2, label) {
3970
+ try {
3971
+ await fn();
3972
+ } catch (error) {
3973
+ const message = label ? `Observer error in ${label}:` : "Observer threw error (ignoring)";
3974
+ logger2.error(message, {
3975
+ error: error instanceof Error ? error.message : String(error)
3976
+ });
3977
+ }
3978
+ }
3979
+ var init_safe_observe = __esm({
3980
+ "src/agent/safe-observe.ts"() {
3981
+ "use strict";
3982
+ }
3983
+ });
3984
+
3968
3985
  // src/gadgets/registry.ts
3969
3986
  var GadgetRegistry;
3970
3987
  var init_registry = __esm({
@@ -4419,6 +4436,8 @@ var init_hook_presets = __esm({
4419
4436
  /**
4420
4437
  * Tracks cumulative token usage across all LLM calls.
4421
4438
  *
4439
+ * @public
4440
+ *
4422
4441
  * **Output:**
4423
4442
  * - Per-call token count with 📊 emoji
4424
4443
  * - Cumulative total across all calls
@@ -4681,6 +4700,8 @@ var init_hook_presets = __esm({
4681
4700
  /**
4682
4701
  * Logs detailed error information for debugging and troubleshooting.
4683
4702
  *
4703
+ * @public
4704
+ *
4684
4705
  * **Output:**
4685
4706
  * - LLM errors with ❌ emoji, including model and recovery status
4686
4707
  * - Gadget errors with full context (parameters, error message)
@@ -4759,6 +4780,8 @@ var init_hook_presets = __esm({
4759
4780
  /**
4760
4781
  * Tracks context compaction events.
4761
4782
  *
4783
+ * @public
4784
+ *
4762
4785
  * **Output:**
4763
4786
  * - Compaction events with 🗜️ emoji
4764
4787
  * - Strategy name, tokens before/after, and savings
@@ -4872,6 +4895,8 @@ var init_hook_presets = __esm({
4872
4895
  /**
4873
4896
  * Returns empty hook configuration for clean output without any logging.
4874
4897
  *
4898
+ * @public
4899
+ *
4875
4900
  * **Output:**
4876
4901
  * - None. Returns {} (empty object).
4877
4902
  *
@@ -5029,6 +5054,8 @@ var init_hook_presets = __esm({
5029
5054
  /**
5030
5055
  * Composite preset combining logging, timing, tokenTracking, and errorLogging.
5031
5056
  *
5057
+ * @public
5058
+ *
5032
5059
  * This is the recommended preset for development and initial production deployments,
5033
5060
  * providing comprehensive observability with a single method call.
5034
5061
  *
@@ -11979,7 +12006,9 @@ ${endPrefix}`
11979
12006
  */
11980
12007
  /**
11981
12008
  * Build AgentOptions with the given user prompt.
11982
- * Centralizes options construction for ask(), askWithImage(), and askWithContent().
12009
+ * Centralizes options construction for ask(), askWithImage(), askWithContent(), and build().
12010
+ *
12011
+ * @param userPrompt - Optional user prompt (omitted for build() which has no prompt)
11983
12012
  */
11984
12013
  buildAgentOptions(userPrompt) {
11985
12014
  if (!this.client) {
@@ -12021,7 +12050,12 @@ ${endPrefix}`
12021
12050
  // Tree context for shared tree model (subagents share parent's tree)
12022
12051
  parentTree: this.parentContext?.tree,
12023
12052
  parentNodeId: this.parentContext?.nodeId,
12024
- baseDepth: this.parentContext ? (this.parentContext.depth ?? 0) + 1 : 0
12053
+ baseDepth: this.parentContext ? (this.parentContext.depth ?? 0) + 1 : 0,
12054
+ // Parent observer hooks for subagent visibility
12055
+ parentObservers: this.parentObservers,
12056
+ // Shared rate limit tracker and retry config (for coordinated limits across subagents)
12057
+ sharedRateLimitTracker: this.sharedRateLimitTracker,
12058
+ sharedRetryConfig: this.sharedRetryConfig
12025
12059
  };
12026
12060
  }
12027
12061
  ask(userPrompt) {
@@ -12173,52 +12207,7 @@ ${endPrefix}`
12173
12207
  * ```
12174
12208
  */
12175
12209
  build() {
12176
- if (!this.client) {
12177
- const { LLMist: LLMistClass } = (init_client(), __toCommonJS(client_exports));
12178
- this.client = new LLMistClass();
12179
- }
12180
- const registry = GadgetRegistry.from(this.gadgets);
12181
- const options = {
12182
- client: this.client,
12183
- model: this.model ?? "openai:gpt-5-nano",
12184
- systemPrompt: this.systemPrompt,
12185
- // No userPrompt - agent.run() will throw if called directly
12186
- registry,
12187
- maxIterations: this.maxIterations,
12188
- budget: this.budget,
12189
- temperature: this.temperature,
12190
- logger: this.logger,
12191
- hooks: this.composeHooks(),
12192
- promptConfig: this.promptConfig,
12193
- initialMessages: this.initialMessages,
12194
- requestHumanInput: this.requestHumanInput,
12195
- gadgetStartPrefix: this.gadgetStartPrefix,
12196
- gadgetEndPrefix: this.gadgetEndPrefix,
12197
- gadgetArgPrefix: this.gadgetArgPrefix,
12198
- textOnlyHandler: this.textOnlyHandler,
12199
- textWithGadgetsHandler: this.textWithGadgetsHandler,
12200
- defaultGadgetTimeoutMs: this.defaultGadgetTimeoutMs,
12201
- gadgetExecutionMode: this.gadgetExecutionMode,
12202
- maxGadgetsPerResponse: this.maxGadgetsPerResponse,
12203
- gadgetOutputLimit: this.gadgetOutputLimit,
12204
- gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
12205
- compactionConfig: this.compactionConfig,
12206
- retryConfig: this.retryConfig,
12207
- rateLimitConfig: this.rateLimitConfig,
12208
- signal: this.signal,
12209
- reasoning: this.reasoningConfig,
12210
- caching: this.cachingConfig,
12211
- subagentConfig: this.subagentConfig,
12212
- // Tree context for shared tree model (subagents share parent's tree)
12213
- parentTree: this.parentContext?.tree,
12214
- parentNodeId: this.parentContext?.nodeId,
12215
- baseDepth: this.parentContext ? (this.parentContext.depth ?? 0) + 1 : 0,
12216
- // Parent observer hooks for subagent visibility
12217
- parentObservers: this.parentObservers,
12218
- // Shared rate limit tracker and retry config (for coordinated limits across subagents)
12219
- sharedRateLimitTracker: this.sharedRateLimitTracker,
12220
- sharedRetryConfig: this.sharedRetryConfig
12221
- };
12210
+ const options = this.buildAgentOptions();
12222
12211
  return new Agent(AGENT_INTERNAL_KEY, options);
12223
12212
  }
12224
12213
  };
@@ -13506,139 +13495,551 @@ var init_executor = __esm({
13506
13495
  }
13507
13496
  });
13508
13497
 
13509
- // src/agent/tree-hook-bridge.ts
13510
- function findParentGadgetInvocationId(tree, nodeId) {
13511
- let currentId = nodeId;
13512
- while (currentId) {
13513
- const node = tree.getNode(currentId);
13514
- if (!node) break;
13515
- currentId = node.parentId;
13516
- if (!currentId) break;
13517
- const parentNode = tree.getNode(currentId);
13518
- if (parentNode?.type === "gadget") {
13519
- return parentNode.invocationId;
13520
- }
13521
- }
13522
- return void 0;
13523
- }
13524
- function getIterationFromTree(tree, nodeId) {
13525
- let currentId = nodeId;
13526
- while (currentId) {
13527
- const node = tree.getNode(currentId);
13528
- if (!node) break;
13529
- if (node.type === "llm_call") {
13530
- return node.iteration;
13531
- }
13532
- currentId = node.parentId;
13533
- }
13534
- return 0;
13535
- }
13536
- function buildSubagentContext(tree, event) {
13537
- const parentGadgetInvocationId = findParentGadgetInvocationId(tree, event.nodeId);
13538
- if (!parentGadgetInvocationId) {
13539
- return void 0;
13540
- }
13541
- return {
13542
- parentGadgetInvocationId,
13543
- depth: event.depth
13544
- };
13545
- }
13546
- function getSubagentContextForNode(tree, nodeId) {
13547
- const node = tree.getNode(nodeId);
13548
- if (!node) return void 0;
13549
- const parentGadgetInvocationId = findParentGadgetInvocationId(tree, nodeId);
13550
- if (!parentGadgetInvocationId) {
13551
- return void 0;
13552
- }
13553
- return {
13554
- parentGadgetInvocationId,
13555
- depth: node.depth
13556
- };
13557
- }
13558
- async function safeObserve(fn, logger2, eventType) {
13559
- try {
13560
- await fn();
13561
- } catch (error) {
13562
- logger2.warn(`Observer error in ${eventType}:`, error);
13563
- }
13564
- }
13565
- function chainObserverCall(chainMap, key, fn, logger2, eventType, cleanup = false) {
13566
- const previousPromise = chainMap.get(key) ?? Promise.resolve();
13567
- const newPromise = previousPromise.then(() => safeObserve(fn, logger2, eventType));
13568
- chainMap.set(key, newPromise);
13569
- if (cleanup) {
13570
- newPromise.finally(() => chainMap.delete(key));
13571
- }
13572
- }
13573
- function bridgeTreeToHooks(tree, hooks, logger2) {
13574
- const gadgetPromiseChains = /* @__PURE__ */ new Map();
13575
- const llmPromiseChains = /* @__PURE__ */ new Map();
13576
- return tree.onAll((event) => {
13577
- const subagentContext = buildSubagentContext(tree, event);
13578
- switch (event.type) {
13579
- // =================================================================
13580
- // GADGET EVENTS - Bridged for subagent visibility
13581
- // =================================================================
13582
- // When a subagent executes gadgets, these events propagate through
13583
- // the shared tree to the parent's hooks.
13584
- // Only bridged for subagent events (depth > 0) to avoid double-calling
13585
- // root agent events which are handled directly in stream-processor.ts
13586
- case "gadget_start": {
13587
- if (subagentContext && hooks.observers?.onGadgetExecutionStart) {
13588
- const gadgetEvent = event;
13589
- const gadgetNode = tree.getNode(event.nodeId);
13590
- const context = {
13591
- iteration: getIterationFromTree(tree, event.nodeId),
13592
- gadgetName: gadgetEvent.name,
13593
- invocationId: gadgetEvent.invocationId,
13594
- parameters: gadgetNode?.parameters ?? {},
13595
- logger: logger2,
13596
- subagentContext
13597
- };
13598
- chainObserverCall(
13599
- gadgetPromiseChains,
13600
- gadgetEvent.invocationId,
13601
- () => hooks.observers?.onGadgetExecutionStart?.(context),
13602
- logger2,
13603
- "onGadgetExecutionStart",
13604
- false
13605
- // Don't cleanup - wait for completion event
13606
- );
13498
+ // src/agent/gadget-concurrency-manager.ts
13499
+ var GadgetConcurrencyManager;
13500
+ var init_gadget_concurrency_manager = __esm({
13501
+ "src/agent/gadget-concurrency-manager.ts"() {
13502
+ "use strict";
13503
+ init_logger();
13504
+ GadgetConcurrencyManager = class {
13505
+ registry;
13506
+ subagentConfig;
13507
+ logger;
13508
+ /** Track active execution count per gadget name */
13509
+ activeCountByGadget = /* @__PURE__ */ new Map();
13510
+ /** Queue of gadgets waiting for a concurrency slot (per gadget name) */
13511
+ concurrencyQueue = /* @__PURE__ */ new Map();
13512
+ /** All active gadget promises, keyed by invocationId */
13513
+ inFlightExecutions = /* @__PURE__ */ new Map();
13514
+ /** Queue of exclusive gadgets deferred until in-flight gadgets complete */
13515
+ exclusiveQueue = [];
13516
+ constructor(options) {
13517
+ this.registry = options.registry;
13518
+ this.subagentConfig = options.subagentConfig;
13519
+ this.logger = options.logger ?? createLogger({ name: "llmist:gadget-concurrency-manager" });
13520
+ }
13521
+ // ==========================================================================
13522
+ // Concurrency limit resolution
13523
+ // ==========================================================================
13524
+ /**
13525
+ * Get the effective concurrency limit for a gadget.
13526
+ * Uses "most restrictive wins" strategy: the lowest non-zero value from
13527
+ * external config (SubagentConfig) and gadget's intrinsic maxConcurrent.
13528
+ *
13529
+ * This ensures gadget authors can set safety floors (e.g., maxConcurrent: 1
13530
+ * for file writers) that cannot be weakened by external configuration.
13531
+ *
13532
+ * @returns 0 if unlimited, otherwise the effective limit
13533
+ */
13534
+ getConcurrencyLimit(gadgetName) {
13535
+ const configLimit = this.subagentConfig?.[gadgetName]?.maxConcurrent;
13536
+ const gadget = this.registry.get(gadgetName);
13537
+ const gadgetLimit = gadget?.maxConcurrent;
13538
+ const config = configLimit || Number.POSITIVE_INFINITY;
13539
+ const intrinsic = gadgetLimit || Number.POSITIVE_INFINITY;
13540
+ const effective = Math.min(config, intrinsic);
13541
+ return effective === Number.POSITIVE_INFINITY ? 0 : effective;
13542
+ }
13543
+ // ==========================================================================
13544
+ // Concurrency checks
13545
+ // ==========================================================================
13546
+ /**
13547
+ * Check whether a gadget call can start immediately given current concurrency state.
13548
+ * Returns false if:
13549
+ * - The gadget is exclusive and other gadgets are in-flight
13550
+ * - The gadget has a concurrency limit and it is already reached
13551
+ *
13552
+ * Does NOT modify any state.
13553
+ */
13554
+ canStart(call) {
13555
+ const gadget = this.registry.get(call.gadgetName);
13556
+ if (gadget?.exclusive && this.inFlightExecutions.size > 0) {
13557
+ return false;
13607
13558
  }
13608
- break;
13559
+ const limit = this.getConcurrencyLimit(call.gadgetName);
13560
+ if (limit > 0) {
13561
+ const activeCount = this.activeCountByGadget.get(call.gadgetName) ?? 0;
13562
+ if (activeCount >= limit) {
13563
+ return false;
13564
+ }
13565
+ }
13566
+ return true;
13609
13567
  }
13610
- case "gadget_complete": {
13611
- if (subagentContext && hooks.observers?.onGadgetExecutionComplete) {
13612
- const gadgetEvent = event;
13613
- const gadgetNode = tree.getNode(event.nodeId);
13614
- const context = {
13615
- iteration: getIterationFromTree(tree, event.nodeId),
13616
- gadgetName: gadgetEvent.name,
13617
- invocationId: gadgetEvent.invocationId,
13618
- parameters: gadgetNode?.parameters ?? {},
13619
- finalResult: gadgetEvent.result,
13620
- executionTimeMs: gadgetEvent.executionTimeMs,
13621
- cost: gadgetEvent.cost,
13622
- logger: logger2,
13623
- subagentContext
13624
- };
13625
- chainObserverCall(
13626
- gadgetPromiseChains,
13627
- gadgetEvent.invocationId,
13628
- () => hooks.observers?.onGadgetExecutionComplete?.(context),
13629
- logger2,
13630
- "onGadgetExecutionComplete",
13631
- true
13632
- // Cleanup after completion
13633
- );
13568
+ /**
13569
+ * Check whether a gadget is marked as exclusive.
13570
+ */
13571
+ isExclusive(gadgetName) {
13572
+ const gadget = this.registry.get(gadgetName);
13573
+ return gadget?.exclusive === true;
13574
+ }
13575
+ /**
13576
+ * Get the current count of in-flight (actively executing) gadgets.
13577
+ */
13578
+ get inFlightCount() {
13579
+ return this.inFlightExecutions.size;
13580
+ }
13581
+ /**
13582
+ * Get the total count of actively executing gadgets across all gadget types.
13583
+ * Used to know when all work is truly complete.
13584
+ */
13585
+ getTotalActiveGadgetCount() {
13586
+ let total = 0;
13587
+ for (const count of this.activeCountByGadget.values()) {
13588
+ total += count;
13634
13589
  }
13635
- break;
13590
+ return total;
13636
13591
  }
13637
- case "gadget_error": {
13638
- if (subagentContext && hooks.observers?.onGadgetExecutionComplete) {
13639
- const gadgetEvent = event;
13640
- const gadgetNode = tree.getNode(event.nodeId);
13641
- const context = {
13592
+ /**
13593
+ * Check if there are any gadgets waiting in concurrency queues.
13594
+ */
13595
+ hasQueuedGadgets() {
13596
+ for (const queue of this.concurrencyQueue.values()) {
13597
+ if (queue.length > 0) return true;
13598
+ }
13599
+ return false;
13600
+ }
13601
+ /**
13602
+ * Get total count of queued gadgets across all queues.
13603
+ */
13604
+ getQueuedGadgetCount() {
13605
+ let count = 0;
13606
+ for (const queue of this.concurrencyQueue.values()) {
13607
+ count += queue.length;
13608
+ }
13609
+ return count;
13610
+ }
13611
+ /**
13612
+ * Check if there are exclusive gadgets waiting.
13613
+ */
13614
+ get hasExclusiveQueued() {
13615
+ return this.exclusiveQueue.length > 0;
13616
+ }
13617
+ /**
13618
+ * Drain and return all exclusive gadgets from the queue.
13619
+ * The caller is responsible for executing them.
13620
+ */
13621
+ drainExclusiveQueue() {
13622
+ const queue = this.exclusiveQueue;
13623
+ this.exclusiveQueue = [];
13624
+ return queue;
13625
+ }
13626
+ // ==========================================================================
13627
+ // State mutation
13628
+ // ==========================================================================
13629
+ /**
13630
+ * Track an in-flight gadget execution.
13631
+ * Increments the active count for the gadget name and registers the promise.
13632
+ *
13633
+ * @param invocationId - Unique ID for this execution
13634
+ * @param gadgetName - Name of the gadget being executed
13635
+ * @param promise - The execution promise to track
13636
+ */
13637
+ trackExecution(invocationId, gadgetName, promise) {
13638
+ const currentCount = this.activeCountByGadget.get(gadgetName) ?? 0;
13639
+ this.activeCountByGadget.set(gadgetName, currentCount + 1);
13640
+ this.inFlightExecutions.set(invocationId, promise);
13641
+ }
13642
+ /**
13643
+ * Called when a gadget execution completes.
13644
+ * Decrements active count and triggers queue processing if a slot opened up.
13645
+ *
13646
+ * @param gadgetName - Name of the gadget that completed
13647
+ * @returns The next queued gadget call for this gadget, if one was promoted, otherwise null
13648
+ */
13649
+ onComplete(gadgetName) {
13650
+ const newCount = (this.activeCountByGadget.get(gadgetName) ?? 1) - 1;
13651
+ this.activeCountByGadget.set(gadgetName, newCount);
13652
+ return this.promoteFromQueue(gadgetName);
13653
+ }
13654
+ /**
13655
+ * Queue a gadget for later execution due to a concurrency limit being reached.
13656
+ *
13657
+ * @param call - The gadget call to defer
13658
+ */
13659
+ queueForLater(call) {
13660
+ this.logger.debug("Gadget queued due to concurrency limit", {
13661
+ gadgetName: call.gadgetName,
13662
+ invocationId: call.invocationId,
13663
+ activeCount: this.activeCountByGadget.get(call.gadgetName) ?? 0,
13664
+ limit: this.getConcurrencyLimit(call.gadgetName)
13665
+ });
13666
+ const queue = this.concurrencyQueue.get(call.gadgetName) ?? [];
13667
+ queue.push(call);
13668
+ this.concurrencyQueue.set(call.gadgetName, queue);
13669
+ }
13670
+ /**
13671
+ * Queue a gadget for exclusive execution (after all in-flight complete).
13672
+ *
13673
+ * @param call - The exclusive gadget call to defer
13674
+ */
13675
+ queueExclusive(call) {
13676
+ this.logger.debug("Deferring exclusive gadget until in-flight gadgets complete", {
13677
+ gadgetName: call.gadgetName,
13678
+ invocationId: call.invocationId,
13679
+ inFlightCount: this.inFlightExecutions.size
13680
+ });
13681
+ this.exclusiveQueue.push(call);
13682
+ }
13683
+ /**
13684
+ * Clear the inFlightExecutions map after all promises have completed.
13685
+ * Called after waitForAll resolves.
13686
+ */
13687
+ clearInFlight() {
13688
+ this.inFlightExecutions.clear();
13689
+ }
13690
+ // ==========================================================================
13691
+ // Waiting
13692
+ // ==========================================================================
13693
+ /**
13694
+ * Wait for all currently in-flight gadget executions to complete.
13695
+ * Resolves when the Promise.all of all tracked promises resolves.
13696
+ *
13697
+ * Note: new executions may be started during waiting (from the queue).
13698
+ * Callers should loop until inFlightCount === 0 AND hasQueuedGadgets() === false.
13699
+ */
13700
+ async waitForAll() {
13701
+ if (this.inFlightExecutions.size === 0) return;
13702
+ await Promise.all(this.inFlightExecutions.values());
13703
+ }
13704
+ /**
13705
+ * Get a promise that resolves when all current in-flight executions complete.
13706
+ * Returns a resolved promise if no executions are in-flight.
13707
+ */
13708
+ getAllDonePromise() {
13709
+ if (this.inFlightExecutions.size === 0) {
13710
+ return Promise.resolve("done");
13711
+ }
13712
+ return Promise.all(this.inFlightExecutions.values()).then(() => "done");
13713
+ }
13714
+ // ==========================================================================
13715
+ // Private helpers
13716
+ // ==========================================================================
13717
+ /**
13718
+ * Check the concurrency queue for a gadget name and promote the next queued
13719
+ * call if a slot is available.
13720
+ *
13721
+ * @param gadgetName - The gadget name to check
13722
+ * @returns The promoted call, or null if queue is empty or limit still reached
13723
+ */
13724
+ promoteFromQueue(gadgetName) {
13725
+ const queue = this.concurrencyQueue.get(gadgetName);
13726
+ if (!queue || queue.length === 0) return null;
13727
+ const limit = this.getConcurrencyLimit(gadgetName);
13728
+ const activeCount = this.activeCountByGadget.get(gadgetName) ?? 0;
13729
+ if (limit === 0 || activeCount < limit) {
13730
+ const nextCall = queue.shift();
13731
+ this.logger.debug("Processing queued gadget", {
13732
+ gadgetName,
13733
+ invocationId: nextCall.invocationId,
13734
+ remainingInQueue: queue.length
13735
+ });
13736
+ return nextCall;
13737
+ }
13738
+ return null;
13739
+ }
13740
+ };
13741
+ }
13742
+ });
13743
+
13744
+ // src/agent/gadget-dependency-resolver.ts
13745
+ var GadgetDependencyResolver;
13746
+ var init_gadget_dependency_resolver = __esm({
13747
+ "src/agent/gadget-dependency-resolver.ts"() {
13748
+ "use strict";
13749
+ GadgetDependencyResolver = class {
13750
+ /** Gadgets waiting for their dependencies to complete */
13751
+ gadgetsAwaitingDependencies = /* @__PURE__ */ new Map();
13752
+ /** Completed gadget results, keyed by invocation ID */
13753
+ completedResults = /* @__PURE__ */ new Map();
13754
+ /** Invocation IDs of gadgets that have failed (error or skipped due to dependency) */
13755
+ failedInvocations = /* @__PURE__ */ new Set();
13756
+ /** Invocation IDs completed in previous iterations (read-only) */
13757
+ priorCompletedInvocations;
13758
+ /** Invocation IDs that failed in previous iterations (read-only) */
13759
+ priorFailedInvocations;
13760
+ constructor(options = {}) {
13761
+ this.priorCompletedInvocations = options.priorCompletedInvocations ?? /* @__PURE__ */ new Set();
13762
+ this.priorFailedInvocations = options.priorFailedInvocations ?? /* @__PURE__ */ new Set();
13763
+ }
13764
+ // ==========================================================================
13765
+ // State mutation
13766
+ // ==========================================================================
13767
+ /**
13768
+ * Queue a gadget call that is waiting for one or more dependencies to complete.
13769
+ * Call this when a gadget's dependencies are not yet all satisfied.
13770
+ *
13771
+ * @param call - The parsed gadget call to defer
13772
+ */
13773
+ addPending(call) {
13774
+ this.gadgetsAwaitingDependencies.set(call.invocationId, call);
13775
+ }
13776
+ /**
13777
+ * Record that a gadget completed successfully.
13778
+ * This may unblock other gadgets that depend on this invocation.
13779
+ *
13780
+ * Also marks as failed if the result contains an error.
13781
+ *
13782
+ * @param result - The completed gadget execution result
13783
+ */
13784
+ markComplete(result) {
13785
+ this.completedResults.set(result.invocationId, result);
13786
+ if (result.error) {
13787
+ this.failedInvocations.add(result.invocationId);
13788
+ }
13789
+ }
13790
+ /**
13791
+ * Mark an invocation ID as failed without recording a full result.
13792
+ * Use this for gadgets that are skipped before execution (e.g., limit exceeded,
13793
+ * self-referential dependency, dependency skip).
13794
+ *
13795
+ * @param invocationId - The invocation ID to mark as failed
13796
+ */
13797
+ markFailed(invocationId) {
13798
+ this.failedInvocations.add(invocationId);
13799
+ }
13800
+ /**
13801
+ * Remove a gadget from the pending queue (called just before execution).
13802
+ *
13803
+ * @param invocationId - The invocation ID to remove from pending
13804
+ */
13805
+ removePending(invocationId) {
13806
+ this.gadgetsAwaitingDependencies.delete(invocationId);
13807
+ }
13808
+ /**
13809
+ * Clear all remaining pending gadgets (e.g., after handling unresolvable deps).
13810
+ */
13811
+ clearPending() {
13812
+ this.gadgetsAwaitingDependencies.clear();
13813
+ }
13814
+ // ==========================================================================
13815
+ // Queries
13816
+ // ==========================================================================
13817
+ /**
13818
+ * Get all gadget calls currently waiting for dependencies.
13819
+ * Returns entries as [invocationId, call] pairs.
13820
+ */
13821
+ getPendingEntries() {
13822
+ return Array.from(this.gadgetsAwaitingDependencies.entries());
13823
+ }
13824
+ /**
13825
+ * Get the number of gadgets currently waiting for dependencies.
13826
+ */
13827
+ get pendingCount() {
13828
+ return this.gadgetsAwaitingDependencies.size;
13829
+ }
13830
+ /**
13831
+ * Check whether a given invocation ID has been completed successfully
13832
+ * (either in this iteration or a prior one).
13833
+ */
13834
+ isCompleted(invocationId) {
13835
+ return this.completedResults.has(invocationId) || this.priorCompletedInvocations.has(invocationId);
13836
+ }
13837
+ /**
13838
+ * Check whether a given invocation ID has failed
13839
+ * (either in this iteration or a prior one).
13840
+ */
13841
+ isFailed(invocationId) {
13842
+ return this.failedInvocations.has(invocationId) || this.priorFailedInvocations.has(invocationId);
13843
+ }
13844
+ /**
13845
+ * Get the execution result for a completed invocation, if available.
13846
+ * Only returns results from the current iteration; prior iterations
13847
+ * are tracked by ID only.
13848
+ */
13849
+ getCompletedResult(invocationId) {
13850
+ return this.completedResults.get(invocationId);
13851
+ }
13852
+ /**
13853
+ * Check if all dependencies for a gadget call are satisfied.
13854
+ * A dependency is satisfied if it completed in this or a prior iteration.
13855
+ *
13856
+ * @param call - The gadget call whose dependencies to check
13857
+ * @returns true if all deps are satisfied, false if any are still pending
13858
+ */
13859
+ isAllSatisfied(call) {
13860
+ return call.dependencies.every((dep) => this.isCompleted(dep));
13861
+ }
13862
+ /**
13863
+ * Find the first failed dependency for a gadget call, if any.
13864
+ * A dependency is considered failed if it failed in this or a prior iteration.
13865
+ *
13866
+ * @param call - The gadget call to check
13867
+ * @returns The invocation ID of the failed dependency, or undefined if none
13868
+ */
13869
+ getFailedDependency(call) {
13870
+ return call.dependencies.find((dep) => this.isFailed(dep));
13871
+ }
13872
+ /**
13873
+ * Separate the pending gadgets into two groups:
13874
+ * - Those ready to execute (all deps satisfied)
13875
+ * - Those ready to skip (at least one dep has failed)
13876
+ *
13877
+ * Gadgets that are neither ready nor skippable remain pending.
13878
+ *
13879
+ * @returns Object with `readyToExecute` and `readyToSkip` arrays
13880
+ */
13881
+ getReadyCalls() {
13882
+ const readyToExecute = [];
13883
+ const readyToSkip = [];
13884
+ for (const [_invocationId, call] of this.gadgetsAwaitingDependencies) {
13885
+ const failedDep = this.getFailedDependency(call);
13886
+ if (failedDep) {
13887
+ readyToSkip.push({ call, failedDep });
13888
+ continue;
13889
+ }
13890
+ if (this.isAllSatisfied(call)) {
13891
+ readyToExecute.push(call);
13892
+ }
13893
+ }
13894
+ return { readyToExecute, readyToSkip };
13895
+ }
13896
+ // ==========================================================================
13897
+ // Cross-iteration accessors (for Agent to accumulate state)
13898
+ // ==========================================================================
13899
+ /**
13900
+ * Get all invocation IDs that completed successfully in this iteration.
13901
+ * Used by Agent to accumulate completed IDs across iterations.
13902
+ */
13903
+ getCompletedInvocationIds() {
13904
+ return new Set(this.completedResults.keys());
13905
+ }
13906
+ /**
13907
+ * Get all invocation IDs that failed in this iteration.
13908
+ * Used by Agent to accumulate failed IDs across iterations.
13909
+ */
13910
+ getFailedInvocationIds() {
13911
+ return new Set(this.failedInvocations);
13912
+ }
13913
+ };
13914
+ }
13915
+ });
13916
+
13917
+ // src/agent/tree-hook-bridge.ts
13918
+ function findParentGadgetInvocationId(tree, nodeId) {
13919
+ let currentId = nodeId;
13920
+ while (currentId) {
13921
+ const node = tree.getNode(currentId);
13922
+ if (!node) break;
13923
+ currentId = node.parentId;
13924
+ if (!currentId) break;
13925
+ const parentNode = tree.getNode(currentId);
13926
+ if (parentNode?.type === "gadget") {
13927
+ return parentNode.invocationId;
13928
+ }
13929
+ }
13930
+ return void 0;
13931
+ }
13932
+ function getIterationFromTree(tree, nodeId) {
13933
+ let currentId = nodeId;
13934
+ while (currentId) {
13935
+ const node = tree.getNode(currentId);
13936
+ if (!node) break;
13937
+ if (node.type === "llm_call") {
13938
+ return node.iteration;
13939
+ }
13940
+ currentId = node.parentId;
13941
+ }
13942
+ return 0;
13943
+ }
13944
+ function buildSubagentContext(tree, event) {
13945
+ const parentGadgetInvocationId = findParentGadgetInvocationId(tree, event.nodeId);
13946
+ if (!parentGadgetInvocationId) {
13947
+ return void 0;
13948
+ }
13949
+ return {
13950
+ parentGadgetInvocationId,
13951
+ depth: event.depth
13952
+ };
13953
+ }
13954
+ function getSubagentContextForNode(tree, nodeId) {
13955
+ const node = tree.getNode(nodeId);
13956
+ if (!node) return void 0;
13957
+ const parentGadgetInvocationId = findParentGadgetInvocationId(tree, nodeId);
13958
+ if (!parentGadgetInvocationId) {
13959
+ return void 0;
13960
+ }
13961
+ return {
13962
+ parentGadgetInvocationId,
13963
+ depth: node.depth
13964
+ };
13965
+ }
13966
+ function chainObserverCall(chainMap, key, fn, logger2, eventType, cleanup = false) {
13967
+ const previousPromise = chainMap.get(key) ?? Promise.resolve();
13968
+ const newPromise = previousPromise.then(() => safeObserve(fn, logger2, eventType));
13969
+ chainMap.set(key, newPromise);
13970
+ if (cleanup) {
13971
+ newPromise.finally(() => chainMap.delete(key));
13972
+ }
13973
+ }
13974
+ function bridgeTreeToHooks(tree, hooks, logger2) {
13975
+ const gadgetPromiseChains = /* @__PURE__ */ new Map();
13976
+ const llmPromiseChains = /* @__PURE__ */ new Map();
13977
+ return tree.onAll((event) => {
13978
+ const subagentContext = buildSubagentContext(tree, event);
13979
+ switch (event.type) {
13980
+ // =================================================================
13981
+ // GADGET EVENTS - Bridged for subagent visibility
13982
+ // =================================================================
13983
+ // When a subagent executes gadgets, these events propagate through
13984
+ // the shared tree to the parent's hooks.
13985
+ // Only bridged for subagent events (depth > 0) to avoid double-calling
13986
+ // root agent events which are handled directly in stream-processor.ts
13987
+ case "gadget_start": {
13988
+ if (subagentContext && hooks.observers?.onGadgetExecutionStart) {
13989
+ const gadgetEvent = event;
13990
+ const gadgetNode = tree.getNode(event.nodeId);
13991
+ const context = {
13992
+ iteration: getIterationFromTree(tree, event.nodeId),
13993
+ gadgetName: gadgetEvent.name,
13994
+ invocationId: gadgetEvent.invocationId,
13995
+ parameters: gadgetNode?.parameters ?? {},
13996
+ logger: logger2,
13997
+ subagentContext
13998
+ };
13999
+ chainObserverCall(
14000
+ gadgetPromiseChains,
14001
+ gadgetEvent.invocationId,
14002
+ () => hooks.observers?.onGadgetExecutionStart?.(context),
14003
+ logger2,
14004
+ "onGadgetExecutionStart",
14005
+ false
14006
+ // Don't cleanup - wait for completion event
14007
+ );
14008
+ }
14009
+ break;
14010
+ }
14011
+ case "gadget_complete": {
14012
+ if (subagentContext && hooks.observers?.onGadgetExecutionComplete) {
14013
+ const gadgetEvent = event;
14014
+ const gadgetNode = tree.getNode(event.nodeId);
14015
+ const context = {
14016
+ iteration: getIterationFromTree(tree, event.nodeId),
14017
+ gadgetName: gadgetEvent.name,
14018
+ invocationId: gadgetEvent.invocationId,
14019
+ parameters: gadgetNode?.parameters ?? {},
14020
+ finalResult: gadgetEvent.result,
14021
+ executionTimeMs: gadgetEvent.executionTimeMs,
14022
+ cost: gadgetEvent.cost,
14023
+ logger: logger2,
14024
+ subagentContext
14025
+ };
14026
+ chainObserverCall(
14027
+ gadgetPromiseChains,
14028
+ gadgetEvent.invocationId,
14029
+ () => hooks.observers?.onGadgetExecutionComplete?.(context),
14030
+ logger2,
14031
+ "onGadgetExecutionComplete",
14032
+ true
14033
+ // Cleanup after completion
14034
+ );
14035
+ }
14036
+ break;
14037
+ }
14038
+ case "gadget_error": {
14039
+ if (subagentContext && hooks.observers?.onGadgetExecutionComplete) {
14040
+ const gadgetEvent = event;
14041
+ const gadgetNode = tree.getNode(event.nodeId);
14042
+ const context = {
13642
14043
  iteration: getIterationFromTree(tree, event.nodeId),
13643
14044
  gadgetName: gadgetEvent.name,
13644
14045
  invocationId: gadgetEvent.invocationId,
@@ -13785,6 +14186,82 @@ function bridgeTreeToHooks(tree, hooks, logger2) {
13785
14186
  var init_tree_hook_bridge = __esm({
13786
14187
  "src/agent/tree-hook-bridge.ts"() {
13787
14188
  "use strict";
14189
+ init_safe_observe();
14190
+ }
14191
+ });
14192
+
14193
+ // src/agent/observer-notifier.ts
14194
+ async function notifyGadgetSkipped(ctx) {
14195
+ const gadgetNode = ctx.tree?.getNodeByInvocationId(ctx.invocationId);
14196
+ const subagentContext = ctx.tree && gadgetNode ? getSubagentContextForNode(ctx.tree, gadgetNode.id) : void 0;
14197
+ const context = {
14198
+ iteration: ctx.iteration,
14199
+ gadgetName: ctx.gadgetName,
14200
+ invocationId: ctx.invocationId,
14201
+ parameters: ctx.parameters,
14202
+ failedDependency: ctx.failedDependency,
14203
+ failedDependencyError: ctx.failedDependencyError,
14204
+ logger: ctx.logger,
14205
+ subagentContext
14206
+ };
14207
+ if (ctx.hooks?.onGadgetSkipped) {
14208
+ const hookFn = ctx.hooks.onGadgetSkipped;
14209
+ await safeObserve(() => hookFn(context), ctx.logger);
14210
+ }
14211
+ if (ctx.parentObservers?.onGadgetSkipped) {
14212
+ const hookFn = ctx.parentObservers.onGadgetSkipped;
14213
+ await safeObserve(() => hookFn(context), ctx.logger);
14214
+ }
14215
+ }
14216
+ async function notifyGadgetStart(ctx) {
14217
+ const gadgetNode = ctx.tree?.getNodeByInvocationId(ctx.invocationId);
14218
+ const subagentContext = ctx.tree && gadgetNode ? getSubagentContextForNode(ctx.tree, gadgetNode.id) : void 0;
14219
+ const context = {
14220
+ iteration: ctx.iteration,
14221
+ gadgetName: ctx.gadgetName,
14222
+ invocationId: ctx.invocationId,
14223
+ parameters: ctx.parameters,
14224
+ logger: ctx.logger,
14225
+ subagentContext
14226
+ };
14227
+ if (ctx.hooks?.onGadgetExecutionStart) {
14228
+ const hookFn = ctx.hooks.onGadgetExecutionStart;
14229
+ await safeObserve(() => hookFn(context), ctx.logger);
14230
+ }
14231
+ if (ctx.parentObservers?.onGadgetExecutionStart) {
14232
+ const hookFn = ctx.parentObservers.onGadgetExecutionStart;
14233
+ await safeObserve(() => hookFn(context), ctx.logger);
14234
+ }
14235
+ }
14236
+ async function notifyGadgetComplete(ctx) {
14237
+ const gadgetNode = ctx.tree?.getNodeByInvocationId(ctx.invocationId);
14238
+ const subagentContext = ctx.tree && gadgetNode ? getSubagentContextForNode(ctx.tree, gadgetNode.id) : void 0;
14239
+ const context = {
14240
+ iteration: ctx.iteration,
14241
+ gadgetName: ctx.gadgetName,
14242
+ invocationId: ctx.invocationId,
14243
+ parameters: ctx.parameters,
14244
+ finalResult: ctx.finalResult,
14245
+ error: ctx.error,
14246
+ executionTimeMs: ctx.executionTimeMs,
14247
+ cost: ctx.cost,
14248
+ logger: ctx.logger,
14249
+ subagentContext
14250
+ };
14251
+ if (ctx.hooks?.onGadgetExecutionComplete) {
14252
+ const hookFn = ctx.hooks.onGadgetExecutionComplete;
14253
+ await safeObserve(() => hookFn(context), ctx.logger);
14254
+ }
14255
+ if (ctx.parentObservers?.onGadgetExecutionComplete) {
14256
+ const hookFn = ctx.parentObservers.onGadgetExecutionComplete;
14257
+ await safeObserve(() => hookFn(context), ctx.logger);
14258
+ }
14259
+ }
14260
+ var init_observer_notifier = __esm({
14261
+ "src/agent/observer-notifier.ts"() {
14262
+ "use strict";
14263
+ init_safe_observe();
14264
+ init_tree_hook_bridge();
13788
14265
  }
13789
14266
  });
13790
14267
 
@@ -13796,11 +14273,13 @@ var init_stream_processor = __esm({
13796
14273
  init_executor();
13797
14274
  init_parser();
13798
14275
  init_logger();
14276
+ init_gadget_concurrency_manager();
14277
+ init_gadget_dependency_resolver();
13799
14278
  init_hook_validators();
13800
- init_tree_hook_bridge();
14279
+ init_observer_notifier();
14280
+ init_safe_observe();
13801
14281
  StreamProcessor = class {
13802
14282
  iteration;
13803
- registry;
13804
14283
  hooks;
13805
14284
  logger;
13806
14285
  parser;
@@ -13812,33 +14291,12 @@ var init_stream_processor = __esm({
13812
14291
  // Gadget execution mode
13813
14292
  gadgetExecutionMode;
13814
14293
  responseText = "";
13815
- observerFailureCount = 0;
13816
- // Dependency tracking for gadget execution DAG
13817
- /** Gadgets waiting for their dependencies to complete */
13818
- gadgetsAwaitingDependencies = /* @__PURE__ */ new Map();
13819
- /** Completed gadget results, keyed by invocation ID */
13820
- completedResults = /* @__PURE__ */ new Map();
13821
- /** Invocation IDs of gadgets that have failed (error or skipped due to dependency) */
13822
- failedInvocations = /* @__PURE__ */ new Set();
13823
- /** Promises for independent gadgets currently executing (fire-and-forget) */
13824
- inFlightExecutions = /* @__PURE__ */ new Map();
14294
+ // Dependency resolution is delegated to GadgetDependencyResolver
14295
+ dependencyResolver;
14296
+ // Concurrency management is delegated to GadgetConcurrencyManager
14297
+ concurrencyManager;
13825
14298
  /** Queue of completed gadget results ready to be yielded (for real-time streaming) */
13826
14299
  completedResultsQueue = [];
13827
- // Concurrency limiting
13828
- /** Subagent configuration map for checking maxConcurrent limits */
13829
- subagentConfig;
13830
- /** Track active execution count per gadget name */
13831
- activeCountByGadget = /* @__PURE__ */ new Map();
13832
- /** Queue of gadgets waiting for a concurrency slot (per gadget name) */
13833
- concurrencyQueue = /* @__PURE__ */ new Map();
13834
- // Exclusive gadget support
13835
- /** Queue of exclusive gadgets deferred until in-flight gadgets complete */
13836
- exclusiveQueue = [];
13837
- // Cross-iteration dependency tracking
13838
- /** Invocation IDs completed in previous iterations (read-only reference from Agent) */
13839
- priorCompletedInvocations;
13840
- /** Invocation IDs that failed in previous iterations (read-only reference from Agent) */
13841
- priorFailedInvocations;
13842
14300
  // Parent observer hooks for subagent visibility
13843
14301
  parentObservers;
13844
14302
  // Gadget limiting per response
@@ -13847,16 +14305,21 @@ var init_stream_processor = __esm({
13847
14305
  limitExceeded = false;
13848
14306
  constructor(options) {
13849
14307
  this.iteration = options.iteration;
13850
- this.registry = options.registry;
13851
14308
  this.hooks = options.hooks ?? {};
13852
14309
  this.logger = options.logger ?? createLogger({ name: "llmist:stream-processor" });
13853
14310
  this.tree = options.tree;
13854
14311
  this.parentNodeId = options.parentNodeId ?? null;
13855
14312
  this.baseDepth = options.baseDepth ?? 0;
13856
14313
  this.gadgetExecutionMode = options.gadgetExecutionMode ?? "parallel";
13857
- this.priorCompletedInvocations = options.priorCompletedInvocations ?? /* @__PURE__ */ new Set();
13858
- this.priorFailedInvocations = options.priorFailedInvocations ?? /* @__PURE__ */ new Set();
13859
- this.subagentConfig = options.subagentConfig;
14314
+ this.dependencyResolver = new GadgetDependencyResolver({
14315
+ priorCompletedInvocations: options.priorCompletedInvocations,
14316
+ priorFailedInvocations: options.priorFailedInvocations
14317
+ });
14318
+ this.concurrencyManager = new GadgetConcurrencyManager({
14319
+ registry: options.registry,
14320
+ subagentConfig: options.subagentConfig,
14321
+ logger: this.logger.getSubLogger({ name: "concurrency" })
14322
+ });
13860
14323
  this.parentObservers = options.parentObservers;
13861
14324
  this.maxGadgetsPerResponse = options.maxGadgetsPerResponse ?? 0;
13862
14325
  this.parser = new GadgetCallParser({
@@ -14098,7 +14561,7 @@ var init_stream_processor = __esm({
14098
14561
  gadgetName: call.gadgetName,
14099
14562
  invocationId: call.invocationId
14100
14563
  });
14101
- this.failedInvocations.add(call.invocationId);
14564
+ this.dependencyResolver.markFailed(call.invocationId);
14102
14565
  const errorMessage = `Gadget "${call.invocationId}" cannot depend on itself (self-referential dependency)`;
14103
14566
  const skipEvent = {
14104
14567
  type: "gadget_skipped",
@@ -14109,39 +14572,21 @@ var init_stream_processor = __esm({
14109
14572
  failedDependencyError: errorMessage
14110
14573
  };
14111
14574
  yield skipEvent;
14112
- const skippedGadgetNode = this.tree?.getNodeByInvocationId(call.invocationId);
14113
- const skippedSubagentContext = this.tree && skippedGadgetNode ? getSubagentContextForNode(this.tree, skippedGadgetNode.id) : void 0;
14114
- if (this.hooks.observers?.onGadgetSkipped) {
14115
- const context = {
14116
- iteration: this.iteration,
14117
- gadgetName: call.gadgetName,
14118
- invocationId: call.invocationId,
14119
- parameters: call.parameters ?? {},
14120
- failedDependency: call.invocationId,
14121
- failedDependencyError: errorMessage,
14122
- logger: this.logger,
14123
- subagentContext: skippedSubagentContext
14124
- };
14125
- await this.safeObserve(() => this.hooks.observers.onGadgetSkipped(context));
14126
- }
14127
- if (this.parentObservers?.onGadgetSkipped) {
14128
- const context = {
14129
- iteration: this.iteration,
14130
- gadgetName: call.gadgetName,
14131
- invocationId: call.invocationId,
14132
- parameters: call.parameters ?? {},
14133
- failedDependency: call.invocationId,
14134
- failedDependencyError: errorMessage,
14135
- logger: this.logger,
14136
- subagentContext: skippedSubagentContext
14137
- };
14138
- await this.safeObserve(() => this.parentObservers.onGadgetSkipped(context));
14139
- }
14575
+ await notifyGadgetSkipped({
14576
+ tree: this.tree,
14577
+ hooks: this.hooks.observers,
14578
+ parentObservers: this.parentObservers,
14579
+ logger: this.logger,
14580
+ iteration: this.iteration,
14581
+ gadgetName: call.gadgetName,
14582
+ invocationId: call.invocationId,
14583
+ parameters: call.parameters ?? {},
14584
+ failedDependency: call.invocationId,
14585
+ failedDependencyError: errorMessage
14586
+ });
14140
14587
  return;
14141
14588
  }
14142
- const failedDep = call.dependencies.find(
14143
- (dep) => this.failedInvocations.has(dep) || this.priorFailedInvocations.has(dep)
14144
- );
14589
+ const failedDep = this.dependencyResolver.getFailedDependency(call);
14145
14590
  if (failedDep) {
14146
14591
  const skipEvents = await this.handleFailedDependency(call, failedDep);
14147
14592
  for (const evt of skipEvents) {
@@ -14149,16 +14594,16 @@ var init_stream_processor = __esm({
14149
14594
  }
14150
14595
  return;
14151
14596
  }
14152
- const unsatisfied = call.dependencies.filter(
14153
- (dep) => !this.completedResults.has(dep) && !this.priorCompletedInvocations.has(dep)
14154
- );
14155
- if (unsatisfied.length > 0) {
14597
+ if (!this.dependencyResolver.isAllSatisfied(call)) {
14598
+ const unsatisfied = call.dependencies.filter(
14599
+ (dep) => !this.dependencyResolver.isCompleted(dep)
14600
+ );
14156
14601
  this.logger.debug("Queueing gadget for later - waiting on dependencies", {
14157
14602
  gadgetName: call.gadgetName,
14158
14603
  invocationId: call.invocationId,
14159
14604
  waitingOn: unsatisfied
14160
14605
  });
14161
- this.gadgetsAwaitingDependencies.set(call.invocationId, call);
14606
+ this.dependencyResolver.addPending(call);
14162
14607
  return;
14163
14608
  }
14164
14609
  const limitCheckGen2 = this.checkGadgetLimitExceeded(call);
@@ -14187,28 +14632,12 @@ var init_stream_processor = __esm({
14187
14632
  if (limitResult.value === true) {
14188
14633
  return;
14189
14634
  }
14190
- const gadget = this.registry.get(call.gadgetName);
14191
- if (gadget?.exclusive && this.inFlightExecutions.size > 0) {
14192
- this.logger.debug("Deferring exclusive gadget until in-flight gadgets complete", {
14193
- gadgetName: call.gadgetName,
14194
- invocationId: call.invocationId,
14195
- inFlightCount: this.inFlightExecutions.size
14196
- });
14197
- this.exclusiveQueue.push(call);
14635
+ if (this.concurrencyManager.isExclusive(call.gadgetName) && this.concurrencyManager.inFlightCount > 0) {
14636
+ this.concurrencyManager.queueExclusive(call);
14198
14637
  return;
14199
14638
  }
14200
- const limit = this.getConcurrencyLimit(call.gadgetName);
14201
- const activeCount = this.activeCountByGadget.get(call.gadgetName) ?? 0;
14202
- if (limit > 0 && activeCount >= limit) {
14203
- this.logger.debug("Gadget queued due to concurrency limit", {
14204
- gadgetName: call.gadgetName,
14205
- invocationId: call.invocationId,
14206
- activeCount,
14207
- limit
14208
- });
14209
- const queue = this.concurrencyQueue.get(call.gadgetName) ?? [];
14210
- queue.push(call);
14211
- this.concurrencyQueue.set(call.gadgetName, queue);
14639
+ if (!this.concurrencyManager.canStart(call)) {
14640
+ this.concurrencyManager.queueForLater(call);
14212
14641
  return;
14213
14642
  }
14214
14643
  if (this.gadgetExecutionMode === "sequential") {
@@ -14219,57 +14648,19 @@ var init_stream_processor = __esm({
14219
14648
  this.startGadgetWithConcurrencyTracking(call);
14220
14649
  }
14221
14650
  }
14222
- /**
14223
- * Get the effective concurrency limit for a gadget.
14224
- * Uses "most restrictive wins" strategy: the lowest non-zero value from
14225
- * external config (SubagentConfig) and gadget's intrinsic maxConcurrent.
14226
- *
14227
- * This ensures gadget authors can set safety floors (e.g., maxConcurrent: 1
14228
- * for file writers) that cannot be weakened by external configuration.
14229
- *
14230
- * @returns 0 if unlimited, otherwise the effective limit
14231
- */
14232
- getConcurrencyLimit(gadgetName) {
14233
- const configLimit = this.subagentConfig?.[gadgetName]?.maxConcurrent;
14234
- const gadget = this.registry.get(gadgetName);
14235
- const gadgetLimit = gadget?.maxConcurrent;
14236
- const config = configLimit || Number.POSITIVE_INFINITY;
14237
- const intrinsic = gadgetLimit || Number.POSITIVE_INFINITY;
14238
- const effective = Math.min(config, intrinsic);
14239
- return effective === Number.POSITIVE_INFINITY ? 0 : effective;
14240
- }
14241
14651
  /**
14242
14652
  * Start a gadget execution with concurrency tracking.
14243
- * Increments active count, starts execution, and schedules queue processing on completion.
14653
+ * Delegates tracking to GadgetConcurrencyManager; schedules queue processing on completion.
14244
14654
  */
14245
14655
  startGadgetWithConcurrencyTracking(call) {
14246
14656
  const gadgetName = call.gadgetName;
14247
- const currentCount = this.activeCountByGadget.get(gadgetName) ?? 0;
14248
- this.activeCountByGadget.set(gadgetName, currentCount + 1);
14249
14657
  const executionPromise = this.executeGadgetAndCollect(call).finally(() => {
14250
- const newCount = (this.activeCountByGadget.get(gadgetName) ?? 1) - 1;
14251
- this.activeCountByGadget.set(gadgetName, newCount);
14252
- this.processQueuedGadget(gadgetName);
14658
+ const nextCall = this.concurrencyManager.onComplete(gadgetName);
14659
+ if (nextCall) {
14660
+ this.startGadgetWithConcurrencyTracking(nextCall);
14661
+ }
14253
14662
  });
14254
- this.inFlightExecutions.set(call.invocationId, executionPromise);
14255
- }
14256
- /**
14257
- * Process the next queued gadget for a given gadget name if a slot is available.
14258
- */
14259
- processQueuedGadget(gadgetName) {
14260
- const queue = this.concurrencyQueue.get(gadgetName);
14261
- if (!queue || queue.length === 0) return;
14262
- const limit = this.getConcurrencyLimit(gadgetName);
14263
- const activeCount = this.activeCountByGadget.get(gadgetName) ?? 0;
14264
- if (limit === 0 || activeCount < limit) {
14265
- const nextCall = queue.shift();
14266
- this.logger.debug("Processing queued gadget", {
14267
- gadgetName,
14268
- invocationId: nextCall.invocationId,
14269
- remainingInQueue: queue.length
14270
- });
14271
- this.startGadgetWithConcurrencyTracking(nextCall);
14272
- }
14663
+ this.concurrencyManager.trackExecution(call.invocationId, gadgetName, executionPromise);
14273
14664
  }
14274
14665
  /**
14275
14666
  * Execute a gadget through the full hook lifecycle and yield events.
@@ -14324,30 +14715,16 @@ var init_stream_processor = __esm({
14324
14715
  this.tree.startGadget(gadgetNode.id);
14325
14716
  }
14326
14717
  }
14327
- const gadgetStartNode = this.tree?.getNodeByInvocationId(call.invocationId);
14328
- const gadgetStartSubagentContext = this.tree && gadgetStartNode ? getSubagentContextForNode(this.tree, gadgetStartNode.id) : void 0;
14329
- if (this.hooks.observers?.onGadgetExecutionStart) {
14330
- const context = {
14331
- iteration: this.iteration,
14332
- gadgetName: call.gadgetName,
14333
- invocationId: call.invocationId,
14334
- parameters,
14335
- logger: this.logger,
14336
- subagentContext: gadgetStartSubagentContext
14337
- };
14338
- await this.safeObserve(() => this.hooks.observers.onGadgetExecutionStart(context));
14339
- }
14340
- if (this.parentObservers?.onGadgetExecutionStart) {
14341
- const context = {
14342
- iteration: this.iteration,
14343
- gadgetName: call.gadgetName,
14344
- invocationId: call.invocationId,
14345
- parameters,
14346
- logger: this.logger,
14347
- subagentContext: gadgetStartSubagentContext
14348
- };
14349
- await this.safeObserve(() => this.parentObservers.onGadgetExecutionStart(context));
14350
- }
14718
+ await notifyGadgetStart({
14719
+ tree: this.tree,
14720
+ hooks: this.hooks.observers,
14721
+ parentObservers: this.parentObservers,
14722
+ logger: this.logger,
14723
+ iteration: this.iteration,
14724
+ gadgetName: call.gadgetName,
14725
+ invocationId: call.invocationId,
14726
+ parameters
14727
+ });
14351
14728
  let result;
14352
14729
  if (shouldSkip) {
14353
14730
  result = {
@@ -14360,7 +14737,7 @@ var init_stream_processor = __esm({
14360
14737
  } else {
14361
14738
  result = await this.executor.execute(call);
14362
14739
  }
14363
- if (result.result && this.hooks.interceptors?.interceptGadgetResult) {
14740
+ if ((result.result || result.error) && this.hooks.interceptors?.interceptGadgetResult) {
14364
14741
  const context = {
14365
14742
  iteration: this.iteration,
14366
14743
  gadgetName: result.gadgetName,
@@ -14369,7 +14746,12 @@ var init_stream_processor = __esm({
14369
14746
  executionTimeMs: result.executionTimeMs,
14370
14747
  logger: this.logger
14371
14748
  };
14372
- result.result = this.hooks.interceptors.interceptGadgetResult(result.result, context);
14749
+ if (result.result) {
14750
+ result.result = this.hooks.interceptors.interceptGadgetResult(result.result, context);
14751
+ }
14752
+ if (result.error) {
14753
+ result.error = this.hooks.interceptors.interceptGadgetResult(result.error, context);
14754
+ }
14373
14755
  }
14374
14756
  if (this.hooks.controllers?.afterGadgetExecution) {
14375
14757
  const context = {
@@ -14416,42 +14798,21 @@ var init_stream_processor = __esm({
14416
14798
  }
14417
14799
  }
14418
14800
  }
14419
- const gadgetCompleteNode = this.tree?.getNodeByInvocationId(result.invocationId);
14420
- const gadgetCompleteSubagentContext = this.tree && gadgetCompleteNode ? getSubagentContextForNode(this.tree, gadgetCompleteNode.id) : void 0;
14421
- if (this.hooks.observers?.onGadgetExecutionComplete) {
14422
- const context = {
14423
- iteration: this.iteration,
14424
- gadgetName: result.gadgetName,
14425
- invocationId: result.invocationId,
14426
- parameters,
14427
- finalResult: result.result,
14428
- error: result.error,
14429
- executionTimeMs: result.executionTimeMs,
14430
- cost: result.cost,
14431
- logger: this.logger,
14432
- subagentContext: gadgetCompleteSubagentContext
14433
- };
14434
- await this.safeObserve(() => this.hooks.observers.onGadgetExecutionComplete(context));
14435
- }
14436
- if (this.parentObservers?.onGadgetExecutionComplete) {
14437
- const context = {
14438
- iteration: this.iteration,
14439
- gadgetName: result.gadgetName,
14440
- invocationId: result.invocationId,
14441
- parameters,
14442
- finalResult: result.result,
14443
- error: result.error,
14444
- executionTimeMs: result.executionTimeMs,
14445
- cost: result.cost,
14446
- logger: this.logger,
14447
- subagentContext: gadgetCompleteSubagentContext
14448
- };
14449
- await this.safeObserve(() => this.parentObservers.onGadgetExecutionComplete(context));
14450
- }
14451
- this.completedResults.set(result.invocationId, result);
14452
- if (result.error) {
14453
- this.failedInvocations.add(result.invocationId);
14454
- }
14801
+ await notifyGadgetComplete({
14802
+ tree: this.tree,
14803
+ hooks: this.hooks.observers,
14804
+ parentObservers: this.parentObservers,
14805
+ logger: this.logger,
14806
+ iteration: this.iteration,
14807
+ gadgetName: result.gadgetName,
14808
+ invocationId: result.invocationId,
14809
+ parameters,
14810
+ finalResult: result.result,
14811
+ error: result.error,
14812
+ executionTimeMs: result.executionTimeMs,
14813
+ cost: result.cost
14814
+ });
14815
+ this.dependencyResolver.markComplete(result);
14455
14816
  yield { type: "gadget_result", result };
14456
14817
  }
14457
14818
  /**
@@ -14483,77 +14844,45 @@ var init_stream_processor = __esm({
14483
14844
  * Clears the inFlightExecutions map after all gadgets complete.
14484
14845
  */
14485
14846
  async *waitForInFlightExecutions() {
14486
- if (this.inFlightExecutions.size === 0 && !this.hasQueuedGadgets() && this.exclusiveQueue.length === 0) {
14847
+ if (this.concurrencyManager.inFlightCount === 0 && !this.concurrencyManager.hasQueuedGadgets() && !this.concurrencyManager.hasExclusiveQueued) {
14487
14848
  return;
14488
14849
  }
14489
14850
  this.logger.debug("Waiting for in-flight gadget executions", {
14490
- count: this.inFlightExecutions.size,
14491
- invocationIds: Array.from(this.inFlightExecutions.keys()),
14492
- queuedCount: this.getQueuedGadgetCount()
14851
+ count: this.concurrencyManager.inFlightCount,
14852
+ queuedCount: this.concurrencyManager.getQueuedGadgetCount()
14493
14853
  });
14494
14854
  const POLL_INTERVAL_MS = 100;
14495
- while (this.inFlightExecutions.size > 0 || this.hasQueuedGadgets()) {
14496
- const allDone = this.inFlightExecutions.size > 0 ? Promise.all(this.inFlightExecutions.values()).then(() => "done") : Promise.resolve("done");
14855
+ while (this.concurrencyManager.inFlightCount > 0 || this.concurrencyManager.hasQueuedGadgets()) {
14856
+ const allDone = this.concurrencyManager.getAllDonePromise();
14497
14857
  const result = await Promise.race([
14498
14858
  allDone,
14499
14859
  new Promise((resolve2) => setTimeout(() => resolve2("poll"), POLL_INTERVAL_MS))
14500
14860
  ]);
14501
14861
  yield* this.drainCompletedResults();
14502
- if (result === "done" && this.getTotalActiveGadgetCount() === 0 && !this.hasQueuedGadgets()) {
14862
+ if (result === "done" && this.concurrencyManager.getTotalActiveGadgetCount() === 0 && !this.concurrencyManager.hasQueuedGadgets()) {
14503
14863
  break;
14504
14864
  }
14505
14865
  }
14506
- this.inFlightExecutions.clear();
14507
- if (this.exclusiveQueue.length > 0) {
14866
+ this.concurrencyManager.clearInFlight();
14867
+ if (this.concurrencyManager.hasExclusiveQueued) {
14868
+ const exclusiveQueue = this.concurrencyManager.drainExclusiveQueue();
14508
14869
  this.logger.debug("Processing deferred exclusive gadgets", {
14509
- count: this.exclusiveQueue.length
14870
+ count: exclusiveQueue.length
14510
14871
  });
14511
- const queue = this.exclusiveQueue;
14512
- this.exclusiveQueue = [];
14513
- for (const call of queue) {
14872
+ for (const call of exclusiveQueue) {
14514
14873
  for await (const evt of this.executeGadgetGenerator(call)) {
14515
14874
  yield evt;
14516
14875
  }
14517
14876
  }
14518
14877
  }
14519
14878
  }
14520
- /**
14521
- * Check if there are any gadgets waiting in concurrency queues.
14522
- */
14523
- hasQueuedGadgets() {
14524
- for (const queue of this.concurrencyQueue.values()) {
14525
- if (queue.length > 0) return true;
14526
- }
14527
- return false;
14528
- }
14529
- /**
14530
- * Get total count of queued gadgets across all queues.
14531
- */
14532
- getQueuedGadgetCount() {
14533
- let count = 0;
14534
- for (const queue of this.concurrencyQueue.values()) {
14535
- count += queue.length;
14536
- }
14537
- return count;
14538
- }
14539
- /**
14540
- * Get total count of actively executing gadgets across all types.
14541
- * Used to know when all work is truly complete (not just when allDone resolves).
14542
- */
14543
- getTotalActiveGadgetCount() {
14544
- let total = 0;
14545
- for (const count of this.activeCountByGadget.values()) {
14546
- total += count;
14547
- }
14548
- return total;
14549
- }
14550
14879
  /**
14551
14880
  * Handle a gadget that cannot execute because a dependency failed.
14552
14881
  * Calls the onDependencySkipped controller to allow customization.
14553
14882
  */
14554
14883
  async handleFailedDependency(call, failedDep) {
14555
14884
  const events = [];
14556
- const depResult = this.completedResults.get(failedDep);
14885
+ const depResult = this.dependencyResolver.getCompletedResult(failedDep);
14557
14886
  const depError = depResult?.error ?? "Dependency failed";
14558
14887
  let action = { action: "skip" };
14559
14888
  if (this.hooks.controllers?.onDependencySkipped) {
@@ -14569,7 +14898,7 @@ var init_stream_processor = __esm({
14569
14898
  action = await this.hooks.controllers.onDependencySkipped(context);
14570
14899
  }
14571
14900
  if (action.action === "skip") {
14572
- this.failedInvocations.add(call.invocationId);
14901
+ this.dependencyResolver.markFailed(call.invocationId);
14573
14902
  if (this.tree) {
14574
14903
  const gadgetNode = this.tree.getNodeByInvocationId(call.invocationId);
14575
14904
  if (gadgetNode) {
@@ -14585,34 +14914,18 @@ var init_stream_processor = __esm({
14585
14914
  failedDependencyError: depError
14586
14915
  };
14587
14916
  events.push(skipEvent);
14588
- const gadgetNodeForSkip = this.tree?.getNodeByInvocationId(call.invocationId);
14589
- const skipSubagentContext = this.tree && gadgetNodeForSkip ? getSubagentContextForNode(this.tree, gadgetNodeForSkip.id) : void 0;
14590
- if (this.hooks.observers?.onGadgetSkipped) {
14591
- const context = {
14592
- iteration: this.iteration,
14593
- gadgetName: call.gadgetName,
14594
- invocationId: call.invocationId,
14595
- parameters: call.parameters ?? {},
14596
- failedDependency: failedDep,
14597
- failedDependencyError: depError,
14598
- logger: this.logger,
14599
- subagentContext: skipSubagentContext
14600
- };
14601
- await this.safeObserve(() => this.hooks.observers.onGadgetSkipped(context));
14602
- }
14603
- if (this.parentObservers?.onGadgetSkipped) {
14604
- const context = {
14605
- iteration: this.iteration,
14606
- gadgetName: call.gadgetName,
14607
- invocationId: call.invocationId,
14608
- parameters: call.parameters ?? {},
14609
- failedDependency: failedDep,
14610
- failedDependencyError: depError,
14611
- logger: this.logger,
14612
- subagentContext: skipSubagentContext
14613
- };
14614
- await this.safeObserve(() => this.parentObservers.onGadgetSkipped(context));
14615
- }
14917
+ await notifyGadgetSkipped({
14918
+ tree: this.tree,
14919
+ hooks: this.hooks.observers,
14920
+ parentObservers: this.parentObservers,
14921
+ logger: this.logger,
14922
+ iteration: this.iteration,
14923
+ gadgetName: call.gadgetName,
14924
+ invocationId: call.invocationId,
14925
+ parameters: call.parameters ?? {},
14926
+ failedDependency: failedDep,
14927
+ failedDependencyError: depError
14928
+ });
14616
14929
  this.logger.info("Gadget skipped due to failed dependency", {
14617
14930
  gadgetName: call.gadgetName,
14618
14931
  invocationId: call.invocationId,
@@ -14635,7 +14948,7 @@ var init_stream_processor = __esm({
14635
14948
  result: action.fallbackResult,
14636
14949
  executionTimeMs: 0
14637
14950
  };
14638
- this.completedResults.set(call.invocationId, fallbackResult);
14951
+ this.dependencyResolver.markComplete(fallbackResult);
14639
14952
  events.push({ type: "gadget_result", result: fallbackResult });
14640
14953
  this.logger.info("Using fallback result for gadget with failed dependency", {
14641
14954
  gadgetName: call.gadgetName,
@@ -14666,7 +14979,7 @@ var init_stream_processor = __esm({
14666
14979
  limit: this.maxGadgetsPerResponse,
14667
14980
  currentCount: this.gadgetStartedCount
14668
14981
  });
14669
- this.failedInvocations.add(call.invocationId);
14982
+ this.dependencyResolver.markFailed(call.invocationId);
14670
14983
  if (this.tree) {
14671
14984
  const gadgetNode = this.tree.getNodeByInvocationId(call.invocationId);
14672
14985
  if (gadgetNode) {
@@ -14687,34 +15000,18 @@ var init_stream_processor = __esm({
14687
15000
  failedDependencyError: errorMessage
14688
15001
  };
14689
15002
  yield skipEvent;
14690
- const limitSkipNode = this.tree?.getNodeByInvocationId(call.invocationId);
14691
- const limitSkipSubagentContext = this.tree && limitSkipNode ? getSubagentContextForNode(this.tree, limitSkipNode.id) : void 0;
14692
- if (this.hooks.observers?.onGadgetSkipped) {
14693
- const context = {
14694
- iteration: this.iteration,
14695
- gadgetName: call.gadgetName,
14696
- invocationId: call.invocationId,
14697
- parameters: call.parameters ?? {},
14698
- failedDependency: "maxGadgetsPerResponse",
14699
- failedDependencyError: errorMessage,
14700
- logger: this.logger,
14701
- subagentContext: limitSkipSubagentContext
14702
- };
14703
- await this.safeObserve(() => this.hooks.observers.onGadgetSkipped(context));
14704
- }
14705
- if (this.parentObservers?.onGadgetSkipped) {
14706
- const context = {
14707
- iteration: this.iteration,
14708
- gadgetName: call.gadgetName,
14709
- invocationId: call.invocationId,
14710
- parameters: call.parameters ?? {},
14711
- failedDependency: "maxGadgetsPerResponse",
14712
- failedDependencyError: errorMessage,
14713
- logger: this.logger,
14714
- subagentContext: limitSkipSubagentContext
14715
- };
14716
- await this.safeObserve(() => this.parentObservers.onGadgetSkipped(context));
14717
- }
15003
+ await notifyGadgetSkipped({
15004
+ tree: this.tree,
15005
+ hooks: this.hooks.observers,
15006
+ parentObservers: this.parentObservers,
15007
+ logger: this.logger,
15008
+ iteration: this.iteration,
15009
+ gadgetName: call.gadgetName,
15010
+ invocationId: call.invocationId,
15011
+ parameters: call.parameters ?? {},
15012
+ failedDependency: "maxGadgetsPerResponse",
15013
+ failedDependencyError: errorMessage
15014
+ });
14718
15015
  return true;
14719
15016
  }
14720
15017
  this.gadgetStartedCount++;
@@ -14732,27 +15029,11 @@ var init_stream_processor = __esm({
14732
15029
  return;
14733
15030
  }
14734
15031
  let progress = true;
14735
- while (progress && this.gadgetsAwaitingDependencies.size > 0) {
15032
+ while (progress && this.dependencyResolver.pendingCount > 0) {
14736
15033
  progress = false;
14737
- const readyToExecute = [];
14738
- const readyToSkip = [];
14739
- for (const [_invocationId, call] of this.gadgetsAwaitingDependencies) {
14740
- const failedDep = call.dependencies.find(
14741
- (dep) => this.failedInvocations.has(dep) || this.priorFailedInvocations.has(dep)
14742
- );
14743
- if (failedDep) {
14744
- readyToSkip.push({ call, failedDep });
14745
- continue;
14746
- }
14747
- const allSatisfied = call.dependencies.every(
14748
- (dep) => this.completedResults.has(dep) || this.priorCompletedInvocations.has(dep)
14749
- );
14750
- if (allSatisfied) {
14751
- readyToExecute.push(call);
14752
- }
14753
- }
15034
+ const { readyToExecute, readyToSkip } = this.dependencyResolver.getReadyCalls();
14754
15035
  for (const { call, failedDep } of readyToSkip) {
14755
- this.gadgetsAwaitingDependencies.delete(call.invocationId);
15036
+ this.dependencyResolver.removePending(call.invocationId);
14756
15037
  const skipEvents = await this.handleFailedDependency(call, failedDep);
14757
15038
  for (const evt of skipEvents) {
14758
15039
  yield evt;
@@ -14761,7 +15042,7 @@ var init_stream_processor = __esm({
14761
15042
  }
14762
15043
  if (readyToExecute.length > 0) {
14763
15044
  for (const call of readyToExecute) {
14764
- this.gadgetsAwaitingDependencies.delete(call.invocationId);
15045
+ this.dependencyResolver.removePending(call.invocationId);
14765
15046
  }
14766
15047
  if (this.gadgetExecutionMode === "sequential") {
14767
15048
  this.logger.debug("Executing ready gadgets sequentially", {
@@ -14814,11 +15095,12 @@ var init_stream_processor = __esm({
14814
15095
  progress = true;
14815
15096
  }
14816
15097
  }
14817
- if (this.gadgetsAwaitingDependencies.size > 0) {
14818
- const pendingIds = new Set(this.gadgetsAwaitingDependencies.keys());
14819
- for (const [invocationId, call] of this.gadgetsAwaitingDependencies) {
15098
+ if (this.dependencyResolver.pendingCount > 0) {
15099
+ const pendingEntries = this.dependencyResolver.getPendingEntries();
15100
+ const pendingIds = new Set(pendingEntries.map(([id]) => id));
15101
+ for (const [invocationId, call] of pendingEntries) {
14820
15102
  const missingDeps = call.dependencies.filter(
14821
- (dep) => !this.completedResults.has(dep) && !this.priorCompletedInvocations.has(dep)
15103
+ (dep) => !this.dependencyResolver.isCompleted(dep)
14822
15104
  );
14823
15105
  const circularDeps = missingDeps.filter((dep) => pendingIds.has(dep));
14824
15106
  const trulyMissingDeps = missingDeps.filter((dep) => !pendingIds.has(dep));
@@ -14838,7 +15120,7 @@ var init_stream_processor = __esm({
14838
15120
  circularDependencies: circularDeps,
14839
15121
  missingDependencies: trulyMissingDeps
14840
15122
  });
14841
- this.failedInvocations.add(invocationId);
15123
+ this.dependencyResolver.markFailed(invocationId);
14842
15124
  const skipEvent = {
14843
15125
  type: "gadget_skipped",
14844
15126
  gadgetName: call.gadgetName,
@@ -14848,51 +15130,20 @@ var init_stream_processor = __esm({
14848
15130
  failedDependencyError: errorMessage
14849
15131
  };
14850
15132
  yield skipEvent;
14851
- const gadgetNodeForTimeout = this.tree?.getNodeByInvocationId(invocationId);
14852
- const timeoutSubagentContext = this.tree && gadgetNodeForTimeout ? getSubagentContextForNode(this.tree, gadgetNodeForTimeout.id) : void 0;
14853
- if (this.hooks.observers?.onGadgetSkipped) {
14854
- const context = {
14855
- iteration: this.iteration,
14856
- gadgetName: call.gadgetName,
14857
- invocationId,
14858
- parameters: call.parameters ?? {},
14859
- failedDependency: missingDeps[0],
14860
- failedDependencyError: errorMessage,
14861
- logger: this.logger,
14862
- subagentContext: timeoutSubagentContext
14863
- };
14864
- await this.safeObserve(() => this.hooks.observers.onGadgetSkipped(context));
14865
- }
14866
- if (this.parentObservers?.onGadgetSkipped) {
14867
- const context = {
14868
- iteration: this.iteration,
14869
- gadgetName: call.gadgetName,
14870
- invocationId,
14871
- parameters: call.parameters ?? {},
14872
- failedDependency: missingDeps[0],
14873
- failedDependencyError: errorMessage,
14874
- logger: this.logger,
14875
- subagentContext: timeoutSubagentContext
14876
- };
14877
- await this.safeObserve(() => this.parentObservers.onGadgetSkipped(context));
14878
- }
15133
+ await notifyGadgetSkipped({
15134
+ tree: this.tree,
15135
+ hooks: this.hooks.observers,
15136
+ parentObservers: this.parentObservers,
15137
+ logger: this.logger,
15138
+ iteration: this.iteration,
15139
+ gadgetName: call.gadgetName,
15140
+ invocationId,
15141
+ parameters: call.parameters ?? {},
15142
+ failedDependency: missingDeps[0],
15143
+ failedDependencyError: errorMessage
15144
+ });
14879
15145
  }
14880
- this.gadgetsAwaitingDependencies.clear();
14881
- }
14882
- }
14883
- /**
14884
- * Safely execute an observer, catching and logging any errors.
14885
- * Observers are non-critical, so errors are logged but don't crash the system.
14886
- */
14887
- async safeObserve(fn) {
14888
- try {
14889
- await fn();
14890
- } catch (error) {
14891
- this.observerFailureCount++;
14892
- this.logger.error("Observer threw error (ignoring)", {
14893
- error: error instanceof Error ? error.message : String(error),
14894
- failureCount: this.observerFailureCount
14895
- });
15146
+ this.dependencyResolver.clearPending();
14896
15147
  }
14897
15148
  }
14898
15149
  /**
@@ -14901,7 +15152,7 @@ var init_stream_processor = __esm({
14901
15152
  */
14902
15153
  async runObserversInParallel(observers) {
14903
15154
  if (observers.length === 0) return;
14904
- await Promise.allSettled(observers.map((observer) => this.safeObserve(observer)));
15155
+ await Promise.allSettled(observers.map((observer) => safeObserve(observer, this.logger)));
14905
15156
  }
14906
15157
  // ==========================================================================
14907
15158
  // Public accessors for cross-iteration dependency tracking
@@ -14911,14 +15162,14 @@ var init_stream_processor = __esm({
14911
15162
  * Used by Agent to accumulate completed IDs across iterations.
14912
15163
  */
14913
15164
  getCompletedInvocationIds() {
14914
- return new Set(this.completedResults.keys());
15165
+ return this.dependencyResolver.getCompletedInvocationIds();
14915
15166
  }
14916
15167
  /**
14917
15168
  * Get all invocation IDs that failed in this iteration.
14918
15169
  * Used by Agent to accumulate failed IDs across iterations.
14919
15170
  */
14920
15171
  getFailedInvocationIds() {
14921
- return new Set(this.failedInvocations);
15172
+ return this.dependencyResolver.getFailedInvocationIds();
14922
15173
  }
14923
15174
  };
14924
15175
  }
@@ -14945,6 +15196,7 @@ var init_agent = __esm({
14945
15196
  init_event_handlers();
14946
15197
  init_gadget_output_store();
14947
15198
  init_hook_validators();
15199
+ init_safe_observe();
14948
15200
  init_stream_processor();
14949
15201
  init_tree_hook_bridge();
14950
15202
  Agent = class {
@@ -15433,7 +15685,7 @@ var init_agent = __esm({
15433
15685
  }
15434
15686
  );
15435
15687
  this.retryConfig.onRetry?.(error, streamAttempt);
15436
- await this.safeObserve(async () => {
15688
+ await safeObserve(async () => {
15437
15689
  if (this.hooks.observers?.onRetryAttempt) {
15438
15690
  const subagentContext = getSubagentContextForNode(this.tree, currentLLMNodeId);
15439
15691
  const hookContext = {
@@ -15447,7 +15699,7 @@ var init_agent = __esm({
15447
15699
  };
15448
15700
  await this.hooks.observers.onRetryAttempt(hookContext);
15449
15701
  }
15450
- });
15702
+ }, this.logger);
15451
15703
  await this.sleep(finalDelay);
15452
15704
  streamMetadata = null;
15453
15705
  gadgetCallCount = 0;
@@ -15477,7 +15729,7 @@ var init_agent = __esm({
15477
15729
  this.logger.silly("LLM response details", {
15478
15730
  rawResponse: result.rawResponse
15479
15731
  });
15480
- await this.safeObserve(async () => {
15732
+ await safeObserve(async () => {
15481
15733
  if (this.hooks.observers?.onLLMCallComplete) {
15482
15734
  const subagentContext = getSubagentContextForNode(this.tree, currentLLMNodeId);
15483
15735
  const context = {
@@ -15493,7 +15745,7 @@ var init_agent = __esm({
15493
15745
  };
15494
15746
  await this.hooks.observers.onLLMCallComplete(context);
15495
15747
  }
15496
- });
15748
+ }, this.logger);
15497
15749
  this.completeLLMCallInTree(currentLLMNodeId, result);
15498
15750
  const finalMessage = await this.processAfterLLMCallController(
15499
15751
  currentIteration,
@@ -15527,7 +15779,7 @@ var init_agent = __esm({
15527
15779
  }
15528
15780
  } catch (error) {
15529
15781
  const errorHandled = await this.handleLLMError(error, currentIteration);
15530
- await this.safeObserve(async () => {
15782
+ await safeObserve(async () => {
15531
15783
  if (this.hooks.observers?.onLLMCallError) {
15532
15784
  const options = llmOptions ?? {
15533
15785
  model: this.model,
@@ -15546,7 +15798,7 @@ var init_agent = __esm({
15546
15798
  };
15547
15799
  await this.hooks.observers.onLLMCallError(context);
15548
15800
  }
15549
- });
15801
+ }, this.logger);
15550
15802
  if (!errorHandled) {
15551
15803
  throw error;
15552
15804
  }
@@ -15573,7 +15825,7 @@ var init_agent = __esm({
15573
15825
  if (currentLLMNodeId) {
15574
15826
  const node = this.tree.getNode(currentLLMNodeId);
15575
15827
  if (node && node.type === "llm_call" && !node.completedAt) {
15576
- await this.safeObserve(async () => {
15828
+ await safeObserve(async () => {
15577
15829
  if (this.hooks.observers?.onLLMCallComplete) {
15578
15830
  const subagentContext = getSubagentContextForNode(this.tree, currentLLMNodeId);
15579
15831
  const context = {
@@ -15595,7 +15847,7 @@ var init_agent = __esm({
15595
15847
  };
15596
15848
  await this.hooks.observers.onLLMCallComplete(context);
15597
15849
  }
15598
- });
15850
+ }, this.logger);
15599
15851
  this.tree.completeLLMCall(currentLLMNodeId, {
15600
15852
  finishReason: "interrupted"
15601
15853
  });
@@ -15615,7 +15867,7 @@ var init_agent = __esm({
15615
15867
  const throttleDelay = this.rateLimitTracker.getRequiredDelayMs();
15616
15868
  if (throttleDelay > 0) {
15617
15869
  this.logger.debug("Rate limit throttling", { delayMs: throttleDelay });
15618
- await this.safeObserve(async () => {
15870
+ await safeObserve(async () => {
15619
15871
  if (this.hooks.observers?.onRateLimitThrottle) {
15620
15872
  const subagentContext = getSubagentContextForNode(this.tree, llmNodeId);
15621
15873
  const context = {
@@ -15627,7 +15879,7 @@ var init_agent = __esm({
15627
15879
  };
15628
15880
  await this.hooks.observers.onRateLimitThrottle(context);
15629
15881
  }
15630
- });
15882
+ }, this.logger);
15631
15883
  await this.sleep(throttleDelay);
15632
15884
  }
15633
15885
  this.rateLimitTracker.reserveRequest();
@@ -15690,18 +15942,6 @@ var init_agent = __esm({
15690
15942
  }
15691
15943
  return true;
15692
15944
  }
15693
- /**
15694
- * Safely execute an observer, catching and logging any errors.
15695
- */
15696
- async safeObserve(fn) {
15697
- try {
15698
- await fn();
15699
- } catch (error) {
15700
- this.logger.error("Observer threw error (ignoring)", {
15701
- error: error instanceof Error ? error.message : String(error)
15702
- });
15703
- }
15704
- }
15705
15945
  /**
15706
15946
  * Resolve max tokens from model catalog.
15707
15947
  */
@@ -15770,7 +16010,7 @@ var init_agent = __esm({
15770
16010
  iteration,
15771
16011
  reason: this.signal.reason
15772
16012
  });
15773
- await this.safeObserve(async () => {
16013
+ await safeObserve(async () => {
15774
16014
  if (this.hooks.observers?.onAbort) {
15775
16015
  const context = {
15776
16016
  iteration,
@@ -15779,7 +16019,7 @@ var init_agent = __esm({
15779
16019
  };
15780
16020
  await this.hooks.observers.onAbort(context);
15781
16021
  }
15782
- });
16022
+ }, this.logger);
15783
16023
  return true;
15784
16024
  }
15785
16025
  /**
@@ -15798,7 +16038,7 @@ var init_agent = __esm({
15798
16038
  tokensBefore: compactionEvent.tokensBefore,
15799
16039
  tokensAfter: compactionEvent.tokensAfter
15800
16040
  });
15801
- await this.safeObserve(async () => {
16041
+ await safeObserve(async () => {
15802
16042
  if (this.hooks.observers?.onCompaction) {
15803
16043
  await this.hooks.observers.onCompaction({
15804
16044
  iteration,
@@ -15808,7 +16048,7 @@ var init_agent = __esm({
15808
16048
  logger: this.logger
15809
16049
  });
15810
16050
  }
15811
- });
16051
+ }, this.logger);
15812
16052
  return { type: "compaction", event: compactionEvent };
15813
16053
  }
15814
16054
  /**
@@ -15861,7 +16101,7 @@ var init_agent = __esm({
15861
16101
  parentId: this.parentNodeId,
15862
16102
  request: llmOptions.messages
15863
16103
  });
15864
- await this.safeObserve(async () => {
16104
+ await safeObserve(async () => {
15865
16105
  if (this.hooks.observers?.onLLMCallStart) {
15866
16106
  const subagentContext = getSubagentContextForNode(this.tree, llmNode.id);
15867
16107
  const context = {
@@ -15872,7 +16112,7 @@ var init_agent = __esm({
15872
16112
  };
15873
16113
  await this.hooks.observers.onLLMCallStart(context);
15874
16114
  }
15875
- });
16115
+ }, this.logger);
15876
16116
  if (this.hooks.controllers?.beforeLLMCall) {
15877
16117
  const context = {
15878
16118
  iteration,
@@ -15895,7 +16135,7 @@ var init_agent = __esm({
15895
16135
  llmOptions = { ...llmOptions, ...action.modifiedOptions };
15896
16136
  }
15897
16137
  }
15898
- await this.safeObserve(async () => {
16138
+ await safeObserve(async () => {
15899
16139
  if (this.hooks.observers?.onLLMCallReady) {
15900
16140
  const subagentContext = getSubagentContextForNode(this.tree, llmNode.id);
15901
16141
  const context = {
@@ -15909,7 +16149,7 @@ var init_agent = __esm({
15909
16149
  };
15910
16150
  await this.hooks.observers.onLLMCallReady(context);
15911
16151
  }
15912
- });
16152
+ }, this.logger);
15913
16153
  return { options: llmOptions, llmNodeId: llmNode.id };
15914
16154
  }
15915
16155
  /**