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.cjs CHANGED
@@ -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({
@@ -4432,6 +4449,8 @@ var init_hook_presets = __esm({
4432
4449
  /**
4433
4450
  * Tracks cumulative token usage across all LLM calls.
4434
4451
  *
4452
+ * @public
4453
+ *
4435
4454
  * **Output:**
4436
4455
  * - Per-call token count with 📊 emoji
4437
4456
  * - Cumulative total across all calls
@@ -4694,6 +4713,8 @@ var init_hook_presets = __esm({
4694
4713
  /**
4695
4714
  * Logs detailed error information for debugging and troubleshooting.
4696
4715
  *
4716
+ * @public
4717
+ *
4697
4718
  * **Output:**
4698
4719
  * - LLM errors with ❌ emoji, including model and recovery status
4699
4720
  * - Gadget errors with full context (parameters, error message)
@@ -4772,6 +4793,8 @@ var init_hook_presets = __esm({
4772
4793
  /**
4773
4794
  * Tracks context compaction events.
4774
4795
  *
4796
+ * @public
4797
+ *
4775
4798
  * **Output:**
4776
4799
  * - Compaction events with 🗜️ emoji
4777
4800
  * - Strategy name, tokens before/after, and savings
@@ -4885,6 +4908,8 @@ var init_hook_presets = __esm({
4885
4908
  /**
4886
4909
  * Returns empty hook configuration for clean output without any logging.
4887
4910
  *
4911
+ * @public
4912
+ *
4888
4913
  * **Output:**
4889
4914
  * - None. Returns {} (empty object).
4890
4915
  *
@@ -5042,6 +5067,8 @@ var init_hook_presets = __esm({
5042
5067
  /**
5043
5068
  * Composite preset combining logging, timing, tokenTracking, and errorLogging.
5044
5069
  *
5070
+ * @public
5071
+ *
5045
5072
  * This is the recommended preset for development and initial production deployments,
5046
5073
  * providing comprehensive observability with a single method call.
5047
5074
  *
@@ -11992,7 +12019,9 @@ ${endPrefix}`
11992
12019
  */
11993
12020
  /**
11994
12021
  * Build AgentOptions with the given user prompt.
11995
- * 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)
11996
12025
  */
11997
12026
  buildAgentOptions(userPrompt) {
11998
12027
  if (!this.client) {
@@ -12034,7 +12063,12 @@ ${endPrefix}`
12034
12063
  // Tree context for shared tree model (subagents share parent's tree)
12035
12064
  parentTree: this.parentContext?.tree,
12036
12065
  parentNodeId: this.parentContext?.nodeId,
12037
- 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
12038
12072
  };
12039
12073
  }
12040
12074
  ask(userPrompt) {
@@ -12186,52 +12220,7 @@ ${endPrefix}`
12186
12220
  * ```
12187
12221
  */
12188
12222
  build() {
12189
- if (!this.client) {
12190
- const { LLMist: LLMistClass } = (init_client(), __toCommonJS(client_exports));
12191
- this.client = new LLMistClass();
12192
- }
12193
- const registry = GadgetRegistry.from(this.gadgets);
12194
- const options = {
12195
- client: this.client,
12196
- model: this.model ?? "openai:gpt-5-nano",
12197
- systemPrompt: this.systemPrompt,
12198
- // No userPrompt - agent.run() will throw if called directly
12199
- registry,
12200
- maxIterations: this.maxIterations,
12201
- budget: this.budget,
12202
- temperature: this.temperature,
12203
- logger: this.logger,
12204
- hooks: this.composeHooks(),
12205
- promptConfig: this.promptConfig,
12206
- initialMessages: this.initialMessages,
12207
- requestHumanInput: this.requestHumanInput,
12208
- gadgetStartPrefix: this.gadgetStartPrefix,
12209
- gadgetEndPrefix: this.gadgetEndPrefix,
12210
- gadgetArgPrefix: this.gadgetArgPrefix,
12211
- textOnlyHandler: this.textOnlyHandler,
12212
- textWithGadgetsHandler: this.textWithGadgetsHandler,
12213
- defaultGadgetTimeoutMs: this.defaultGadgetTimeoutMs,
12214
- gadgetExecutionMode: this.gadgetExecutionMode,
12215
- maxGadgetsPerResponse: this.maxGadgetsPerResponse,
12216
- gadgetOutputLimit: this.gadgetOutputLimit,
12217
- gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
12218
- compactionConfig: this.compactionConfig,
12219
- retryConfig: this.retryConfig,
12220
- rateLimitConfig: this.rateLimitConfig,
12221
- signal: this.signal,
12222
- reasoning: this.reasoningConfig,
12223
- caching: this.cachingConfig,
12224
- subagentConfig: this.subagentConfig,
12225
- // Tree context for shared tree model (subagents share parent's tree)
12226
- parentTree: this.parentContext?.tree,
12227
- parentNodeId: this.parentContext?.nodeId,
12228
- baseDepth: this.parentContext ? (this.parentContext.depth ?? 0) + 1 : 0,
12229
- // Parent observer hooks for subagent visibility
12230
- parentObservers: this.parentObservers,
12231
- // Shared rate limit tracker and retry config (for coordinated limits across subagents)
12232
- sharedRateLimitTracker: this.sharedRateLimitTracker,
12233
- sharedRetryConfig: this.sharedRetryConfig
12234
- };
12223
+ const options = this.buildAgentOptions();
12235
12224
  return new Agent(AGENT_INTERNAL_KEY, options);
12236
12225
  }
12237
12226
  };
@@ -13519,139 +13508,551 @@ var init_executor = __esm({
13519
13508
  }
13520
13509
  });
13521
13510
 
13522
- // src/agent/tree-hook-bridge.ts
13523
- function findParentGadgetInvocationId(tree, nodeId) {
13524
- let currentId = nodeId;
13525
- while (currentId) {
13526
- const node = tree.getNode(currentId);
13527
- if (!node) break;
13528
- currentId = node.parentId;
13529
- if (!currentId) break;
13530
- const parentNode = tree.getNode(currentId);
13531
- if (parentNode?.type === "gadget") {
13532
- return parentNode.invocationId;
13533
- }
13534
- }
13535
- return void 0;
13536
- }
13537
- function getIterationFromTree(tree, nodeId) {
13538
- let currentId = nodeId;
13539
- while (currentId) {
13540
- const node = tree.getNode(currentId);
13541
- if (!node) break;
13542
- if (node.type === "llm_call") {
13543
- return node.iteration;
13544
- }
13545
- currentId = node.parentId;
13546
- }
13547
- return 0;
13548
- }
13549
- function buildSubagentContext(tree, event) {
13550
- const parentGadgetInvocationId = findParentGadgetInvocationId(tree, event.nodeId);
13551
- if (!parentGadgetInvocationId) {
13552
- return void 0;
13553
- }
13554
- return {
13555
- parentGadgetInvocationId,
13556
- depth: event.depth
13557
- };
13558
- }
13559
- function getSubagentContextForNode(tree, nodeId) {
13560
- const node = tree.getNode(nodeId);
13561
- if (!node) return void 0;
13562
- const parentGadgetInvocationId = findParentGadgetInvocationId(tree, nodeId);
13563
- if (!parentGadgetInvocationId) {
13564
- return void 0;
13565
- }
13566
- return {
13567
- parentGadgetInvocationId,
13568
- depth: node.depth
13569
- };
13570
- }
13571
- async function safeObserve(fn, logger2, eventType) {
13572
- try {
13573
- await fn();
13574
- } catch (error) {
13575
- logger2.warn(`Observer error in ${eventType}:`, error);
13576
- }
13577
- }
13578
- function chainObserverCall(chainMap, key, fn, logger2, eventType, cleanup = false) {
13579
- const previousPromise = chainMap.get(key) ?? Promise.resolve();
13580
- const newPromise = previousPromise.then(() => safeObserve(fn, logger2, eventType));
13581
- chainMap.set(key, newPromise);
13582
- if (cleanup) {
13583
- newPromise.finally(() => chainMap.delete(key));
13584
- }
13585
- }
13586
- function bridgeTreeToHooks(tree, hooks, logger2) {
13587
- const gadgetPromiseChains = /* @__PURE__ */ new Map();
13588
- const llmPromiseChains = /* @__PURE__ */ new Map();
13589
- return tree.onAll((event) => {
13590
- const subagentContext = buildSubagentContext(tree, event);
13591
- switch (event.type) {
13592
- // =================================================================
13593
- // GADGET EVENTS - Bridged for subagent visibility
13594
- // =================================================================
13595
- // When a subagent executes gadgets, these events propagate through
13596
- // the shared tree to the parent's hooks.
13597
- // Only bridged for subagent events (depth > 0) to avoid double-calling
13598
- // root agent events which are handled directly in stream-processor.ts
13599
- case "gadget_start": {
13600
- if (subagentContext && hooks.observers?.onGadgetExecutionStart) {
13601
- const gadgetEvent = event;
13602
- const gadgetNode = tree.getNode(event.nodeId);
13603
- const context = {
13604
- iteration: getIterationFromTree(tree, event.nodeId),
13605
- gadgetName: gadgetEvent.name,
13606
- invocationId: gadgetEvent.invocationId,
13607
- parameters: gadgetNode?.parameters ?? {},
13608
- logger: logger2,
13609
- subagentContext
13610
- };
13611
- chainObserverCall(
13612
- gadgetPromiseChains,
13613
- gadgetEvent.invocationId,
13614
- () => hooks.observers?.onGadgetExecutionStart?.(context),
13615
- logger2,
13616
- "onGadgetExecutionStart",
13617
- false
13618
- // Don't cleanup - wait for completion event
13619
- );
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" });
13533
+ }
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;
13620
13571
  }
13621
- 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;
13622
13580
  }
13623
- case "gadget_complete": {
13624
- if (subagentContext && hooks.observers?.onGadgetExecutionComplete) {
13625
- const gadgetEvent = event;
13626
- const gadgetNode = tree.getNode(event.nodeId);
13627
- const context = {
13628
- iteration: getIterationFromTree(tree, event.nodeId),
13629
- gadgetName: gadgetEvent.name,
13630
- invocationId: gadgetEvent.invocationId,
13631
- parameters: gadgetNode?.parameters ?? {},
13632
- finalResult: gadgetEvent.result,
13633
- executionTimeMs: gadgetEvent.executionTimeMs,
13634
- cost: gadgetEvent.cost,
13635
- logger: logger2,
13636
- subagentContext
13637
- };
13638
- chainObserverCall(
13639
- gadgetPromiseChains,
13640
- gadgetEvent.invocationId,
13641
- () => hooks.observers?.onGadgetExecutionComplete?.(context),
13642
- logger2,
13643
- "onGadgetExecutionComplete",
13644
- true
13645
- // Cleanup after completion
13646
- );
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;
13647
13602
  }
13648
- break;
13603
+ return total;
13649
13604
  }
13650
- case "gadget_error": {
13651
- if (subagentContext && hooks.observers?.onGadgetExecutionComplete) {
13652
- const gadgetEvent = event;
13653
- const gadgetNode = tree.getNode(event.nodeId);
13654
- const context = {
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;
14054
+ const gadgetNode = tree.getNode(event.nodeId);
14055
+ const context = {
13655
14056
  iteration: getIterationFromTree(tree, event.nodeId),
13656
14057
  gadgetName: gadgetEvent.name,
13657
14058
  invocationId: gadgetEvent.invocationId,
@@ -13798,6 +14199,82 @@ function bridgeTreeToHooks(tree, hooks, logger2) {
13798
14199
  var init_tree_hook_bridge = __esm({
13799
14200
  "src/agent/tree-hook-bridge.ts"() {
13800
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();
13801
14278
  }
13802
14279
  });
13803
14280
 
@@ -13809,11 +14286,13 @@ var init_stream_processor = __esm({
13809
14286
  init_executor();
13810
14287
  init_parser();
13811
14288
  init_logger();
14289
+ init_gadget_concurrency_manager();
14290
+ init_gadget_dependency_resolver();
13812
14291
  init_hook_validators();
13813
- init_tree_hook_bridge();
14292
+ init_observer_notifier();
14293
+ init_safe_observe();
13814
14294
  StreamProcessor = class {
13815
14295
  iteration;
13816
- registry;
13817
14296
  hooks;
13818
14297
  logger;
13819
14298
  parser;
@@ -13825,33 +14304,12 @@ var init_stream_processor = __esm({
13825
14304
  // Gadget execution mode
13826
14305
  gadgetExecutionMode;
13827
14306
  responseText = "";
13828
- observerFailureCount = 0;
13829
- // Dependency tracking for gadget execution DAG
13830
- /** Gadgets waiting for their dependencies to complete */
13831
- gadgetsAwaitingDependencies = /* @__PURE__ */ new Map();
13832
- /** Completed gadget results, keyed by invocation ID */
13833
- completedResults = /* @__PURE__ */ new Map();
13834
- /** Invocation IDs of gadgets that have failed (error or skipped due to dependency) */
13835
- failedInvocations = /* @__PURE__ */ new Set();
13836
- /** Promises for independent gadgets currently executing (fire-and-forget) */
13837
- inFlightExecutions = /* @__PURE__ */ new Map();
14307
+ // Dependency resolution is delegated to GadgetDependencyResolver
14308
+ dependencyResolver;
14309
+ // Concurrency management is delegated to GadgetConcurrencyManager
14310
+ concurrencyManager;
13838
14311
  /** Queue of completed gadget results ready to be yielded (for real-time streaming) */
13839
14312
  completedResultsQueue = [];
13840
- // Concurrency limiting
13841
- /** Subagent configuration map for checking maxConcurrent limits */
13842
- subagentConfig;
13843
- /** Track active execution count per gadget name */
13844
- activeCountByGadget = /* @__PURE__ */ new Map();
13845
- /** Queue of gadgets waiting for a concurrency slot (per gadget name) */
13846
- concurrencyQueue = /* @__PURE__ */ new Map();
13847
- // Exclusive gadget support
13848
- /** Queue of exclusive gadgets deferred until in-flight gadgets complete */
13849
- exclusiveQueue = [];
13850
- // Cross-iteration dependency tracking
13851
- /** Invocation IDs completed in previous iterations (read-only reference from Agent) */
13852
- priorCompletedInvocations;
13853
- /** Invocation IDs that failed in previous iterations (read-only reference from Agent) */
13854
- priorFailedInvocations;
13855
14313
  // Parent observer hooks for subagent visibility
13856
14314
  parentObservers;
13857
14315
  // Gadget limiting per response
@@ -13860,16 +14318,21 @@ var init_stream_processor = __esm({
13860
14318
  limitExceeded = false;
13861
14319
  constructor(options) {
13862
14320
  this.iteration = options.iteration;
13863
- this.registry = options.registry;
13864
14321
  this.hooks = options.hooks ?? {};
13865
14322
  this.logger = options.logger ?? createLogger({ name: "llmist:stream-processor" });
13866
14323
  this.tree = options.tree;
13867
14324
  this.parentNodeId = options.parentNodeId ?? null;
13868
14325
  this.baseDepth = options.baseDepth ?? 0;
13869
14326
  this.gadgetExecutionMode = options.gadgetExecutionMode ?? "parallel";
13870
- this.priorCompletedInvocations = options.priorCompletedInvocations ?? /* @__PURE__ */ new Set();
13871
- this.priorFailedInvocations = options.priorFailedInvocations ?? /* @__PURE__ */ new Set();
13872
- 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
+ });
13873
14336
  this.parentObservers = options.parentObservers;
13874
14337
  this.maxGadgetsPerResponse = options.maxGadgetsPerResponse ?? 0;
13875
14338
  this.parser = new GadgetCallParser({
@@ -14111,7 +14574,7 @@ var init_stream_processor = __esm({
14111
14574
  gadgetName: call.gadgetName,
14112
14575
  invocationId: call.invocationId
14113
14576
  });
14114
- this.failedInvocations.add(call.invocationId);
14577
+ this.dependencyResolver.markFailed(call.invocationId);
14115
14578
  const errorMessage = `Gadget "${call.invocationId}" cannot depend on itself (self-referential dependency)`;
14116
14579
  const skipEvent = {
14117
14580
  type: "gadget_skipped",
@@ -14122,39 +14585,21 @@ var init_stream_processor = __esm({
14122
14585
  failedDependencyError: errorMessage
14123
14586
  };
14124
14587
  yield skipEvent;
14125
- const skippedGadgetNode = this.tree?.getNodeByInvocationId(call.invocationId);
14126
- const skippedSubagentContext = this.tree && skippedGadgetNode ? getSubagentContextForNode(this.tree, skippedGadgetNode.id) : void 0;
14127
- if (this.hooks.observers?.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.hooks.observers.onGadgetSkipped(context));
14139
- }
14140
- if (this.parentObservers?.onGadgetSkipped) {
14141
- const context = {
14142
- iteration: this.iteration,
14143
- gadgetName: call.gadgetName,
14144
- invocationId: call.invocationId,
14145
- parameters: call.parameters ?? {},
14146
- failedDependency: call.invocationId,
14147
- failedDependencyError: errorMessage,
14148
- logger: this.logger,
14149
- subagentContext: skippedSubagentContext
14150
- };
14151
- await this.safeObserve(() => this.parentObservers.onGadgetSkipped(context));
14152
- }
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
+ });
14153
14600
  return;
14154
14601
  }
14155
- const failedDep = call.dependencies.find(
14156
- (dep) => this.failedInvocations.has(dep) || this.priorFailedInvocations.has(dep)
14157
- );
14602
+ const failedDep = this.dependencyResolver.getFailedDependency(call);
14158
14603
  if (failedDep) {
14159
14604
  const skipEvents = await this.handleFailedDependency(call, failedDep);
14160
14605
  for (const evt of skipEvents) {
@@ -14162,16 +14607,16 @@ var init_stream_processor = __esm({
14162
14607
  }
14163
14608
  return;
14164
14609
  }
14165
- const unsatisfied = call.dependencies.filter(
14166
- (dep) => !this.completedResults.has(dep) && !this.priorCompletedInvocations.has(dep)
14167
- );
14168
- if (unsatisfied.length > 0) {
14610
+ if (!this.dependencyResolver.isAllSatisfied(call)) {
14611
+ const unsatisfied = call.dependencies.filter(
14612
+ (dep) => !this.dependencyResolver.isCompleted(dep)
14613
+ );
14169
14614
  this.logger.debug("Queueing gadget for later - waiting on dependencies", {
14170
14615
  gadgetName: call.gadgetName,
14171
14616
  invocationId: call.invocationId,
14172
14617
  waitingOn: unsatisfied
14173
14618
  });
14174
- this.gadgetsAwaitingDependencies.set(call.invocationId, call);
14619
+ this.dependencyResolver.addPending(call);
14175
14620
  return;
14176
14621
  }
14177
14622
  const limitCheckGen2 = this.checkGadgetLimitExceeded(call);
@@ -14200,28 +14645,12 @@ var init_stream_processor = __esm({
14200
14645
  if (limitResult.value === true) {
14201
14646
  return;
14202
14647
  }
14203
- const gadget = this.registry.get(call.gadgetName);
14204
- if (gadget?.exclusive && this.inFlightExecutions.size > 0) {
14205
- this.logger.debug("Deferring exclusive gadget until in-flight gadgets complete", {
14206
- gadgetName: call.gadgetName,
14207
- invocationId: call.invocationId,
14208
- inFlightCount: this.inFlightExecutions.size
14209
- });
14210
- this.exclusiveQueue.push(call);
14648
+ if (this.concurrencyManager.isExclusive(call.gadgetName) && this.concurrencyManager.inFlightCount > 0) {
14649
+ this.concurrencyManager.queueExclusive(call);
14211
14650
  return;
14212
14651
  }
14213
- const limit = this.getConcurrencyLimit(call.gadgetName);
14214
- const activeCount = this.activeCountByGadget.get(call.gadgetName) ?? 0;
14215
- if (limit > 0 && activeCount >= limit) {
14216
- this.logger.debug("Gadget queued due to concurrency limit", {
14217
- gadgetName: call.gadgetName,
14218
- invocationId: call.invocationId,
14219
- activeCount,
14220
- limit
14221
- });
14222
- const queue = this.concurrencyQueue.get(call.gadgetName) ?? [];
14223
- queue.push(call);
14224
- this.concurrencyQueue.set(call.gadgetName, queue);
14652
+ if (!this.concurrencyManager.canStart(call)) {
14653
+ this.concurrencyManager.queueForLater(call);
14225
14654
  return;
14226
14655
  }
14227
14656
  if (this.gadgetExecutionMode === "sequential") {
@@ -14232,57 +14661,19 @@ var init_stream_processor = __esm({
14232
14661
  this.startGadgetWithConcurrencyTracking(call);
14233
14662
  }
14234
14663
  }
14235
- /**
14236
- * Get the effective concurrency limit for a gadget.
14237
- * Uses "most restrictive wins" strategy: the lowest non-zero value from
14238
- * external config (SubagentConfig) and gadget's intrinsic maxConcurrent.
14239
- *
14240
- * This ensures gadget authors can set safety floors (e.g., maxConcurrent: 1
14241
- * for file writers) that cannot be weakened by external configuration.
14242
- *
14243
- * @returns 0 if unlimited, otherwise the effective limit
14244
- */
14245
- getConcurrencyLimit(gadgetName) {
14246
- const configLimit = this.subagentConfig?.[gadgetName]?.maxConcurrent;
14247
- const gadget = this.registry.get(gadgetName);
14248
- const gadgetLimit = gadget?.maxConcurrent;
14249
- const config = configLimit || Number.POSITIVE_INFINITY;
14250
- const intrinsic = gadgetLimit || Number.POSITIVE_INFINITY;
14251
- const effective = Math.min(config, intrinsic);
14252
- return effective === Number.POSITIVE_INFINITY ? 0 : effective;
14253
- }
14254
14664
  /**
14255
14665
  * Start a gadget execution with concurrency tracking.
14256
- * Increments active count, starts execution, and schedules queue processing on completion.
14666
+ * Delegates tracking to GadgetConcurrencyManager; schedules queue processing on completion.
14257
14667
  */
14258
14668
  startGadgetWithConcurrencyTracking(call) {
14259
14669
  const gadgetName = call.gadgetName;
14260
- const currentCount = this.activeCountByGadget.get(gadgetName) ?? 0;
14261
- this.activeCountByGadget.set(gadgetName, currentCount + 1);
14262
14670
  const executionPromise = this.executeGadgetAndCollect(call).finally(() => {
14263
- const newCount = (this.activeCountByGadget.get(gadgetName) ?? 1) - 1;
14264
- this.activeCountByGadget.set(gadgetName, newCount);
14265
- this.processQueuedGadget(gadgetName);
14671
+ const nextCall = this.concurrencyManager.onComplete(gadgetName);
14672
+ if (nextCall) {
14673
+ this.startGadgetWithConcurrencyTracking(nextCall);
14674
+ }
14266
14675
  });
14267
- this.inFlightExecutions.set(call.invocationId, executionPromise);
14268
- }
14269
- /**
14270
- * Process the next queued gadget for a given gadget name if a slot is available.
14271
- */
14272
- processQueuedGadget(gadgetName) {
14273
- const queue = this.concurrencyQueue.get(gadgetName);
14274
- if (!queue || queue.length === 0) return;
14275
- const limit = this.getConcurrencyLimit(gadgetName);
14276
- const activeCount = this.activeCountByGadget.get(gadgetName) ?? 0;
14277
- if (limit === 0 || activeCount < limit) {
14278
- const nextCall = queue.shift();
14279
- this.logger.debug("Processing queued gadget", {
14280
- gadgetName,
14281
- invocationId: nextCall.invocationId,
14282
- remainingInQueue: queue.length
14283
- });
14284
- this.startGadgetWithConcurrencyTracking(nextCall);
14285
- }
14676
+ this.concurrencyManager.trackExecution(call.invocationId, gadgetName, executionPromise);
14286
14677
  }
14287
14678
  /**
14288
14679
  * Execute a gadget through the full hook lifecycle and yield events.
@@ -14337,30 +14728,16 @@ var init_stream_processor = __esm({
14337
14728
  this.tree.startGadget(gadgetNode.id);
14338
14729
  }
14339
14730
  }
14340
- const gadgetStartNode = this.tree?.getNodeByInvocationId(call.invocationId);
14341
- const gadgetStartSubagentContext = this.tree && gadgetStartNode ? getSubagentContextForNode(this.tree, gadgetStartNode.id) : void 0;
14342
- if (this.hooks.observers?.onGadgetExecutionStart) {
14343
- const context = {
14344
- iteration: this.iteration,
14345
- gadgetName: call.gadgetName,
14346
- invocationId: call.invocationId,
14347
- parameters,
14348
- logger: this.logger,
14349
- subagentContext: gadgetStartSubagentContext
14350
- };
14351
- await this.safeObserve(() => this.hooks.observers.onGadgetExecutionStart(context));
14352
- }
14353
- if (this.parentObservers?.onGadgetExecutionStart) {
14354
- const context = {
14355
- iteration: this.iteration,
14356
- gadgetName: call.gadgetName,
14357
- invocationId: call.invocationId,
14358
- parameters,
14359
- logger: this.logger,
14360
- subagentContext: gadgetStartSubagentContext
14361
- };
14362
- await this.safeObserve(() => this.parentObservers.onGadgetExecutionStart(context));
14363
- }
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
+ });
14364
14741
  let result;
14365
14742
  if (shouldSkip) {
14366
14743
  result = {
@@ -14373,7 +14750,7 @@ var init_stream_processor = __esm({
14373
14750
  } else {
14374
14751
  result = await this.executor.execute(call);
14375
14752
  }
14376
- if (result.result && this.hooks.interceptors?.interceptGadgetResult) {
14753
+ if ((result.result || result.error) && this.hooks.interceptors?.interceptGadgetResult) {
14377
14754
  const context = {
14378
14755
  iteration: this.iteration,
14379
14756
  gadgetName: result.gadgetName,
@@ -14382,7 +14759,12 @@ var init_stream_processor = __esm({
14382
14759
  executionTimeMs: result.executionTimeMs,
14383
14760
  logger: this.logger
14384
14761
  };
14385
- 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
+ }
14386
14768
  }
14387
14769
  if (this.hooks.controllers?.afterGadgetExecution) {
14388
14770
  const context = {
@@ -14429,42 +14811,21 @@ var init_stream_processor = __esm({
14429
14811
  }
14430
14812
  }
14431
14813
  }
14432
- const gadgetCompleteNode = this.tree?.getNodeByInvocationId(result.invocationId);
14433
- const gadgetCompleteSubagentContext = this.tree && gadgetCompleteNode ? getSubagentContextForNode(this.tree, gadgetCompleteNode.id) : void 0;
14434
- if (this.hooks.observers?.onGadgetExecutionComplete) {
14435
- const context = {
14436
- iteration: this.iteration,
14437
- gadgetName: result.gadgetName,
14438
- invocationId: result.invocationId,
14439
- parameters,
14440
- finalResult: result.result,
14441
- error: result.error,
14442
- executionTimeMs: result.executionTimeMs,
14443
- cost: result.cost,
14444
- logger: this.logger,
14445
- subagentContext: gadgetCompleteSubagentContext
14446
- };
14447
- await this.safeObserve(() => this.hooks.observers.onGadgetExecutionComplete(context));
14448
- }
14449
- if (this.parentObservers?.onGadgetExecutionComplete) {
14450
- const context = {
14451
- iteration: this.iteration,
14452
- gadgetName: result.gadgetName,
14453
- invocationId: result.invocationId,
14454
- parameters,
14455
- finalResult: result.result,
14456
- error: result.error,
14457
- executionTimeMs: result.executionTimeMs,
14458
- cost: result.cost,
14459
- logger: this.logger,
14460
- subagentContext: gadgetCompleteSubagentContext
14461
- };
14462
- await this.safeObserve(() => this.parentObservers.onGadgetExecutionComplete(context));
14463
- }
14464
- this.completedResults.set(result.invocationId, result);
14465
- if (result.error) {
14466
- this.failedInvocations.add(result.invocationId);
14467
- }
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);
14468
14829
  yield { type: "gadget_result", result };
14469
14830
  }
14470
14831
  /**
@@ -14496,77 +14857,45 @@ var init_stream_processor = __esm({
14496
14857
  * Clears the inFlightExecutions map after all gadgets complete.
14497
14858
  */
14498
14859
  async *waitForInFlightExecutions() {
14499
- if (this.inFlightExecutions.size === 0 && !this.hasQueuedGadgets() && this.exclusiveQueue.length === 0) {
14860
+ if (this.concurrencyManager.inFlightCount === 0 && !this.concurrencyManager.hasQueuedGadgets() && !this.concurrencyManager.hasExclusiveQueued) {
14500
14861
  return;
14501
14862
  }
14502
14863
  this.logger.debug("Waiting for in-flight gadget executions", {
14503
- count: this.inFlightExecutions.size,
14504
- invocationIds: Array.from(this.inFlightExecutions.keys()),
14505
- queuedCount: this.getQueuedGadgetCount()
14864
+ count: this.concurrencyManager.inFlightCount,
14865
+ queuedCount: this.concurrencyManager.getQueuedGadgetCount()
14506
14866
  });
14507
14867
  const POLL_INTERVAL_MS = 100;
14508
- while (this.inFlightExecutions.size > 0 || this.hasQueuedGadgets()) {
14509
- 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();
14510
14870
  const result = await Promise.race([
14511
14871
  allDone,
14512
14872
  new Promise((resolve2) => setTimeout(() => resolve2("poll"), POLL_INTERVAL_MS))
14513
14873
  ]);
14514
14874
  yield* this.drainCompletedResults();
14515
- if (result === "done" && this.getTotalActiveGadgetCount() === 0 && !this.hasQueuedGadgets()) {
14875
+ if (result === "done" && this.concurrencyManager.getTotalActiveGadgetCount() === 0 && !this.concurrencyManager.hasQueuedGadgets()) {
14516
14876
  break;
14517
14877
  }
14518
14878
  }
14519
- this.inFlightExecutions.clear();
14520
- if (this.exclusiveQueue.length > 0) {
14879
+ this.concurrencyManager.clearInFlight();
14880
+ if (this.concurrencyManager.hasExclusiveQueued) {
14881
+ const exclusiveQueue = this.concurrencyManager.drainExclusiveQueue();
14521
14882
  this.logger.debug("Processing deferred exclusive gadgets", {
14522
- count: this.exclusiveQueue.length
14883
+ count: exclusiveQueue.length
14523
14884
  });
14524
- const queue = this.exclusiveQueue;
14525
- this.exclusiveQueue = [];
14526
- for (const call of queue) {
14885
+ for (const call of exclusiveQueue) {
14527
14886
  for await (const evt of this.executeGadgetGenerator(call)) {
14528
14887
  yield evt;
14529
14888
  }
14530
14889
  }
14531
14890
  }
14532
14891
  }
14533
- /**
14534
- * Check if there are any gadgets waiting in concurrency queues.
14535
- */
14536
- hasQueuedGadgets() {
14537
- for (const queue of this.concurrencyQueue.values()) {
14538
- if (queue.length > 0) return true;
14539
- }
14540
- return false;
14541
- }
14542
- /**
14543
- * Get total count of queued gadgets across all queues.
14544
- */
14545
- getQueuedGadgetCount() {
14546
- let count = 0;
14547
- for (const queue of this.concurrencyQueue.values()) {
14548
- count += queue.length;
14549
- }
14550
- return count;
14551
- }
14552
- /**
14553
- * Get total count of actively executing gadgets across all types.
14554
- * Used to know when all work is truly complete (not just when allDone resolves).
14555
- */
14556
- getTotalActiveGadgetCount() {
14557
- let total = 0;
14558
- for (const count of this.activeCountByGadget.values()) {
14559
- total += count;
14560
- }
14561
- return total;
14562
- }
14563
14892
  /**
14564
14893
  * Handle a gadget that cannot execute because a dependency failed.
14565
14894
  * Calls the onDependencySkipped controller to allow customization.
14566
14895
  */
14567
14896
  async handleFailedDependency(call, failedDep) {
14568
14897
  const events = [];
14569
- const depResult = this.completedResults.get(failedDep);
14898
+ const depResult = this.dependencyResolver.getCompletedResult(failedDep);
14570
14899
  const depError = depResult?.error ?? "Dependency failed";
14571
14900
  let action = { action: "skip" };
14572
14901
  if (this.hooks.controllers?.onDependencySkipped) {
@@ -14582,7 +14911,7 @@ var init_stream_processor = __esm({
14582
14911
  action = await this.hooks.controllers.onDependencySkipped(context);
14583
14912
  }
14584
14913
  if (action.action === "skip") {
14585
- this.failedInvocations.add(call.invocationId);
14914
+ this.dependencyResolver.markFailed(call.invocationId);
14586
14915
  if (this.tree) {
14587
14916
  const gadgetNode = this.tree.getNodeByInvocationId(call.invocationId);
14588
14917
  if (gadgetNode) {
@@ -14598,34 +14927,18 @@ var init_stream_processor = __esm({
14598
14927
  failedDependencyError: depError
14599
14928
  };
14600
14929
  events.push(skipEvent);
14601
- const gadgetNodeForSkip = this.tree?.getNodeByInvocationId(call.invocationId);
14602
- const skipSubagentContext = this.tree && gadgetNodeForSkip ? getSubagentContextForNode(this.tree, gadgetNodeForSkip.id) : void 0;
14603
- if (this.hooks.observers?.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.hooks.observers.onGadgetSkipped(context));
14615
- }
14616
- if (this.parentObservers?.onGadgetSkipped) {
14617
- const context = {
14618
- iteration: this.iteration,
14619
- gadgetName: call.gadgetName,
14620
- invocationId: call.invocationId,
14621
- parameters: call.parameters ?? {},
14622
- failedDependency: failedDep,
14623
- failedDependencyError: depError,
14624
- logger: this.logger,
14625
- subagentContext: skipSubagentContext
14626
- };
14627
- await this.safeObserve(() => this.parentObservers.onGadgetSkipped(context));
14628
- }
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
+ });
14629
14942
  this.logger.info("Gadget skipped due to failed dependency", {
14630
14943
  gadgetName: call.gadgetName,
14631
14944
  invocationId: call.invocationId,
@@ -14648,7 +14961,7 @@ var init_stream_processor = __esm({
14648
14961
  result: action.fallbackResult,
14649
14962
  executionTimeMs: 0
14650
14963
  };
14651
- this.completedResults.set(call.invocationId, fallbackResult);
14964
+ this.dependencyResolver.markComplete(fallbackResult);
14652
14965
  events.push({ type: "gadget_result", result: fallbackResult });
14653
14966
  this.logger.info("Using fallback result for gadget with failed dependency", {
14654
14967
  gadgetName: call.gadgetName,
@@ -14679,7 +14992,7 @@ var init_stream_processor = __esm({
14679
14992
  limit: this.maxGadgetsPerResponse,
14680
14993
  currentCount: this.gadgetStartedCount
14681
14994
  });
14682
- this.failedInvocations.add(call.invocationId);
14995
+ this.dependencyResolver.markFailed(call.invocationId);
14683
14996
  if (this.tree) {
14684
14997
  const gadgetNode = this.tree.getNodeByInvocationId(call.invocationId);
14685
14998
  if (gadgetNode) {
@@ -14700,34 +15013,18 @@ var init_stream_processor = __esm({
14700
15013
  failedDependencyError: errorMessage
14701
15014
  };
14702
15015
  yield skipEvent;
14703
- const limitSkipNode = this.tree?.getNodeByInvocationId(call.invocationId);
14704
- const limitSkipSubagentContext = this.tree && limitSkipNode ? getSubagentContextForNode(this.tree, limitSkipNode.id) : void 0;
14705
- if (this.hooks.observers?.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.hooks.observers.onGadgetSkipped(context));
14717
- }
14718
- if (this.parentObservers?.onGadgetSkipped) {
14719
- const context = {
14720
- iteration: this.iteration,
14721
- gadgetName: call.gadgetName,
14722
- invocationId: call.invocationId,
14723
- parameters: call.parameters ?? {},
14724
- failedDependency: "maxGadgetsPerResponse",
14725
- failedDependencyError: errorMessage,
14726
- logger: this.logger,
14727
- subagentContext: limitSkipSubagentContext
14728
- };
14729
- await this.safeObserve(() => this.parentObservers.onGadgetSkipped(context));
14730
- }
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
+ });
14731
15028
  return true;
14732
15029
  }
14733
15030
  this.gadgetStartedCount++;
@@ -14745,27 +15042,11 @@ var init_stream_processor = __esm({
14745
15042
  return;
14746
15043
  }
14747
15044
  let progress = true;
14748
- while (progress && this.gadgetsAwaitingDependencies.size > 0) {
15045
+ while (progress && this.dependencyResolver.pendingCount > 0) {
14749
15046
  progress = false;
14750
- const readyToExecute = [];
14751
- const readyToSkip = [];
14752
- for (const [_invocationId, call] of this.gadgetsAwaitingDependencies) {
14753
- const failedDep = call.dependencies.find(
14754
- (dep) => this.failedInvocations.has(dep) || this.priorFailedInvocations.has(dep)
14755
- );
14756
- if (failedDep) {
14757
- readyToSkip.push({ call, failedDep });
14758
- continue;
14759
- }
14760
- const allSatisfied = call.dependencies.every(
14761
- (dep) => this.completedResults.has(dep) || this.priorCompletedInvocations.has(dep)
14762
- );
14763
- if (allSatisfied) {
14764
- readyToExecute.push(call);
14765
- }
14766
- }
15047
+ const { readyToExecute, readyToSkip } = this.dependencyResolver.getReadyCalls();
14767
15048
  for (const { call, failedDep } of readyToSkip) {
14768
- this.gadgetsAwaitingDependencies.delete(call.invocationId);
15049
+ this.dependencyResolver.removePending(call.invocationId);
14769
15050
  const skipEvents = await this.handleFailedDependency(call, failedDep);
14770
15051
  for (const evt of skipEvents) {
14771
15052
  yield evt;
@@ -14774,7 +15055,7 @@ var init_stream_processor = __esm({
14774
15055
  }
14775
15056
  if (readyToExecute.length > 0) {
14776
15057
  for (const call of readyToExecute) {
14777
- this.gadgetsAwaitingDependencies.delete(call.invocationId);
15058
+ this.dependencyResolver.removePending(call.invocationId);
14778
15059
  }
14779
15060
  if (this.gadgetExecutionMode === "sequential") {
14780
15061
  this.logger.debug("Executing ready gadgets sequentially", {
@@ -14827,11 +15108,12 @@ var init_stream_processor = __esm({
14827
15108
  progress = true;
14828
15109
  }
14829
15110
  }
14830
- if (this.gadgetsAwaitingDependencies.size > 0) {
14831
- const pendingIds = new Set(this.gadgetsAwaitingDependencies.keys());
14832
- 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) {
14833
15115
  const missingDeps = call.dependencies.filter(
14834
- (dep) => !this.completedResults.has(dep) && !this.priorCompletedInvocations.has(dep)
15116
+ (dep) => !this.dependencyResolver.isCompleted(dep)
14835
15117
  );
14836
15118
  const circularDeps = missingDeps.filter((dep) => pendingIds.has(dep));
14837
15119
  const trulyMissingDeps = missingDeps.filter((dep) => !pendingIds.has(dep));
@@ -14851,7 +15133,7 @@ var init_stream_processor = __esm({
14851
15133
  circularDependencies: circularDeps,
14852
15134
  missingDependencies: trulyMissingDeps
14853
15135
  });
14854
- this.failedInvocations.add(invocationId);
15136
+ this.dependencyResolver.markFailed(invocationId);
14855
15137
  const skipEvent = {
14856
15138
  type: "gadget_skipped",
14857
15139
  gadgetName: call.gadgetName,
@@ -14861,51 +15143,20 @@ var init_stream_processor = __esm({
14861
15143
  failedDependencyError: errorMessage
14862
15144
  };
14863
15145
  yield skipEvent;
14864
- const gadgetNodeForTimeout = this.tree?.getNodeByInvocationId(invocationId);
14865
- const timeoutSubagentContext = this.tree && gadgetNodeForTimeout ? getSubagentContextForNode(this.tree, gadgetNodeForTimeout.id) : void 0;
14866
- if (this.hooks.observers?.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.hooks.observers.onGadgetSkipped(context));
14878
- }
14879
- if (this.parentObservers?.onGadgetSkipped) {
14880
- const context = {
14881
- iteration: this.iteration,
14882
- gadgetName: call.gadgetName,
14883
- invocationId,
14884
- parameters: call.parameters ?? {},
14885
- failedDependency: missingDeps[0],
14886
- failedDependencyError: errorMessage,
14887
- logger: this.logger,
14888
- subagentContext: timeoutSubagentContext
14889
- };
14890
- await this.safeObserve(() => this.parentObservers.onGadgetSkipped(context));
14891
- }
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
+ });
14892
15158
  }
14893
- this.gadgetsAwaitingDependencies.clear();
14894
- }
14895
- }
14896
- /**
14897
- * Safely execute an observer, catching and logging any errors.
14898
- * Observers are non-critical, so errors are logged but don't crash the system.
14899
- */
14900
- async safeObserve(fn) {
14901
- try {
14902
- await fn();
14903
- } catch (error) {
14904
- this.observerFailureCount++;
14905
- this.logger.error("Observer threw error (ignoring)", {
14906
- error: error instanceof Error ? error.message : String(error),
14907
- failureCount: this.observerFailureCount
14908
- });
15159
+ this.dependencyResolver.clearPending();
14909
15160
  }
14910
15161
  }
14911
15162
  /**
@@ -14914,7 +15165,7 @@ var init_stream_processor = __esm({
14914
15165
  */
14915
15166
  async runObserversInParallel(observers) {
14916
15167
  if (observers.length === 0) return;
14917
- await Promise.allSettled(observers.map((observer) => this.safeObserve(observer)));
15168
+ await Promise.allSettled(observers.map((observer) => safeObserve(observer, this.logger)));
14918
15169
  }
14919
15170
  // ==========================================================================
14920
15171
  // Public accessors for cross-iteration dependency tracking
@@ -14924,14 +15175,14 @@ var init_stream_processor = __esm({
14924
15175
  * Used by Agent to accumulate completed IDs across iterations.
14925
15176
  */
14926
15177
  getCompletedInvocationIds() {
14927
- return new Set(this.completedResults.keys());
15178
+ return this.dependencyResolver.getCompletedInvocationIds();
14928
15179
  }
14929
15180
  /**
14930
15181
  * Get all invocation IDs that failed in this iteration.
14931
15182
  * Used by Agent to accumulate failed IDs across iterations.
14932
15183
  */
14933
15184
  getFailedInvocationIds() {
14934
- return new Set(this.failedInvocations);
15185
+ return this.dependencyResolver.getFailedInvocationIds();
14935
15186
  }
14936
15187
  };
14937
15188
  }
@@ -14958,6 +15209,7 @@ var init_agent = __esm({
14958
15209
  init_event_handlers();
14959
15210
  init_gadget_output_store();
14960
15211
  init_hook_validators();
15212
+ init_safe_observe();
14961
15213
  init_stream_processor();
14962
15214
  init_tree_hook_bridge();
14963
15215
  Agent = class {
@@ -15446,7 +15698,7 @@ var init_agent = __esm({
15446
15698
  }
15447
15699
  );
15448
15700
  this.retryConfig.onRetry?.(error, streamAttempt);
15449
- await this.safeObserve(async () => {
15701
+ await safeObserve(async () => {
15450
15702
  if (this.hooks.observers?.onRetryAttempt) {
15451
15703
  const subagentContext = getSubagentContextForNode(this.tree, currentLLMNodeId);
15452
15704
  const hookContext = {
@@ -15460,7 +15712,7 @@ var init_agent = __esm({
15460
15712
  };
15461
15713
  await this.hooks.observers.onRetryAttempt(hookContext);
15462
15714
  }
15463
- });
15715
+ }, this.logger);
15464
15716
  await this.sleep(finalDelay);
15465
15717
  streamMetadata = null;
15466
15718
  gadgetCallCount = 0;
@@ -15490,7 +15742,7 @@ var init_agent = __esm({
15490
15742
  this.logger.silly("LLM response details", {
15491
15743
  rawResponse: result.rawResponse
15492
15744
  });
15493
- await this.safeObserve(async () => {
15745
+ await safeObserve(async () => {
15494
15746
  if (this.hooks.observers?.onLLMCallComplete) {
15495
15747
  const subagentContext = getSubagentContextForNode(this.tree, currentLLMNodeId);
15496
15748
  const context = {
@@ -15506,7 +15758,7 @@ var init_agent = __esm({
15506
15758
  };
15507
15759
  await this.hooks.observers.onLLMCallComplete(context);
15508
15760
  }
15509
- });
15761
+ }, this.logger);
15510
15762
  this.completeLLMCallInTree(currentLLMNodeId, result);
15511
15763
  const finalMessage = await this.processAfterLLMCallController(
15512
15764
  currentIteration,
@@ -15540,7 +15792,7 @@ var init_agent = __esm({
15540
15792
  }
15541
15793
  } catch (error) {
15542
15794
  const errorHandled = await this.handleLLMError(error, currentIteration);
15543
- await this.safeObserve(async () => {
15795
+ await safeObserve(async () => {
15544
15796
  if (this.hooks.observers?.onLLMCallError) {
15545
15797
  const options = llmOptions ?? {
15546
15798
  model: this.model,
@@ -15559,7 +15811,7 @@ var init_agent = __esm({
15559
15811
  };
15560
15812
  await this.hooks.observers.onLLMCallError(context);
15561
15813
  }
15562
- });
15814
+ }, this.logger);
15563
15815
  if (!errorHandled) {
15564
15816
  throw error;
15565
15817
  }
@@ -15586,7 +15838,7 @@ var init_agent = __esm({
15586
15838
  if (currentLLMNodeId) {
15587
15839
  const node = this.tree.getNode(currentLLMNodeId);
15588
15840
  if (node && node.type === "llm_call" && !node.completedAt) {
15589
- await this.safeObserve(async () => {
15841
+ await safeObserve(async () => {
15590
15842
  if (this.hooks.observers?.onLLMCallComplete) {
15591
15843
  const subagentContext = getSubagentContextForNode(this.tree, currentLLMNodeId);
15592
15844
  const context = {
@@ -15608,7 +15860,7 @@ var init_agent = __esm({
15608
15860
  };
15609
15861
  await this.hooks.observers.onLLMCallComplete(context);
15610
15862
  }
15611
- });
15863
+ }, this.logger);
15612
15864
  this.tree.completeLLMCall(currentLLMNodeId, {
15613
15865
  finishReason: "interrupted"
15614
15866
  });
@@ -15628,7 +15880,7 @@ var init_agent = __esm({
15628
15880
  const throttleDelay = this.rateLimitTracker.getRequiredDelayMs();
15629
15881
  if (throttleDelay > 0) {
15630
15882
  this.logger.debug("Rate limit throttling", { delayMs: throttleDelay });
15631
- await this.safeObserve(async () => {
15883
+ await safeObserve(async () => {
15632
15884
  if (this.hooks.observers?.onRateLimitThrottle) {
15633
15885
  const subagentContext = getSubagentContextForNode(this.tree, llmNodeId);
15634
15886
  const context = {
@@ -15640,7 +15892,7 @@ var init_agent = __esm({
15640
15892
  };
15641
15893
  await this.hooks.observers.onRateLimitThrottle(context);
15642
15894
  }
15643
- });
15895
+ }, this.logger);
15644
15896
  await this.sleep(throttleDelay);
15645
15897
  }
15646
15898
  this.rateLimitTracker.reserveRequest();
@@ -15703,18 +15955,6 @@ var init_agent = __esm({
15703
15955
  }
15704
15956
  return true;
15705
15957
  }
15706
- /**
15707
- * Safely execute an observer, catching and logging any errors.
15708
- */
15709
- async safeObserve(fn) {
15710
- try {
15711
- await fn();
15712
- } catch (error) {
15713
- this.logger.error("Observer threw error (ignoring)", {
15714
- error: error instanceof Error ? error.message : String(error)
15715
- });
15716
- }
15717
- }
15718
15958
  /**
15719
15959
  * Resolve max tokens from model catalog.
15720
15960
  */
@@ -15783,7 +16023,7 @@ var init_agent = __esm({
15783
16023
  iteration,
15784
16024
  reason: this.signal.reason
15785
16025
  });
15786
- await this.safeObserve(async () => {
16026
+ await safeObserve(async () => {
15787
16027
  if (this.hooks.observers?.onAbort) {
15788
16028
  const context = {
15789
16029
  iteration,
@@ -15792,7 +16032,7 @@ var init_agent = __esm({
15792
16032
  };
15793
16033
  await this.hooks.observers.onAbort(context);
15794
16034
  }
15795
- });
16035
+ }, this.logger);
15796
16036
  return true;
15797
16037
  }
15798
16038
  /**
@@ -15811,7 +16051,7 @@ var init_agent = __esm({
15811
16051
  tokensBefore: compactionEvent.tokensBefore,
15812
16052
  tokensAfter: compactionEvent.tokensAfter
15813
16053
  });
15814
- await this.safeObserve(async () => {
16054
+ await safeObserve(async () => {
15815
16055
  if (this.hooks.observers?.onCompaction) {
15816
16056
  await this.hooks.observers.onCompaction({
15817
16057
  iteration,
@@ -15821,7 +16061,7 @@ var init_agent = __esm({
15821
16061
  logger: this.logger
15822
16062
  });
15823
16063
  }
15824
- });
16064
+ }, this.logger);
15825
16065
  return { type: "compaction", event: compactionEvent };
15826
16066
  }
15827
16067
  /**
@@ -15874,7 +16114,7 @@ var init_agent = __esm({
15874
16114
  parentId: this.parentNodeId,
15875
16115
  request: llmOptions.messages
15876
16116
  });
15877
- await this.safeObserve(async () => {
16117
+ await safeObserve(async () => {
15878
16118
  if (this.hooks.observers?.onLLMCallStart) {
15879
16119
  const subagentContext = getSubagentContextForNode(this.tree, llmNode.id);
15880
16120
  const context = {
@@ -15885,7 +16125,7 @@ var init_agent = __esm({
15885
16125
  };
15886
16126
  await this.hooks.observers.onLLMCallStart(context);
15887
16127
  }
15888
- });
16128
+ }, this.logger);
15889
16129
  if (this.hooks.controllers?.beforeLLMCall) {
15890
16130
  const context = {
15891
16131
  iteration,
@@ -15908,7 +16148,7 @@ var init_agent = __esm({
15908
16148
  llmOptions = { ...llmOptions, ...action.modifiedOptions };
15909
16149
  }
15910
16150
  }
15911
- await this.safeObserve(async () => {
16151
+ await safeObserve(async () => {
15912
16152
  if (this.hooks.observers?.onLLMCallReady) {
15913
16153
  const subagentContext = getSubagentContextForNode(this.tree, llmNode.id);
15914
16154
  const context = {
@@ -15922,7 +16162,7 @@ var init_agent = __esm({
15922
16162
  };
15923
16163
  await this.hooks.observers.onLLMCallReady(context);
15924
16164
  }
15925
- });
16165
+ }, this.logger);
15926
16166
  return { options: llmOptions, llmNodeId: llmNode.id };
15927
16167
  }
15928
16168
  /**