llmist 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3485,7 +3485,7 @@ var init_executor = __esm({
3485
3485
  init_exceptions();
3486
3486
  init_parser();
3487
3487
  GadgetExecutor = class {
3488
- constructor(registry, requestHumanInput, logger, defaultGadgetTimeoutMs, errorFormatterOptions, client, mediaStore, agentConfig, subagentConfig) {
3488
+ constructor(registry, requestHumanInput, logger, defaultGadgetTimeoutMs, errorFormatterOptions, client, mediaStore, agentConfig, subagentConfig, onNestedEvent) {
3489
3489
  this.registry = registry;
3490
3490
  this.requestHumanInput = requestHumanInput;
3491
3491
  this.defaultGadgetTimeoutMs = defaultGadgetTimeoutMs;
@@ -3493,6 +3493,7 @@ var init_executor = __esm({
3493
3493
  this.mediaStore = mediaStore;
3494
3494
  this.agentConfig = agentConfig;
3495
3495
  this.subagentConfig = subagentConfig;
3496
+ this.onNestedEvent = onNestedEvent;
3496
3497
  this.logger = logger ?? createLogger({ name: "llmist:executor" });
3497
3498
  this.errorFormatter = new GadgetExecutionErrorFormatter(errorFormatterOptions);
3498
3499
  this.argPrefix = errorFormatterOptions?.argPrefix ?? GADGET_ARG_PREFIX;
@@ -3638,7 +3639,9 @@ var init_executor = __esm({
3638
3639
  llmist: this.client ? new CostReportingLLMistWrapper(this.client, reportCost) : void 0,
3639
3640
  signal: abortController.signal,
3640
3641
  agentConfig: this.agentConfig,
3641
- subagentConfig: this.subagentConfig
3642
+ subagentConfig: this.subagentConfig,
3643
+ invocationId: call.invocationId,
3644
+ onNestedEvent: this.onNestedEvent
3642
3645
  };
3643
3646
  let rawResult;
3644
3647
  if (timeoutMs && timeoutMs > 0) {
@@ -3877,14 +3880,21 @@ var init_stream_processor = __esm({
3877
3880
  options.client,
3878
3881
  options.mediaStore,
3879
3882
  options.agentConfig,
3880
- options.subagentConfig
3883
+ options.subagentConfig,
3884
+ options.onNestedEvent
3881
3885
  );
3882
3886
  }
3883
3887
  /**
3884
- * Process an LLM stream and return structured results.
3888
+ * Process an LLM stream and yield events in real-time.
3889
+ *
3890
+ * This is an async generator that yields events immediately as they occur:
3891
+ * - Text events are yielded as text is streamed from the LLM
3892
+ * - gadget_call events are yielded immediately when a gadget call is parsed
3893
+ * - gadget_result events are yielded when gadget execution completes
3894
+ *
3895
+ * The final event is always a StreamCompletionEvent containing metadata.
3885
3896
  */
3886
- async process(stream2) {
3887
- const outputs = [];
3897
+ async *process(stream2) {
3888
3898
  let finishReason = null;
3889
3899
  let usage;
3890
3900
  let didExecuteGadgets = false;
@@ -3930,14 +3940,13 @@ var init_stream_processor = __esm({
3930
3940
  continue;
3931
3941
  }
3932
3942
  for (const event of this.parser.feed(processedChunk)) {
3933
- const processedEvents = await this.processEvent(event);
3934
- outputs.push(...processedEvents);
3935
- if (processedEvents.some((e) => e.type === "gadget_result")) {
3936
- didExecuteGadgets = true;
3937
- }
3938
- for (const evt of processedEvents) {
3939
- if (evt.type === "gadget_result" && evt.result.breaksLoop) {
3940
- shouldBreakLoop = true;
3943
+ for await (const processedEvent of this.processEventGenerator(event)) {
3944
+ yield processedEvent;
3945
+ if (processedEvent.type === "gadget_result") {
3946
+ didExecuteGadgets = true;
3947
+ if (processedEvent.result.breaksLoop) {
3948
+ shouldBreakLoop = true;
3949
+ }
3941
3950
  }
3942
3951
  }
3943
3952
  }
@@ -3948,25 +3957,23 @@ var init_stream_processor = __esm({
3948
3957
  }
3949
3958
  if (!this.executionHalted) {
3950
3959
  for (const event of this.parser.finalize()) {
3951
- const processedEvents = await this.processEvent(event);
3952
- outputs.push(...processedEvents);
3953
- if (processedEvents.some((e) => e.type === "gadget_result")) {
3954
- didExecuteGadgets = true;
3955
- }
3956
- for (const evt of processedEvents) {
3957
- if (evt.type === "gadget_result" && evt.result.breaksLoop) {
3958
- shouldBreakLoop = true;
3960
+ for await (const processedEvent of this.processEventGenerator(event)) {
3961
+ yield processedEvent;
3962
+ if (processedEvent.type === "gadget_result") {
3963
+ didExecuteGadgets = true;
3964
+ if (processedEvent.result.breaksLoop) {
3965
+ shouldBreakLoop = true;
3966
+ }
3959
3967
  }
3960
3968
  }
3961
3969
  }
3962
- const finalPendingEvents = await this.processPendingGadgets();
3963
- outputs.push(...finalPendingEvents);
3964
- if (finalPendingEvents.some((e) => e.type === "gadget_result")) {
3965
- didExecuteGadgets = true;
3966
- }
3967
- for (const evt of finalPendingEvents) {
3968
- if (evt.type === "gadget_result" && evt.result.breaksLoop) {
3969
- shouldBreakLoop = true;
3970
+ for await (const evt of this.processPendingGadgetsGenerator()) {
3971
+ yield evt;
3972
+ if (evt.type === "gadget_result") {
3973
+ didExecuteGadgets = true;
3974
+ if (evt.result.breaksLoop) {
3975
+ shouldBreakLoop = true;
3976
+ }
3970
3977
  }
3971
3978
  }
3972
3979
  }
@@ -3979,8 +3986,8 @@ var init_stream_processor = __esm({
3979
3986
  };
3980
3987
  finalMessage = this.hooks.interceptors.interceptAssistantMessage(finalMessage, context);
3981
3988
  }
3982
- return {
3983
- outputs,
3989
+ const completionEvent = {
3990
+ type: "stream_complete",
3984
3991
  shouldBreakLoop,
3985
3992
  didExecuteGadgets,
3986
3993
  finishReason,
@@ -3988,9 +3995,11 @@ var init_stream_processor = __esm({
3988
3995
  rawResponse: this.responseText,
3989
3996
  finalMessage
3990
3997
  };
3998
+ yield completionEvent;
3991
3999
  }
3992
4000
  /**
3993
4001
  * Process a single parsed event (text or gadget call).
4002
+ * @deprecated Use processEventGenerator for real-time streaming
3994
4003
  */
3995
4004
  async processEvent(event) {
3996
4005
  if (event.type === "text") {
@@ -4000,6 +4009,23 @@ var init_stream_processor = __esm({
4000
4009
  }
4001
4010
  return [event];
4002
4011
  }
4012
+ /**
4013
+ * Process a single parsed event, yielding events in real-time.
4014
+ * Generator version of processEvent for streaming support.
4015
+ */
4016
+ async *processEventGenerator(event) {
4017
+ if (event.type === "text") {
4018
+ for (const e of await this.processTextEvent(event)) {
4019
+ yield e;
4020
+ }
4021
+ } else if (event.type === "gadget_call") {
4022
+ for await (const e of this.processGadgetCallGenerator(event.call)) {
4023
+ yield e;
4024
+ }
4025
+ } else {
4026
+ yield event;
4027
+ }
4028
+ }
4003
4029
  /**
4004
4030
  * Process a text event through interceptors.
4005
4031
  */
@@ -4076,9 +4102,68 @@ var init_stream_processor = __esm({
4076
4102
  events.push(...triggeredEvents);
4077
4103
  return events;
4078
4104
  }
4105
+ /**
4106
+ * Process a gadget call, yielding events in real-time.
4107
+ *
4108
+ * Key difference from processGadgetCall: yields gadget_call event IMMEDIATELY
4109
+ * when parsed (before execution), enabling real-time UI feedback.
4110
+ */
4111
+ async *processGadgetCallGenerator(call) {
4112
+ if (this.executionHalted) {
4113
+ this.logger.debug("Skipping gadget execution due to previous error", {
4114
+ gadgetName: call.gadgetName
4115
+ });
4116
+ return;
4117
+ }
4118
+ yield { type: "gadget_call", call };
4119
+ if (call.dependencies.length > 0) {
4120
+ if (call.dependencies.includes(call.invocationId)) {
4121
+ this.logger.warn("Gadget has self-referential dependency (depends on itself)", {
4122
+ gadgetName: call.gadgetName,
4123
+ invocationId: call.invocationId
4124
+ });
4125
+ this.failedInvocations.add(call.invocationId);
4126
+ const skipEvent = {
4127
+ type: "gadget_skipped",
4128
+ gadgetName: call.gadgetName,
4129
+ invocationId: call.invocationId,
4130
+ parameters: call.parameters ?? {},
4131
+ failedDependency: call.invocationId,
4132
+ failedDependencyError: `Gadget "${call.invocationId}" cannot depend on itself (self-referential dependency)`
4133
+ };
4134
+ yield skipEvent;
4135
+ return;
4136
+ }
4137
+ const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
4138
+ if (failedDep) {
4139
+ const skipEvents = await this.handleFailedDependency(call, failedDep);
4140
+ for (const evt of skipEvents) {
4141
+ yield evt;
4142
+ }
4143
+ return;
4144
+ }
4145
+ const unsatisfied = call.dependencies.filter((dep) => !this.completedResults.has(dep));
4146
+ if (unsatisfied.length > 0) {
4147
+ this.logger.debug("Queueing gadget for later - waiting on dependencies", {
4148
+ gadgetName: call.gadgetName,
4149
+ invocationId: call.invocationId,
4150
+ waitingOn: unsatisfied
4151
+ });
4152
+ this.gadgetsAwaitingDependencies.set(call.invocationId, call);
4153
+ return;
4154
+ }
4155
+ }
4156
+ for await (const evt of this.executeGadgetGenerator(call)) {
4157
+ yield evt;
4158
+ }
4159
+ for await (const evt of this.processPendingGadgetsGenerator()) {
4160
+ yield evt;
4161
+ }
4162
+ }
4079
4163
  /**
4080
4164
  * Execute a gadget through the full hook lifecycle.
4081
4165
  * This is the core execution logic, extracted from processGadgetCall.
4166
+ * @deprecated Use executeGadgetGenerator for real-time streaming
4082
4167
  */
4083
4168
  async executeGadgetWithHooks(call) {
4084
4169
  const events = [];
@@ -4231,6 +4316,159 @@ var init_stream_processor = __esm({
4231
4316
  }
4232
4317
  return events;
4233
4318
  }
4319
+ /**
4320
+ * Execute a gadget and yield the result event.
4321
+ * Generator version that yields gadget_result immediately when execution completes.
4322
+ */
4323
+ async *executeGadgetGenerator(call) {
4324
+ if (call.parseError) {
4325
+ this.logger.warn("Gadget has parse error", {
4326
+ gadgetName: call.gadgetName,
4327
+ error: call.parseError,
4328
+ rawParameters: call.parametersRaw
4329
+ });
4330
+ const shouldContinue = await this.checkCanRecoverFromError(
4331
+ call.parseError,
4332
+ call.gadgetName,
4333
+ "parse",
4334
+ call.parameters
4335
+ );
4336
+ if (!shouldContinue) {
4337
+ this.executionHalted = true;
4338
+ }
4339
+ }
4340
+ let parameters = call.parameters ?? {};
4341
+ if (this.hooks.interceptors?.interceptGadgetParameters) {
4342
+ const context = {
4343
+ iteration: this.iteration,
4344
+ gadgetName: call.gadgetName,
4345
+ invocationId: call.invocationId,
4346
+ logger: this.logger
4347
+ };
4348
+ parameters = this.hooks.interceptors.interceptGadgetParameters(parameters, context);
4349
+ }
4350
+ call.parameters = parameters;
4351
+ let shouldSkip = false;
4352
+ let syntheticResult;
4353
+ if (this.hooks.controllers?.beforeGadgetExecution) {
4354
+ const context = {
4355
+ iteration: this.iteration,
4356
+ gadgetName: call.gadgetName,
4357
+ invocationId: call.invocationId,
4358
+ parameters,
4359
+ logger: this.logger
4360
+ };
4361
+ const action = await this.hooks.controllers.beforeGadgetExecution(context);
4362
+ validateBeforeGadgetExecutionAction(action);
4363
+ if (action.action === "skip") {
4364
+ shouldSkip = true;
4365
+ syntheticResult = action.syntheticResult;
4366
+ this.logger.info("Controller skipped gadget execution", {
4367
+ gadgetName: call.gadgetName
4368
+ });
4369
+ }
4370
+ }
4371
+ const startObservers = [];
4372
+ if (this.hooks.observers?.onGadgetExecutionStart) {
4373
+ startObservers.push(async () => {
4374
+ const context = {
4375
+ iteration: this.iteration,
4376
+ gadgetName: call.gadgetName,
4377
+ invocationId: call.invocationId,
4378
+ parameters,
4379
+ logger: this.logger
4380
+ };
4381
+ await this.hooks.observers.onGadgetExecutionStart(context);
4382
+ });
4383
+ }
4384
+ await this.runObserversInParallel(startObservers);
4385
+ let result;
4386
+ if (shouldSkip) {
4387
+ result = {
4388
+ gadgetName: call.gadgetName,
4389
+ invocationId: call.invocationId,
4390
+ parameters,
4391
+ result: syntheticResult ?? "Execution skipped",
4392
+ executionTimeMs: 0
4393
+ };
4394
+ } else {
4395
+ result = await this.executor.execute(call);
4396
+ }
4397
+ const originalResult = result.result;
4398
+ if (result.result && this.hooks.interceptors?.interceptGadgetResult) {
4399
+ const context = {
4400
+ iteration: this.iteration,
4401
+ gadgetName: result.gadgetName,
4402
+ invocationId: result.invocationId,
4403
+ parameters,
4404
+ executionTimeMs: result.executionTimeMs,
4405
+ logger: this.logger
4406
+ };
4407
+ result.result = this.hooks.interceptors.interceptGadgetResult(result.result, context);
4408
+ }
4409
+ if (this.hooks.controllers?.afterGadgetExecution) {
4410
+ const context = {
4411
+ iteration: this.iteration,
4412
+ gadgetName: result.gadgetName,
4413
+ invocationId: result.invocationId,
4414
+ parameters,
4415
+ result: result.result,
4416
+ error: result.error,
4417
+ executionTimeMs: result.executionTimeMs,
4418
+ logger: this.logger
4419
+ };
4420
+ const action = await this.hooks.controllers.afterGadgetExecution(context);
4421
+ validateAfterGadgetExecutionAction(action);
4422
+ if (action.action === "recover" && result.error) {
4423
+ this.logger.info("Controller recovered from gadget error", {
4424
+ gadgetName: result.gadgetName,
4425
+ originalError: result.error
4426
+ });
4427
+ result = {
4428
+ ...result,
4429
+ error: void 0,
4430
+ result: action.fallbackResult
4431
+ };
4432
+ }
4433
+ }
4434
+ const completeObservers = [];
4435
+ if (this.hooks.observers?.onGadgetExecutionComplete) {
4436
+ completeObservers.push(async () => {
4437
+ const context = {
4438
+ iteration: this.iteration,
4439
+ gadgetName: result.gadgetName,
4440
+ invocationId: result.invocationId,
4441
+ parameters,
4442
+ originalResult,
4443
+ finalResult: result.result,
4444
+ error: result.error,
4445
+ executionTimeMs: result.executionTimeMs,
4446
+ breaksLoop: result.breaksLoop,
4447
+ cost: result.cost,
4448
+ logger: this.logger
4449
+ };
4450
+ await this.hooks.observers.onGadgetExecutionComplete(context);
4451
+ });
4452
+ }
4453
+ await this.runObserversInParallel(completeObservers);
4454
+ this.completedResults.set(result.invocationId, result);
4455
+ if (result.error) {
4456
+ this.failedInvocations.add(result.invocationId);
4457
+ }
4458
+ yield { type: "gadget_result", result };
4459
+ if (result.error) {
4460
+ const errorType = this.determineErrorType(call, result);
4461
+ const shouldContinue = await this.checkCanRecoverFromError(
4462
+ result.error,
4463
+ result.gadgetName,
4464
+ errorType,
4465
+ result.parameters
4466
+ );
4467
+ if (!shouldContinue) {
4468
+ this.executionHalted = true;
4469
+ }
4470
+ }
4471
+ }
4234
4472
  /**
4235
4473
  * Handle a gadget that cannot execute because a dependency failed.
4236
4474
  * Calls the onDependencySkipped controller to allow customization.
@@ -4387,6 +4625,99 @@ var init_stream_processor = __esm({
4387
4625
  }
4388
4626
  return events;
4389
4627
  }
4628
+ /**
4629
+ * Process pending gadgets, yielding events in real-time.
4630
+ * Generator version that yields events as gadgets complete.
4631
+ *
4632
+ * Note: Gadgets are still executed in parallel for efficiency,
4633
+ * but results are yielded as they become available.
4634
+ */
4635
+ async *processPendingGadgetsGenerator() {
4636
+ let progress = true;
4637
+ while (progress && this.gadgetsAwaitingDependencies.size > 0) {
4638
+ progress = false;
4639
+ const readyToExecute = [];
4640
+ const readyToSkip = [];
4641
+ for (const [_invocationId, call] of this.gadgetsAwaitingDependencies) {
4642
+ const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
4643
+ if (failedDep) {
4644
+ readyToSkip.push({ call, failedDep });
4645
+ continue;
4646
+ }
4647
+ const allSatisfied = call.dependencies.every((dep) => this.completedResults.has(dep));
4648
+ if (allSatisfied) {
4649
+ readyToExecute.push(call);
4650
+ }
4651
+ }
4652
+ for (const { call, failedDep } of readyToSkip) {
4653
+ this.gadgetsAwaitingDependencies.delete(call.invocationId);
4654
+ const skipEvents = await this.handleFailedDependency(call, failedDep);
4655
+ for (const evt of skipEvents) {
4656
+ yield evt;
4657
+ }
4658
+ progress = true;
4659
+ }
4660
+ if (readyToExecute.length > 0) {
4661
+ this.logger.debug("Executing ready gadgets in parallel", {
4662
+ count: readyToExecute.length,
4663
+ invocationIds: readyToExecute.map((c) => c.invocationId)
4664
+ });
4665
+ for (const call of readyToExecute) {
4666
+ this.gadgetsAwaitingDependencies.delete(call.invocationId);
4667
+ }
4668
+ const eventSets = await Promise.all(
4669
+ readyToExecute.map(async (call) => {
4670
+ const events = [];
4671
+ for await (const evt of this.executeGadgetGenerator(call)) {
4672
+ events.push(evt);
4673
+ }
4674
+ return events;
4675
+ })
4676
+ );
4677
+ for (const events of eventSets) {
4678
+ for (const evt of events) {
4679
+ yield evt;
4680
+ }
4681
+ }
4682
+ progress = true;
4683
+ }
4684
+ }
4685
+ if (this.gadgetsAwaitingDependencies.size > 0) {
4686
+ const pendingIds = new Set(this.gadgetsAwaitingDependencies.keys());
4687
+ for (const [invocationId, call] of this.gadgetsAwaitingDependencies) {
4688
+ const missingDeps = call.dependencies.filter((dep) => !this.completedResults.has(dep));
4689
+ const circularDeps = missingDeps.filter((dep) => pendingIds.has(dep));
4690
+ const trulyMissingDeps = missingDeps.filter((dep) => !pendingIds.has(dep));
4691
+ let errorMessage;
4692
+ let logLevel = "warn";
4693
+ if (circularDeps.length > 0 && trulyMissingDeps.length > 0) {
4694
+ errorMessage = `Dependencies unresolvable: circular=[${circularDeps.join(", ")}], missing=[${trulyMissingDeps.join(", ")}]`;
4695
+ logLevel = "error";
4696
+ } else if (circularDeps.length > 0) {
4697
+ errorMessage = `Circular dependency detected: "${invocationId}" depends on "${circularDeps[0]}" which also depends on "${invocationId}" (directly or indirectly)`;
4698
+ } else {
4699
+ errorMessage = `Dependency "${missingDeps[0]}" was never executed - check that the invocation ID exists and is spelled correctly`;
4700
+ }
4701
+ this.logger[logLevel]("Gadget has unresolvable dependencies", {
4702
+ gadgetName: call.gadgetName,
4703
+ invocationId,
4704
+ circularDependencies: circularDeps,
4705
+ missingDependencies: trulyMissingDeps
4706
+ });
4707
+ this.failedInvocations.add(invocationId);
4708
+ const skipEvent = {
4709
+ type: "gadget_skipped",
4710
+ gadgetName: call.gadgetName,
4711
+ invocationId,
4712
+ parameters: call.parameters ?? {},
4713
+ failedDependency: missingDeps[0],
4714
+ failedDependencyError: errorMessage
4715
+ };
4716
+ yield skipEvent;
4717
+ }
4718
+ this.gadgetsAwaitingDependencies.clear();
4719
+ }
4720
+ }
4390
4721
  /**
4391
4722
  * Safely execute an observer, catching and logging any errors.
4392
4723
  * Observers are non-critical, so errors are logged but don't crash the system.
@@ -4509,6 +4840,8 @@ var init_agent = __esm({
4509
4840
  // Subagent configuration
4510
4841
  agentContextConfig;
4511
4842
  subagentConfig;
4843
+ // Nested event callback for subagent gadgets
4844
+ onNestedEvent;
4512
4845
  /**
4513
4846
  * Creates a new Agent instance.
4514
4847
  * @internal This constructor is private. Use LLMist.createAgent() or AgentBuilder instead.
@@ -4586,6 +4919,7 @@ var init_agent = __esm({
4586
4919
  temperature: this.temperature
4587
4920
  };
4588
4921
  this.subagentConfig = options.subagentConfig;
4922
+ this.onNestedEvent = options.onNestedEvent;
4589
4923
  }
4590
4924
  /**
4591
4925
  * Get the gadget registry for this agent.
@@ -4816,12 +5150,30 @@ var init_agent = __esm({
4816
5150
  client: this.client,
4817
5151
  mediaStore: this.mediaStore,
4818
5152
  agentConfig: this.agentContextConfig,
4819
- subagentConfig: this.subagentConfig
5153
+ subagentConfig: this.subagentConfig,
5154
+ onNestedEvent: this.onNestedEvent
4820
5155
  });
4821
- const result = await processor.process(stream2);
4822
- for (const output of result.outputs) {
4823
- yield output;
5156
+ let streamMetadata = null;
5157
+ let gadgetCallCount = 0;
5158
+ const textOutputs = [];
5159
+ const gadgetResults = [];
5160
+ for await (const event of processor.process(stream2)) {
5161
+ if (event.type === "stream_complete") {
5162
+ streamMetadata = event;
5163
+ continue;
5164
+ }
5165
+ if (event.type === "text") {
5166
+ textOutputs.push(event.content);
5167
+ } else if (event.type === "gadget_result") {
5168
+ gadgetCallCount++;
5169
+ gadgetResults.push(event);
5170
+ }
5171
+ yield event;
4824
5172
  }
5173
+ if (!streamMetadata) {
5174
+ throw new Error("Stream processing completed without metadata event");
5175
+ }
5176
+ const result = streamMetadata;
4825
5177
  this.logger.info("LLM response completed", {
4826
5178
  finishReason: result.finishReason,
4827
5179
  usage: result.usage,
@@ -4846,9 +5198,6 @@ var init_agent = __esm({
4846
5198
  });
4847
5199
  let finalMessage = result.finalMessage;
4848
5200
  if (this.hooks.controllers?.afterLLMCall) {
4849
- const gadgetCallCount = result.outputs.filter(
4850
- (output) => output.type === "gadget_result"
4851
- ).length;
4852
5201
  const context = {
4853
5202
  iteration: currentIteration,
4854
5203
  maxIterations: this.maxIterations,
@@ -4878,9 +5227,7 @@ var init_agent = __esm({
4878
5227
  }
4879
5228
  if (result.didExecuteGadgets) {
4880
5229
  if (this.textWithGadgetsHandler) {
4881
- const textContent = result.outputs.filter(
4882
- (output) => output.type === "text"
4883
- ).map((output) => output.content).join("");
5230
+ const textContent = textOutputs.join("");
4884
5231
  if (textContent.trim()) {
4885
5232
  const { gadgetName, parameterMapping, resultMapping } = this.textWithGadgetsHandler;
4886
5233
  this.conversation.addGadgetCallResult(
@@ -4890,7 +5237,7 @@ var init_agent = __esm({
4890
5237
  );
4891
5238
  }
4892
5239
  }
4893
- for (const output of result.outputs) {
5240
+ for (const output of gadgetResults) {
4894
5241
  if (output.type === "gadget_result") {
4895
5242
  const gadgetResult = output.result;
4896
5243
  this.conversation.addGadgetCallResult(
@@ -5127,6 +5474,8 @@ var init_builder = __esm({
5127
5474
  signal;
5128
5475
  trailingMessage;
5129
5476
  subagentConfig;
5477
+ nestedEventCallback;
5478
+ parentContext;
5130
5479
  constructor(client) {
5131
5480
  this.client = client;
5132
5481
  }
@@ -5623,6 +5972,74 @@ var init_builder = __esm({
5623
5972
  this.subagentConfig = config;
5624
5973
  return this;
5625
5974
  }
5975
+ /**
5976
+ * Set the callback for nested subagent events.
5977
+ *
5978
+ * Subagent gadgets (like BrowseWeb) can use ExecutionContext.onNestedEvent
5979
+ * to report their internal LLM calls and gadget executions in real-time.
5980
+ * This callback receives those events, enabling hierarchical progress display.
5981
+ *
5982
+ * @param callback - Function to handle nested agent events
5983
+ * @returns This builder for chaining
5984
+ *
5985
+ * @example
5986
+ * ```typescript
5987
+ * .withNestedEventCallback((event) => {
5988
+ * if (event.type === "llm_call_start") {
5989
+ * console.log(` Nested LLM #${event.event.iteration} starting...`);
5990
+ * } else if (event.type === "gadget_call") {
5991
+ * console.log(` ⏵ ${event.event.call.gadgetName}...`);
5992
+ * }
5993
+ * })
5994
+ * ```
5995
+ */
5996
+ withNestedEventCallback(callback) {
5997
+ this.nestedEventCallback = callback;
5998
+ return this;
5999
+ }
6000
+ /**
6001
+ * Enable automatic nested event forwarding to parent agent.
6002
+ *
6003
+ * When building a subagent inside a gadget, call this method to automatically
6004
+ * forward all LLM calls and gadget events to the parent agent. This enables
6005
+ * hierarchical progress display without any manual event handling.
6006
+ *
6007
+ * The method extracts `invocationId` and `onNestedEvent` from the execution
6008
+ * context and sets up automatic forwarding via hooks and event wrapping.
6009
+ *
6010
+ * @param ctx - ExecutionContext passed to the gadget's execute() method
6011
+ * @param depth - Nesting depth (default: 1 for direct child)
6012
+ * @returns This builder for chaining
6013
+ *
6014
+ * @example
6015
+ * ```typescript
6016
+ * // In a subagent gadget like BrowseWeb - ONE LINE enables auto-forwarding:
6017
+ * execute: async (params, ctx) => {
6018
+ * const agent = new AgentBuilder(client)
6019
+ * .withModel(model)
6020
+ * .withGadgets(Navigate, Click, Screenshot)
6021
+ * .withParentContext(ctx) // <-- This is all you need!
6022
+ * .ask(params.task);
6023
+ *
6024
+ * for await (const event of agent.run()) {
6025
+ * // Events automatically forwarded - just process normally
6026
+ * if (event.type === "text") {
6027
+ * result = event.content;
6028
+ * }
6029
+ * }
6030
+ * }
6031
+ * ```
6032
+ */
6033
+ withParentContext(ctx, depth = 1) {
6034
+ if (ctx.onNestedEvent && ctx.invocationId) {
6035
+ this.parentContext = {
6036
+ invocationId: ctx.invocationId,
6037
+ onNestedEvent: ctx.onNestedEvent,
6038
+ depth
6039
+ };
6040
+ }
6041
+ return this;
6042
+ }
5626
6043
  /**
5627
6044
  * Add an ephemeral trailing message that appears at the end of each LLM request.
5628
6045
  *
@@ -5690,14 +6107,58 @@ ${endPrefix}`
5690
6107
  return this;
5691
6108
  }
5692
6109
  /**
5693
- * Compose the final hooks, including trailing message if configured.
6110
+ * Compose the final hooks, including:
6111
+ * - Trailing message injection (if configured)
6112
+ * - Nested event forwarding for LLM calls (if parentContext is set)
5694
6113
  */
5695
6114
  composeHooks() {
6115
+ let hooks = this.hooks;
6116
+ if (this.parentContext) {
6117
+ const { invocationId, onNestedEvent, depth } = this.parentContext;
6118
+ const existingOnLLMCallStart = hooks?.observers?.onLLMCallStart;
6119
+ const existingOnLLMCallComplete = hooks?.observers?.onLLMCallComplete;
6120
+ hooks = {
6121
+ ...hooks,
6122
+ observers: {
6123
+ ...hooks?.observers,
6124
+ onLLMCallStart: async (context) => {
6125
+ onNestedEvent({
6126
+ type: "llm_call_start",
6127
+ gadgetInvocationId: invocationId,
6128
+ depth,
6129
+ event: {
6130
+ iteration: context.iteration,
6131
+ model: context.options.model
6132
+ }
6133
+ });
6134
+ if (existingOnLLMCallStart) {
6135
+ await existingOnLLMCallStart(context);
6136
+ }
6137
+ },
6138
+ onLLMCallComplete: async (context) => {
6139
+ onNestedEvent({
6140
+ type: "llm_call_end",
6141
+ gadgetInvocationId: invocationId,
6142
+ depth,
6143
+ event: {
6144
+ iteration: context.iteration,
6145
+ model: context.options.model,
6146
+ outputTokens: context.usage?.outputTokens,
6147
+ finishReason: context.finishReason
6148
+ }
6149
+ });
6150
+ if (existingOnLLMCallComplete) {
6151
+ await existingOnLLMCallComplete(context);
6152
+ }
6153
+ }
6154
+ }
6155
+ };
6156
+ }
5696
6157
  if (!this.trailingMessage) {
5697
- return this.hooks;
6158
+ return hooks;
5698
6159
  }
5699
6160
  const trailingMsg = this.trailingMessage;
5700
- const existingBeforeLLMCall = this.hooks?.controllers?.beforeLLMCall;
6161
+ const existingBeforeLLMCall = hooks?.controllers?.beforeLLMCall;
5701
6162
  const trailingMessageController = async (ctx) => {
5702
6163
  const result = existingBeforeLLMCall ? await existingBeforeLLMCall(ctx) : { action: "proceed" };
5703
6164
  if (result.action === "skip") {
@@ -5712,9 +6173,9 @@ ${endPrefix}`
5712
6173
  };
5713
6174
  };
5714
6175
  return {
5715
- ...this.hooks,
6176
+ ...hooks,
5716
6177
  controllers: {
5717
- ...this.hooks?.controllers,
6178
+ ...hooks?.controllers,
5718
6179
  beforeLLMCall: trailingMessageController
5719
6180
  }
5720
6181
  };
@@ -5775,6 +6236,19 @@ ${endPrefix}`
5775
6236
  this.client = new LLMistClass();
5776
6237
  }
5777
6238
  const registry = GadgetRegistry.from(this.gadgets);
6239
+ let onNestedEvent = this.nestedEventCallback;
6240
+ if (this.parentContext) {
6241
+ const { invocationId, onNestedEvent: parentCallback, depth } = this.parentContext;
6242
+ const existingCallback = this.nestedEventCallback;
6243
+ onNestedEvent = (event) => {
6244
+ parentCallback({
6245
+ ...event,
6246
+ gadgetInvocationId: invocationId,
6247
+ depth: event.depth + depth
6248
+ });
6249
+ existingCallback?.(event);
6250
+ };
6251
+ }
5778
6252
  return {
5779
6253
  client: this.client,
5780
6254
  model: this.model ?? "openai:gpt-5-nano",
@@ -5800,7 +6274,8 @@ ${endPrefix}`
5800
6274
  gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
5801
6275
  compactionConfig: this.compactionConfig,
5802
6276
  signal: this.signal,
5803
- subagentConfig: this.subagentConfig
6277
+ subagentConfig: this.subagentConfig,
6278
+ onNestedEvent
5804
6279
  };
5805
6280
  }
5806
6281
  ask(userPrompt) {
@@ -5957,6 +6432,19 @@ ${endPrefix}`
5957
6432
  this.client = new LLMistClass();
5958
6433
  }
5959
6434
  const registry = GadgetRegistry.from(this.gadgets);
6435
+ let onNestedEvent = this.nestedEventCallback;
6436
+ if (this.parentContext) {
6437
+ const { invocationId, onNestedEvent: parentCallback, depth } = this.parentContext;
6438
+ const existingCallback = this.nestedEventCallback;
6439
+ onNestedEvent = (event) => {
6440
+ parentCallback({
6441
+ ...event,
6442
+ gadgetInvocationId: invocationId,
6443
+ depth: event.depth + depth
6444
+ });
6445
+ existingCallback?.(event);
6446
+ };
6447
+ }
5960
6448
  const options = {
5961
6449
  client: this.client,
5962
6450
  model: this.model ?? "openai:gpt-5-nano",
@@ -5982,7 +6470,8 @@ ${endPrefix}`
5982
6470
  gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
5983
6471
  compactionConfig: this.compactionConfig,
5984
6472
  signal: this.signal,
5985
- subagentConfig: this.subagentConfig
6473
+ subagentConfig: this.subagentConfig,
6474
+ onNestedEvent
5986
6475
  };
5987
6476
  return new Agent(AGENT_INTERNAL_KEY, options);
5988
6477
  }