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.
package/dist/index.cjs CHANGED
@@ -3562,7 +3562,7 @@ var init_executor = __esm({
3562
3562
  init_exceptions();
3563
3563
  init_parser();
3564
3564
  GadgetExecutor = class {
3565
- constructor(registry, requestHumanInput, logger, defaultGadgetTimeoutMs, errorFormatterOptions, client, mediaStore, agentConfig, subagentConfig) {
3565
+ constructor(registry, requestHumanInput, logger, defaultGadgetTimeoutMs, errorFormatterOptions, client, mediaStore, agentConfig, subagentConfig, onNestedEvent) {
3566
3566
  this.registry = registry;
3567
3567
  this.requestHumanInput = requestHumanInput;
3568
3568
  this.defaultGadgetTimeoutMs = defaultGadgetTimeoutMs;
@@ -3570,6 +3570,7 @@ var init_executor = __esm({
3570
3570
  this.mediaStore = mediaStore;
3571
3571
  this.agentConfig = agentConfig;
3572
3572
  this.subagentConfig = subagentConfig;
3573
+ this.onNestedEvent = onNestedEvent;
3573
3574
  this.logger = logger ?? createLogger({ name: "llmist:executor" });
3574
3575
  this.errorFormatter = new GadgetExecutionErrorFormatter(errorFormatterOptions);
3575
3576
  this.argPrefix = errorFormatterOptions?.argPrefix ?? GADGET_ARG_PREFIX;
@@ -3715,7 +3716,9 @@ var init_executor = __esm({
3715
3716
  llmist: this.client ? new CostReportingLLMistWrapper(this.client, reportCost) : void 0,
3716
3717
  signal: abortController.signal,
3717
3718
  agentConfig: this.agentConfig,
3718
- subagentConfig: this.subagentConfig
3719
+ subagentConfig: this.subagentConfig,
3720
+ invocationId: call.invocationId,
3721
+ onNestedEvent: this.onNestedEvent
3719
3722
  };
3720
3723
  let rawResult;
3721
3724
  if (timeoutMs && timeoutMs > 0) {
@@ -3954,14 +3957,21 @@ var init_stream_processor = __esm({
3954
3957
  options.client,
3955
3958
  options.mediaStore,
3956
3959
  options.agentConfig,
3957
- options.subagentConfig
3960
+ options.subagentConfig,
3961
+ options.onNestedEvent
3958
3962
  );
3959
3963
  }
3960
3964
  /**
3961
- * Process an LLM stream and return structured results.
3965
+ * Process an LLM stream and yield events in real-time.
3966
+ *
3967
+ * This is an async generator that yields events immediately as they occur:
3968
+ * - Text events are yielded as text is streamed from the LLM
3969
+ * - gadget_call events are yielded immediately when a gadget call is parsed
3970
+ * - gadget_result events are yielded when gadget execution completes
3971
+ *
3972
+ * The final event is always a StreamCompletionEvent containing metadata.
3962
3973
  */
3963
- async process(stream2) {
3964
- const outputs = [];
3974
+ async *process(stream2) {
3965
3975
  let finishReason = null;
3966
3976
  let usage;
3967
3977
  let didExecuteGadgets = false;
@@ -4007,14 +4017,13 @@ var init_stream_processor = __esm({
4007
4017
  continue;
4008
4018
  }
4009
4019
  for (const event of this.parser.feed(processedChunk)) {
4010
- const processedEvents = await this.processEvent(event);
4011
- outputs.push(...processedEvents);
4012
- if (processedEvents.some((e) => e.type === "gadget_result")) {
4013
- didExecuteGadgets = true;
4014
- }
4015
- for (const evt of processedEvents) {
4016
- if (evt.type === "gadget_result" && evt.result.breaksLoop) {
4017
- shouldBreakLoop = true;
4020
+ for await (const processedEvent of this.processEventGenerator(event)) {
4021
+ yield processedEvent;
4022
+ if (processedEvent.type === "gadget_result") {
4023
+ didExecuteGadgets = true;
4024
+ if (processedEvent.result.breaksLoop) {
4025
+ shouldBreakLoop = true;
4026
+ }
4018
4027
  }
4019
4028
  }
4020
4029
  }
@@ -4025,25 +4034,23 @@ var init_stream_processor = __esm({
4025
4034
  }
4026
4035
  if (!this.executionHalted) {
4027
4036
  for (const event of this.parser.finalize()) {
4028
- const processedEvents = await this.processEvent(event);
4029
- outputs.push(...processedEvents);
4030
- if (processedEvents.some((e) => e.type === "gadget_result")) {
4031
- didExecuteGadgets = true;
4032
- }
4033
- for (const evt of processedEvents) {
4034
- if (evt.type === "gadget_result" && evt.result.breaksLoop) {
4035
- shouldBreakLoop = true;
4037
+ for await (const processedEvent of this.processEventGenerator(event)) {
4038
+ yield processedEvent;
4039
+ if (processedEvent.type === "gadget_result") {
4040
+ didExecuteGadgets = true;
4041
+ if (processedEvent.result.breaksLoop) {
4042
+ shouldBreakLoop = true;
4043
+ }
4036
4044
  }
4037
4045
  }
4038
4046
  }
4039
- const finalPendingEvents = await this.processPendingGadgets();
4040
- outputs.push(...finalPendingEvents);
4041
- if (finalPendingEvents.some((e) => e.type === "gadget_result")) {
4042
- didExecuteGadgets = true;
4043
- }
4044
- for (const evt of finalPendingEvents) {
4045
- if (evt.type === "gadget_result" && evt.result.breaksLoop) {
4046
- shouldBreakLoop = true;
4047
+ for await (const evt of this.processPendingGadgetsGenerator()) {
4048
+ yield evt;
4049
+ if (evt.type === "gadget_result") {
4050
+ didExecuteGadgets = true;
4051
+ if (evt.result.breaksLoop) {
4052
+ shouldBreakLoop = true;
4053
+ }
4047
4054
  }
4048
4055
  }
4049
4056
  }
@@ -4056,8 +4063,8 @@ var init_stream_processor = __esm({
4056
4063
  };
4057
4064
  finalMessage = this.hooks.interceptors.interceptAssistantMessage(finalMessage, context);
4058
4065
  }
4059
- return {
4060
- outputs,
4066
+ const completionEvent = {
4067
+ type: "stream_complete",
4061
4068
  shouldBreakLoop,
4062
4069
  didExecuteGadgets,
4063
4070
  finishReason,
@@ -4065,9 +4072,11 @@ var init_stream_processor = __esm({
4065
4072
  rawResponse: this.responseText,
4066
4073
  finalMessage
4067
4074
  };
4075
+ yield completionEvent;
4068
4076
  }
4069
4077
  /**
4070
4078
  * Process a single parsed event (text or gadget call).
4079
+ * @deprecated Use processEventGenerator for real-time streaming
4071
4080
  */
4072
4081
  async processEvent(event) {
4073
4082
  if (event.type === "text") {
@@ -4077,6 +4086,23 @@ var init_stream_processor = __esm({
4077
4086
  }
4078
4087
  return [event];
4079
4088
  }
4089
+ /**
4090
+ * Process a single parsed event, yielding events in real-time.
4091
+ * Generator version of processEvent for streaming support.
4092
+ */
4093
+ async *processEventGenerator(event) {
4094
+ if (event.type === "text") {
4095
+ for (const e of await this.processTextEvent(event)) {
4096
+ yield e;
4097
+ }
4098
+ } else if (event.type === "gadget_call") {
4099
+ for await (const e of this.processGadgetCallGenerator(event.call)) {
4100
+ yield e;
4101
+ }
4102
+ } else {
4103
+ yield event;
4104
+ }
4105
+ }
4080
4106
  /**
4081
4107
  * Process a text event through interceptors.
4082
4108
  */
@@ -4153,9 +4179,68 @@ var init_stream_processor = __esm({
4153
4179
  events.push(...triggeredEvents);
4154
4180
  return events;
4155
4181
  }
4182
+ /**
4183
+ * Process a gadget call, yielding events in real-time.
4184
+ *
4185
+ * Key difference from processGadgetCall: yields gadget_call event IMMEDIATELY
4186
+ * when parsed (before execution), enabling real-time UI feedback.
4187
+ */
4188
+ async *processGadgetCallGenerator(call) {
4189
+ if (this.executionHalted) {
4190
+ this.logger.debug("Skipping gadget execution due to previous error", {
4191
+ gadgetName: call.gadgetName
4192
+ });
4193
+ return;
4194
+ }
4195
+ yield { type: "gadget_call", call };
4196
+ if (call.dependencies.length > 0) {
4197
+ if (call.dependencies.includes(call.invocationId)) {
4198
+ this.logger.warn("Gadget has self-referential dependency (depends on itself)", {
4199
+ gadgetName: call.gadgetName,
4200
+ invocationId: call.invocationId
4201
+ });
4202
+ this.failedInvocations.add(call.invocationId);
4203
+ const skipEvent = {
4204
+ type: "gadget_skipped",
4205
+ gadgetName: call.gadgetName,
4206
+ invocationId: call.invocationId,
4207
+ parameters: call.parameters ?? {},
4208
+ failedDependency: call.invocationId,
4209
+ failedDependencyError: `Gadget "${call.invocationId}" cannot depend on itself (self-referential dependency)`
4210
+ };
4211
+ yield skipEvent;
4212
+ return;
4213
+ }
4214
+ const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
4215
+ if (failedDep) {
4216
+ const skipEvents = await this.handleFailedDependency(call, failedDep);
4217
+ for (const evt of skipEvents) {
4218
+ yield evt;
4219
+ }
4220
+ return;
4221
+ }
4222
+ const unsatisfied = call.dependencies.filter((dep) => !this.completedResults.has(dep));
4223
+ if (unsatisfied.length > 0) {
4224
+ this.logger.debug("Queueing gadget for later - waiting on dependencies", {
4225
+ gadgetName: call.gadgetName,
4226
+ invocationId: call.invocationId,
4227
+ waitingOn: unsatisfied
4228
+ });
4229
+ this.gadgetsAwaitingDependencies.set(call.invocationId, call);
4230
+ return;
4231
+ }
4232
+ }
4233
+ for await (const evt of this.executeGadgetGenerator(call)) {
4234
+ yield evt;
4235
+ }
4236
+ for await (const evt of this.processPendingGadgetsGenerator()) {
4237
+ yield evt;
4238
+ }
4239
+ }
4156
4240
  /**
4157
4241
  * Execute a gadget through the full hook lifecycle.
4158
4242
  * This is the core execution logic, extracted from processGadgetCall.
4243
+ * @deprecated Use executeGadgetGenerator for real-time streaming
4159
4244
  */
4160
4245
  async executeGadgetWithHooks(call) {
4161
4246
  const events = [];
@@ -4308,6 +4393,159 @@ var init_stream_processor = __esm({
4308
4393
  }
4309
4394
  return events;
4310
4395
  }
4396
+ /**
4397
+ * Execute a gadget and yield the result event.
4398
+ * Generator version that yields gadget_result immediately when execution completes.
4399
+ */
4400
+ async *executeGadgetGenerator(call) {
4401
+ if (call.parseError) {
4402
+ this.logger.warn("Gadget has parse error", {
4403
+ gadgetName: call.gadgetName,
4404
+ error: call.parseError,
4405
+ rawParameters: call.parametersRaw
4406
+ });
4407
+ const shouldContinue = await this.checkCanRecoverFromError(
4408
+ call.parseError,
4409
+ call.gadgetName,
4410
+ "parse",
4411
+ call.parameters
4412
+ );
4413
+ if (!shouldContinue) {
4414
+ this.executionHalted = true;
4415
+ }
4416
+ }
4417
+ let parameters = call.parameters ?? {};
4418
+ if (this.hooks.interceptors?.interceptGadgetParameters) {
4419
+ const context = {
4420
+ iteration: this.iteration,
4421
+ gadgetName: call.gadgetName,
4422
+ invocationId: call.invocationId,
4423
+ logger: this.logger
4424
+ };
4425
+ parameters = this.hooks.interceptors.interceptGadgetParameters(parameters, context);
4426
+ }
4427
+ call.parameters = parameters;
4428
+ let shouldSkip = false;
4429
+ let syntheticResult;
4430
+ if (this.hooks.controllers?.beforeGadgetExecution) {
4431
+ const context = {
4432
+ iteration: this.iteration,
4433
+ gadgetName: call.gadgetName,
4434
+ invocationId: call.invocationId,
4435
+ parameters,
4436
+ logger: this.logger
4437
+ };
4438
+ const action = await this.hooks.controllers.beforeGadgetExecution(context);
4439
+ validateBeforeGadgetExecutionAction(action);
4440
+ if (action.action === "skip") {
4441
+ shouldSkip = true;
4442
+ syntheticResult = action.syntheticResult;
4443
+ this.logger.info("Controller skipped gadget execution", {
4444
+ gadgetName: call.gadgetName
4445
+ });
4446
+ }
4447
+ }
4448
+ const startObservers = [];
4449
+ if (this.hooks.observers?.onGadgetExecutionStart) {
4450
+ startObservers.push(async () => {
4451
+ const context = {
4452
+ iteration: this.iteration,
4453
+ gadgetName: call.gadgetName,
4454
+ invocationId: call.invocationId,
4455
+ parameters,
4456
+ logger: this.logger
4457
+ };
4458
+ await this.hooks.observers.onGadgetExecutionStart(context);
4459
+ });
4460
+ }
4461
+ await this.runObserversInParallel(startObservers);
4462
+ let result;
4463
+ if (shouldSkip) {
4464
+ result = {
4465
+ gadgetName: call.gadgetName,
4466
+ invocationId: call.invocationId,
4467
+ parameters,
4468
+ result: syntheticResult ?? "Execution skipped",
4469
+ executionTimeMs: 0
4470
+ };
4471
+ } else {
4472
+ result = await this.executor.execute(call);
4473
+ }
4474
+ const originalResult = result.result;
4475
+ if (result.result && this.hooks.interceptors?.interceptGadgetResult) {
4476
+ const context = {
4477
+ iteration: this.iteration,
4478
+ gadgetName: result.gadgetName,
4479
+ invocationId: result.invocationId,
4480
+ parameters,
4481
+ executionTimeMs: result.executionTimeMs,
4482
+ logger: this.logger
4483
+ };
4484
+ result.result = this.hooks.interceptors.interceptGadgetResult(result.result, context);
4485
+ }
4486
+ if (this.hooks.controllers?.afterGadgetExecution) {
4487
+ const context = {
4488
+ iteration: this.iteration,
4489
+ gadgetName: result.gadgetName,
4490
+ invocationId: result.invocationId,
4491
+ parameters,
4492
+ result: result.result,
4493
+ error: result.error,
4494
+ executionTimeMs: result.executionTimeMs,
4495
+ logger: this.logger
4496
+ };
4497
+ const action = await this.hooks.controllers.afterGadgetExecution(context);
4498
+ validateAfterGadgetExecutionAction(action);
4499
+ if (action.action === "recover" && result.error) {
4500
+ this.logger.info("Controller recovered from gadget error", {
4501
+ gadgetName: result.gadgetName,
4502
+ originalError: result.error
4503
+ });
4504
+ result = {
4505
+ ...result,
4506
+ error: void 0,
4507
+ result: action.fallbackResult
4508
+ };
4509
+ }
4510
+ }
4511
+ const completeObservers = [];
4512
+ if (this.hooks.observers?.onGadgetExecutionComplete) {
4513
+ completeObservers.push(async () => {
4514
+ const context = {
4515
+ iteration: this.iteration,
4516
+ gadgetName: result.gadgetName,
4517
+ invocationId: result.invocationId,
4518
+ parameters,
4519
+ originalResult,
4520
+ finalResult: result.result,
4521
+ error: result.error,
4522
+ executionTimeMs: result.executionTimeMs,
4523
+ breaksLoop: result.breaksLoop,
4524
+ cost: result.cost,
4525
+ logger: this.logger
4526
+ };
4527
+ await this.hooks.observers.onGadgetExecutionComplete(context);
4528
+ });
4529
+ }
4530
+ await this.runObserversInParallel(completeObservers);
4531
+ this.completedResults.set(result.invocationId, result);
4532
+ if (result.error) {
4533
+ this.failedInvocations.add(result.invocationId);
4534
+ }
4535
+ yield { type: "gadget_result", result };
4536
+ if (result.error) {
4537
+ const errorType = this.determineErrorType(call, result);
4538
+ const shouldContinue = await this.checkCanRecoverFromError(
4539
+ result.error,
4540
+ result.gadgetName,
4541
+ errorType,
4542
+ result.parameters
4543
+ );
4544
+ if (!shouldContinue) {
4545
+ this.executionHalted = true;
4546
+ }
4547
+ }
4548
+ }
4311
4549
  /**
4312
4550
  * Handle a gadget that cannot execute because a dependency failed.
4313
4551
  * Calls the onDependencySkipped controller to allow customization.
@@ -4464,6 +4702,99 @@ var init_stream_processor = __esm({
4464
4702
  }
4465
4703
  return events;
4466
4704
  }
4705
+ /**
4706
+ * Process pending gadgets, yielding events in real-time.
4707
+ * Generator version that yields events as gadgets complete.
4708
+ *
4709
+ * Note: Gadgets are still executed in parallel for efficiency,
4710
+ * but results are yielded as they become available.
4711
+ */
4712
+ async *processPendingGadgetsGenerator() {
4713
+ let progress = true;
4714
+ while (progress && this.gadgetsAwaitingDependencies.size > 0) {
4715
+ progress = false;
4716
+ const readyToExecute = [];
4717
+ const readyToSkip = [];
4718
+ for (const [_invocationId, call] of this.gadgetsAwaitingDependencies) {
4719
+ const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
4720
+ if (failedDep) {
4721
+ readyToSkip.push({ call, failedDep });
4722
+ continue;
4723
+ }
4724
+ const allSatisfied = call.dependencies.every((dep) => this.completedResults.has(dep));
4725
+ if (allSatisfied) {
4726
+ readyToExecute.push(call);
4727
+ }
4728
+ }
4729
+ for (const { call, failedDep } of readyToSkip) {
4730
+ this.gadgetsAwaitingDependencies.delete(call.invocationId);
4731
+ const skipEvents = await this.handleFailedDependency(call, failedDep);
4732
+ for (const evt of skipEvents) {
4733
+ yield evt;
4734
+ }
4735
+ progress = true;
4736
+ }
4737
+ if (readyToExecute.length > 0) {
4738
+ this.logger.debug("Executing ready gadgets in parallel", {
4739
+ count: readyToExecute.length,
4740
+ invocationIds: readyToExecute.map((c) => c.invocationId)
4741
+ });
4742
+ for (const call of readyToExecute) {
4743
+ this.gadgetsAwaitingDependencies.delete(call.invocationId);
4744
+ }
4745
+ const eventSets = await Promise.all(
4746
+ readyToExecute.map(async (call) => {
4747
+ const events = [];
4748
+ for await (const evt of this.executeGadgetGenerator(call)) {
4749
+ events.push(evt);
4750
+ }
4751
+ return events;
4752
+ })
4753
+ );
4754
+ for (const events of eventSets) {
4755
+ for (const evt of events) {
4756
+ yield evt;
4757
+ }
4758
+ }
4759
+ progress = true;
4760
+ }
4761
+ }
4762
+ if (this.gadgetsAwaitingDependencies.size > 0) {
4763
+ const pendingIds = new Set(this.gadgetsAwaitingDependencies.keys());
4764
+ for (const [invocationId, call] of this.gadgetsAwaitingDependencies) {
4765
+ const missingDeps = call.dependencies.filter((dep) => !this.completedResults.has(dep));
4766
+ const circularDeps = missingDeps.filter((dep) => pendingIds.has(dep));
4767
+ const trulyMissingDeps = missingDeps.filter((dep) => !pendingIds.has(dep));
4768
+ let errorMessage;
4769
+ let logLevel = "warn";
4770
+ if (circularDeps.length > 0 && trulyMissingDeps.length > 0) {
4771
+ errorMessage = `Dependencies unresolvable: circular=[${circularDeps.join(", ")}], missing=[${trulyMissingDeps.join(", ")}]`;
4772
+ logLevel = "error";
4773
+ } else if (circularDeps.length > 0) {
4774
+ errorMessage = `Circular dependency detected: "${invocationId}" depends on "${circularDeps[0]}" which also depends on "${invocationId}" (directly or indirectly)`;
4775
+ } else {
4776
+ errorMessage = `Dependency "${missingDeps[0]}" was never executed - check that the invocation ID exists and is spelled correctly`;
4777
+ }
4778
+ this.logger[logLevel]("Gadget has unresolvable dependencies", {
4779
+ gadgetName: call.gadgetName,
4780
+ invocationId,
4781
+ circularDependencies: circularDeps,
4782
+ missingDependencies: trulyMissingDeps
4783
+ });
4784
+ this.failedInvocations.add(invocationId);
4785
+ const skipEvent = {
4786
+ type: "gadget_skipped",
4787
+ gadgetName: call.gadgetName,
4788
+ invocationId,
4789
+ parameters: call.parameters ?? {},
4790
+ failedDependency: missingDeps[0],
4791
+ failedDependencyError: errorMessage
4792
+ };
4793
+ yield skipEvent;
4794
+ }
4795
+ this.gadgetsAwaitingDependencies.clear();
4796
+ }
4797
+ }
4467
4798
  /**
4468
4799
  * Safely execute an observer, catching and logging any errors.
4469
4800
  * Observers are non-critical, so errors are logged but don't crash the system.
@@ -4586,6 +4917,8 @@ var init_agent = __esm({
4586
4917
  // Subagent configuration
4587
4918
  agentContextConfig;
4588
4919
  subagentConfig;
4920
+ // Nested event callback for subagent gadgets
4921
+ onNestedEvent;
4589
4922
  /**
4590
4923
  * Creates a new Agent instance.
4591
4924
  * @internal This constructor is private. Use LLMist.createAgent() or AgentBuilder instead.
@@ -4663,6 +4996,7 @@ var init_agent = __esm({
4663
4996
  temperature: this.temperature
4664
4997
  };
4665
4998
  this.subagentConfig = options.subagentConfig;
4999
+ this.onNestedEvent = options.onNestedEvent;
4666
5000
  }
4667
5001
  /**
4668
5002
  * Get the gadget registry for this agent.
@@ -4893,12 +5227,30 @@ var init_agent = __esm({
4893
5227
  client: this.client,
4894
5228
  mediaStore: this.mediaStore,
4895
5229
  agentConfig: this.agentContextConfig,
4896
- subagentConfig: this.subagentConfig
5230
+ subagentConfig: this.subagentConfig,
5231
+ onNestedEvent: this.onNestedEvent
4897
5232
  });
4898
- const result = await processor.process(stream2);
4899
- for (const output of result.outputs) {
4900
- yield output;
5233
+ let streamMetadata = null;
5234
+ let gadgetCallCount = 0;
5235
+ const textOutputs = [];
5236
+ const gadgetResults = [];
5237
+ for await (const event of processor.process(stream2)) {
5238
+ if (event.type === "stream_complete") {
5239
+ streamMetadata = event;
5240
+ continue;
5241
+ }
5242
+ if (event.type === "text") {
5243
+ textOutputs.push(event.content);
5244
+ } else if (event.type === "gadget_result") {
5245
+ gadgetCallCount++;
5246
+ gadgetResults.push(event);
5247
+ }
5248
+ yield event;
4901
5249
  }
5250
+ if (!streamMetadata) {
5251
+ throw new Error("Stream processing completed without metadata event");
5252
+ }
5253
+ const result = streamMetadata;
4902
5254
  this.logger.info("LLM response completed", {
4903
5255
  finishReason: result.finishReason,
4904
5256
  usage: result.usage,
@@ -4923,9 +5275,6 @@ var init_agent = __esm({
4923
5275
  });
4924
5276
  let finalMessage = result.finalMessage;
4925
5277
  if (this.hooks.controllers?.afterLLMCall) {
4926
- const gadgetCallCount = result.outputs.filter(
4927
- (output) => output.type === "gadget_result"
4928
- ).length;
4929
5278
  const context = {
4930
5279
  iteration: currentIteration,
4931
5280
  maxIterations: this.maxIterations,
@@ -4955,9 +5304,7 @@ var init_agent = __esm({
4955
5304
  }
4956
5305
  if (result.didExecuteGadgets) {
4957
5306
  if (this.textWithGadgetsHandler) {
4958
- const textContent = result.outputs.filter(
4959
- (output) => output.type === "text"
4960
- ).map((output) => output.content).join("");
5307
+ const textContent = textOutputs.join("");
4961
5308
  if (textContent.trim()) {
4962
5309
  const { gadgetName, parameterMapping, resultMapping } = this.textWithGadgetsHandler;
4963
5310
  this.conversation.addGadgetCallResult(
@@ -4967,7 +5314,7 @@ var init_agent = __esm({
4967
5314
  );
4968
5315
  }
4969
5316
  }
4970
- for (const output of result.outputs) {
5317
+ for (const output of gadgetResults) {
4971
5318
  if (output.type === "gadget_result") {
4972
5319
  const gadgetResult = output.result;
4973
5320
  this.conversation.addGadgetCallResult(
@@ -8475,6 +8822,8 @@ var init_builder = __esm({
8475
8822
  signal;
8476
8823
  trailingMessage;
8477
8824
  subagentConfig;
8825
+ nestedEventCallback;
8826
+ parentContext;
8478
8827
  constructor(client) {
8479
8828
  this.client = client;
8480
8829
  }
@@ -8971,6 +9320,74 @@ var init_builder = __esm({
8971
9320
  this.subagentConfig = config;
8972
9321
  return this;
8973
9322
  }
9323
+ /**
9324
+ * Set the callback for nested subagent events.
9325
+ *
9326
+ * Subagent gadgets (like BrowseWeb) can use ExecutionContext.onNestedEvent
9327
+ * to report their internal LLM calls and gadget executions in real-time.
9328
+ * This callback receives those events, enabling hierarchical progress display.
9329
+ *
9330
+ * @param callback - Function to handle nested agent events
9331
+ * @returns This builder for chaining
9332
+ *
9333
+ * @example
9334
+ * ```typescript
9335
+ * .withNestedEventCallback((event) => {
9336
+ * if (event.type === "llm_call_start") {
9337
+ * console.log(` Nested LLM #${event.event.iteration} starting...`);
9338
+ * } else if (event.type === "gadget_call") {
9339
+ * console.log(` ⏵ ${event.event.call.gadgetName}...`);
9340
+ * }
9341
+ * })
9342
+ * ```
9343
+ */
9344
+ withNestedEventCallback(callback) {
9345
+ this.nestedEventCallback = callback;
9346
+ return this;
9347
+ }
9348
+ /**
9349
+ * Enable automatic nested event forwarding to parent agent.
9350
+ *
9351
+ * When building a subagent inside a gadget, call this method to automatically
9352
+ * forward all LLM calls and gadget events to the parent agent. This enables
9353
+ * hierarchical progress display without any manual event handling.
9354
+ *
9355
+ * The method extracts `invocationId` and `onNestedEvent` from the execution
9356
+ * context and sets up automatic forwarding via hooks and event wrapping.
9357
+ *
9358
+ * @param ctx - ExecutionContext passed to the gadget's execute() method
9359
+ * @param depth - Nesting depth (default: 1 for direct child)
9360
+ * @returns This builder for chaining
9361
+ *
9362
+ * @example
9363
+ * ```typescript
9364
+ * // In a subagent gadget like BrowseWeb - ONE LINE enables auto-forwarding:
9365
+ * execute: async (params, ctx) => {
9366
+ * const agent = new AgentBuilder(client)
9367
+ * .withModel(model)
9368
+ * .withGadgets(Navigate, Click, Screenshot)
9369
+ * .withParentContext(ctx) // <-- This is all you need!
9370
+ * .ask(params.task);
9371
+ *
9372
+ * for await (const event of agent.run()) {
9373
+ * // Events automatically forwarded - just process normally
9374
+ * if (event.type === "text") {
9375
+ * result = event.content;
9376
+ * }
9377
+ * }
9378
+ * }
9379
+ * ```
9380
+ */
9381
+ withParentContext(ctx, depth = 1) {
9382
+ if (ctx.onNestedEvent && ctx.invocationId) {
9383
+ this.parentContext = {
9384
+ invocationId: ctx.invocationId,
9385
+ onNestedEvent: ctx.onNestedEvent,
9386
+ depth
9387
+ };
9388
+ }
9389
+ return this;
9390
+ }
8974
9391
  /**
8975
9392
  * Add an ephemeral trailing message that appears at the end of each LLM request.
8976
9393
  *
@@ -9038,14 +9455,58 @@ ${endPrefix}`
9038
9455
  return this;
9039
9456
  }
9040
9457
  /**
9041
- * Compose the final hooks, including trailing message if configured.
9458
+ * Compose the final hooks, including:
9459
+ * - Trailing message injection (if configured)
9460
+ * - Nested event forwarding for LLM calls (if parentContext is set)
9042
9461
  */
9043
9462
  composeHooks() {
9463
+ let hooks = this.hooks;
9464
+ if (this.parentContext) {
9465
+ const { invocationId, onNestedEvent, depth } = this.parentContext;
9466
+ const existingOnLLMCallStart = hooks?.observers?.onLLMCallStart;
9467
+ const existingOnLLMCallComplete = hooks?.observers?.onLLMCallComplete;
9468
+ hooks = {
9469
+ ...hooks,
9470
+ observers: {
9471
+ ...hooks?.observers,
9472
+ onLLMCallStart: async (context) => {
9473
+ onNestedEvent({
9474
+ type: "llm_call_start",
9475
+ gadgetInvocationId: invocationId,
9476
+ depth,
9477
+ event: {
9478
+ iteration: context.iteration,
9479
+ model: context.options.model
9480
+ }
9481
+ });
9482
+ if (existingOnLLMCallStart) {
9483
+ await existingOnLLMCallStart(context);
9484
+ }
9485
+ },
9486
+ onLLMCallComplete: async (context) => {
9487
+ onNestedEvent({
9488
+ type: "llm_call_end",
9489
+ gadgetInvocationId: invocationId,
9490
+ depth,
9491
+ event: {
9492
+ iteration: context.iteration,
9493
+ model: context.options.model,
9494
+ outputTokens: context.usage?.outputTokens,
9495
+ finishReason: context.finishReason
9496
+ }
9497
+ });
9498
+ if (existingOnLLMCallComplete) {
9499
+ await existingOnLLMCallComplete(context);
9500
+ }
9501
+ }
9502
+ }
9503
+ };
9504
+ }
9044
9505
  if (!this.trailingMessage) {
9045
- return this.hooks;
9506
+ return hooks;
9046
9507
  }
9047
9508
  const trailingMsg = this.trailingMessage;
9048
- const existingBeforeLLMCall = this.hooks?.controllers?.beforeLLMCall;
9509
+ const existingBeforeLLMCall = hooks?.controllers?.beforeLLMCall;
9049
9510
  const trailingMessageController = async (ctx) => {
9050
9511
  const result = existingBeforeLLMCall ? await existingBeforeLLMCall(ctx) : { action: "proceed" };
9051
9512
  if (result.action === "skip") {
@@ -9060,9 +9521,9 @@ ${endPrefix}`
9060
9521
  };
9061
9522
  };
9062
9523
  return {
9063
- ...this.hooks,
9524
+ ...hooks,
9064
9525
  controllers: {
9065
- ...this.hooks?.controllers,
9526
+ ...hooks?.controllers,
9066
9527
  beforeLLMCall: trailingMessageController
9067
9528
  }
9068
9529
  };
@@ -9123,6 +9584,19 @@ ${endPrefix}`
9123
9584
  this.client = new LLMistClass();
9124
9585
  }
9125
9586
  const registry = GadgetRegistry.from(this.gadgets);
9587
+ let onNestedEvent = this.nestedEventCallback;
9588
+ if (this.parentContext) {
9589
+ const { invocationId, onNestedEvent: parentCallback, depth } = this.parentContext;
9590
+ const existingCallback = this.nestedEventCallback;
9591
+ onNestedEvent = (event) => {
9592
+ parentCallback({
9593
+ ...event,
9594
+ gadgetInvocationId: invocationId,
9595
+ depth: event.depth + depth
9596
+ });
9597
+ existingCallback?.(event);
9598
+ };
9599
+ }
9126
9600
  return {
9127
9601
  client: this.client,
9128
9602
  model: this.model ?? "openai:gpt-5-nano",
@@ -9148,7 +9622,8 @@ ${endPrefix}`
9148
9622
  gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
9149
9623
  compactionConfig: this.compactionConfig,
9150
9624
  signal: this.signal,
9151
- subagentConfig: this.subagentConfig
9625
+ subagentConfig: this.subagentConfig,
9626
+ onNestedEvent
9152
9627
  };
9153
9628
  }
9154
9629
  ask(userPrompt) {
@@ -9305,6 +9780,19 @@ ${endPrefix}`
9305
9780
  this.client = new LLMistClass();
9306
9781
  }
9307
9782
  const registry = GadgetRegistry.from(this.gadgets);
9783
+ let onNestedEvent = this.nestedEventCallback;
9784
+ if (this.parentContext) {
9785
+ const { invocationId, onNestedEvent: parentCallback, depth } = this.parentContext;
9786
+ const existingCallback = this.nestedEventCallback;
9787
+ onNestedEvent = (event) => {
9788
+ parentCallback({
9789
+ ...event,
9790
+ gadgetInvocationId: invocationId,
9791
+ depth: event.depth + depth
9792
+ });
9793
+ existingCallback?.(event);
9794
+ };
9795
+ }
9308
9796
  const options = {
9309
9797
  client: this.client,
9310
9798
  model: this.model ?? "openai:gpt-5-nano",
@@ -9330,7 +9818,8 @@ ${endPrefix}`
9330
9818
  gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
9331
9819
  compactionConfig: this.compactionConfig,
9332
9820
  signal: this.signal,
9333
- subagentConfig: this.subagentConfig
9821
+ subagentConfig: this.subagentConfig,
9822
+ onNestedEvent
9334
9823
  };
9335
9824
  return new Agent(AGENT_INTERNAL_KEY, options);
9336
9825
  }