llmist 3.0.0 → 4.0.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, onSubagentEvent) {
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.onSubagentEvent = onSubagentEvent;
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
+ onSubagentEvent: this.onSubagentEvent
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.onSubagentEvent
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,12 @@ var init_agent = __esm({
4586
4917
  // Subagent configuration
4587
4918
  agentContextConfig;
4588
4919
  subagentConfig;
4920
+ // Subagent event callback for subagent gadgets
4921
+ userSubagentEventCallback;
4922
+ // Internal queue for yielding subagent events in run()
4923
+ pendingSubagentEvents = [];
4924
+ // Combined callback that queues events AND calls user callback
4925
+ onSubagentEvent;
4589
4926
  /**
4590
4927
  * Creates a new Agent instance.
4591
4928
  * @internal This constructor is private. Use LLMist.createAgent() or AgentBuilder instead.
@@ -4663,6 +5000,71 @@ var init_agent = __esm({
4663
5000
  temperature: this.temperature
4664
5001
  };
4665
5002
  this.subagentConfig = options.subagentConfig;
5003
+ this.userSubagentEventCallback = options.onSubagentEvent;
5004
+ this.onSubagentEvent = (event) => {
5005
+ this.pendingSubagentEvents.push(event);
5006
+ this.userSubagentEventCallback?.(event);
5007
+ const subagentContext = {
5008
+ parentGadgetInvocationId: event.gadgetInvocationId,
5009
+ depth: event.depth
5010
+ };
5011
+ if (event.type === "llm_call_start") {
5012
+ const info = event.event;
5013
+ void this.hooks?.observers?.onLLMCallStart?.({
5014
+ iteration: info.iteration,
5015
+ options: { model: info.model, messages: [] },
5016
+ logger: this.logger,
5017
+ subagentContext
5018
+ });
5019
+ } else if (event.type === "llm_call_end") {
5020
+ const info = event.event;
5021
+ void this.hooks?.observers?.onLLMCallComplete?.({
5022
+ iteration: info.iteration,
5023
+ options: { model: info.model, messages: [] },
5024
+ finishReason: info.finishReason ?? null,
5025
+ usage: info.outputTokens ? {
5026
+ inputTokens: info.inputTokens ?? 0,
5027
+ outputTokens: info.outputTokens,
5028
+ totalTokens: (info.inputTokens ?? 0) + info.outputTokens
5029
+ } : void 0,
5030
+ rawResponse: "",
5031
+ finalMessage: "",
5032
+ logger: this.logger,
5033
+ subagentContext
5034
+ });
5035
+ } else if (event.type === "gadget_call") {
5036
+ const gadgetEvent = event.event;
5037
+ void this.hooks?.observers?.onGadgetExecutionStart?.({
5038
+ iteration: 0,
5039
+ gadgetName: gadgetEvent.call.gadgetName,
5040
+ invocationId: gadgetEvent.call.invocationId,
5041
+ parameters: gadgetEvent.call.parameters ?? {},
5042
+ logger: this.logger,
5043
+ subagentContext
5044
+ });
5045
+ } else if (event.type === "gadget_result") {
5046
+ const resultEvent = event.event;
5047
+ void this.hooks?.observers?.onGadgetExecutionComplete?.({
5048
+ iteration: 0,
5049
+ gadgetName: resultEvent.result.gadgetName ?? "unknown",
5050
+ invocationId: resultEvent.result.invocationId,
5051
+ parameters: {},
5052
+ executionTimeMs: resultEvent.result.executionTimeMs ?? 0,
5053
+ logger: this.logger,
5054
+ subagentContext
5055
+ });
5056
+ }
5057
+ };
5058
+ }
5059
+ /**
5060
+ * Flush pending subagent events as StreamEvents.
5061
+ * Called from run() to yield queued subagent events from subagent gadgets.
5062
+ */
5063
+ *flushPendingSubagentEvents() {
5064
+ while (this.pendingSubagentEvents.length > 0) {
5065
+ const event = this.pendingSubagentEvents.shift();
5066
+ yield { type: "subagent_event", subagentEvent: event };
5067
+ }
4666
5068
  }
4667
5069
  /**
4668
5070
  * Get the gadget registry for this agent.
@@ -4893,12 +5295,31 @@ var init_agent = __esm({
4893
5295
  client: this.client,
4894
5296
  mediaStore: this.mediaStore,
4895
5297
  agentConfig: this.agentContextConfig,
4896
- subagentConfig: this.subagentConfig
5298
+ subagentConfig: this.subagentConfig,
5299
+ onSubagentEvent: this.onSubagentEvent
4897
5300
  });
4898
- const result = await processor.process(stream2);
4899
- for (const output of result.outputs) {
4900
- yield output;
5301
+ let streamMetadata = null;
5302
+ let gadgetCallCount = 0;
5303
+ const textOutputs = [];
5304
+ const gadgetResults = [];
5305
+ for await (const event of processor.process(stream2)) {
5306
+ if (event.type === "stream_complete") {
5307
+ streamMetadata = event;
5308
+ continue;
5309
+ }
5310
+ if (event.type === "text") {
5311
+ textOutputs.push(event.content);
5312
+ } else if (event.type === "gadget_result") {
5313
+ gadgetCallCount++;
5314
+ gadgetResults.push(event);
5315
+ }
5316
+ yield event;
5317
+ yield* this.flushPendingSubagentEvents();
4901
5318
  }
5319
+ if (!streamMetadata) {
5320
+ throw new Error("Stream processing completed without metadata event");
5321
+ }
5322
+ const result = streamMetadata;
4902
5323
  this.logger.info("LLM response completed", {
4903
5324
  finishReason: result.finishReason,
4904
5325
  usage: result.usage,
@@ -4923,9 +5344,6 @@ var init_agent = __esm({
4923
5344
  });
4924
5345
  let finalMessage = result.finalMessage;
4925
5346
  if (this.hooks.controllers?.afterLLMCall) {
4926
- const gadgetCallCount = result.outputs.filter(
4927
- (output) => output.type === "gadget_result"
4928
- ).length;
4929
5347
  const context = {
4930
5348
  iteration: currentIteration,
4931
5349
  maxIterations: this.maxIterations,
@@ -4955,9 +5373,7 @@ var init_agent = __esm({
4955
5373
  }
4956
5374
  if (result.didExecuteGadgets) {
4957
5375
  if (this.textWithGadgetsHandler) {
4958
- const textContent = result.outputs.filter(
4959
- (output) => output.type === "text"
4960
- ).map((output) => output.content).join("");
5376
+ const textContent = textOutputs.join("");
4961
5377
  if (textContent.trim()) {
4962
5378
  const { gadgetName, parameterMapping, resultMapping } = this.textWithGadgetsHandler;
4963
5379
  this.conversation.addGadgetCallResult(
@@ -4967,7 +5383,7 @@ var init_agent = __esm({
4967
5383
  );
4968
5384
  }
4969
5385
  }
4970
- for (const output of result.outputs) {
5386
+ for (const output of gadgetResults) {
4971
5387
  if (output.type === "gadget_result") {
4972
5388
  const gadgetResult = output.result;
4973
5389
  this.conversation.addGadgetCallResult(
@@ -8475,6 +8891,8 @@ var init_builder = __esm({
8475
8891
  signal;
8476
8892
  trailingMessage;
8477
8893
  subagentConfig;
8894
+ subagentEventCallback;
8895
+ parentContext;
8478
8896
  constructor(client) {
8479
8897
  this.client = client;
8480
8898
  }
@@ -8971,6 +9389,74 @@ var init_builder = __esm({
8971
9389
  this.subagentConfig = config;
8972
9390
  return this;
8973
9391
  }
9392
+ /**
9393
+ * Set the callback for subagent events.
9394
+ *
9395
+ * Subagent gadgets (like BrowseWeb) can use ExecutionContext.onSubagentEvent
9396
+ * to report their internal LLM calls and gadget executions in real-time.
9397
+ * This callback receives those events, enabling hierarchical progress display.
9398
+ *
9399
+ * @param callback - Function to handle subagent events
9400
+ * @returns This builder for chaining
9401
+ *
9402
+ * @example
9403
+ * ```typescript
9404
+ * .withSubagentEventCallback((event) => {
9405
+ * if (event.type === "llm_call_start") {
9406
+ * console.log(` Subagent LLM #${event.event.iteration} starting...`);
9407
+ * } else if (event.type === "gadget_call") {
9408
+ * console.log(` ⏵ ${event.event.call.gadgetName}...`);
9409
+ * }
9410
+ * })
9411
+ * ```
9412
+ */
9413
+ withSubagentEventCallback(callback) {
9414
+ this.subagentEventCallback = callback;
9415
+ return this;
9416
+ }
9417
+ /**
9418
+ * Enable automatic subagent event forwarding to parent agent.
9419
+ *
9420
+ * When building a subagent inside a gadget, call this method to automatically
9421
+ * forward all LLM calls and gadget events to the parent agent. This enables
9422
+ * hierarchical progress display without any manual event handling.
9423
+ *
9424
+ * The method extracts `invocationId` and `onSubagentEvent` from the execution
9425
+ * context and sets up automatic forwarding via hooks and event wrapping.
9426
+ *
9427
+ * @param ctx - ExecutionContext passed to the gadget's execute() method
9428
+ * @param depth - Nesting depth (default: 1 for direct child)
9429
+ * @returns This builder for chaining
9430
+ *
9431
+ * @example
9432
+ * ```typescript
9433
+ * // In a subagent gadget like BrowseWeb - ONE LINE enables auto-forwarding:
9434
+ * execute: async (params, ctx) => {
9435
+ * const agent = new AgentBuilder(client)
9436
+ * .withModel(model)
9437
+ * .withGadgets(Navigate, Click, Screenshot)
9438
+ * .withParentContext(ctx) // <-- This is all you need!
9439
+ * .ask(params.task);
9440
+ *
9441
+ * for await (const event of agent.run()) {
9442
+ * // Events automatically forwarded - just process normally
9443
+ * if (event.type === "text") {
9444
+ * result = event.content;
9445
+ * }
9446
+ * }
9447
+ * }
9448
+ * ```
9449
+ */
9450
+ withParentContext(ctx, depth = 1) {
9451
+ if (ctx.onSubagentEvent && ctx.invocationId) {
9452
+ this.parentContext = {
9453
+ invocationId: ctx.invocationId,
9454
+ onSubagentEvent: ctx.onSubagentEvent,
9455
+ depth
9456
+ };
9457
+ }
9458
+ return this;
9459
+ }
8974
9460
  /**
8975
9461
  * Add an ephemeral trailing message that appears at the end of each LLM request.
8976
9462
  *
@@ -9038,14 +9524,92 @@ ${endPrefix}`
9038
9524
  return this;
9039
9525
  }
9040
9526
  /**
9041
- * Compose the final hooks, including trailing message if configured.
9527
+ * Compose the final hooks, including:
9528
+ * - Trailing message injection (if configured)
9529
+ * - Subagent event forwarding for LLM calls (if parentContext is set)
9042
9530
  */
9043
9531
  composeHooks() {
9532
+ let hooks = this.hooks;
9533
+ if (this.parentContext) {
9534
+ const { invocationId, onSubagentEvent, depth } = this.parentContext;
9535
+ const existingOnLLMCallStart = hooks?.observers?.onLLMCallStart;
9536
+ const existingOnLLMCallComplete = hooks?.observers?.onLLMCallComplete;
9537
+ const existingOnGadgetExecutionStart = hooks?.observers?.onGadgetExecutionStart;
9538
+ const existingOnGadgetExecutionComplete = hooks?.observers?.onGadgetExecutionComplete;
9539
+ hooks = {
9540
+ ...hooks,
9541
+ observers: {
9542
+ ...hooks?.observers,
9543
+ onLLMCallStart: async (context) => {
9544
+ onSubagentEvent({
9545
+ type: "llm_call_start",
9546
+ gadgetInvocationId: invocationId,
9547
+ depth,
9548
+ event: {
9549
+ iteration: context.iteration,
9550
+ model: context.options.model
9551
+ }
9552
+ });
9553
+ if (existingOnLLMCallStart) {
9554
+ await existingOnLLMCallStart(context);
9555
+ }
9556
+ },
9557
+ onLLMCallComplete: async (context) => {
9558
+ onSubagentEvent({
9559
+ type: "llm_call_end",
9560
+ gadgetInvocationId: invocationId,
9561
+ depth,
9562
+ event: {
9563
+ iteration: context.iteration,
9564
+ model: context.options.model,
9565
+ outputTokens: context.usage?.outputTokens,
9566
+ finishReason: context.finishReason
9567
+ }
9568
+ });
9569
+ if (existingOnLLMCallComplete) {
9570
+ await existingOnLLMCallComplete(context);
9571
+ }
9572
+ },
9573
+ onGadgetExecutionStart: async (context) => {
9574
+ onSubagentEvent({
9575
+ type: "gadget_call",
9576
+ gadgetInvocationId: invocationId,
9577
+ depth,
9578
+ event: {
9579
+ call: {
9580
+ invocationId: context.invocationId,
9581
+ gadgetName: context.gadgetName,
9582
+ parameters: context.parameters
9583
+ }
9584
+ }
9585
+ });
9586
+ if (existingOnGadgetExecutionStart) {
9587
+ await existingOnGadgetExecutionStart(context);
9588
+ }
9589
+ },
9590
+ onGadgetExecutionComplete: async (context) => {
9591
+ onSubagentEvent({
9592
+ type: "gadget_result",
9593
+ gadgetInvocationId: invocationId,
9594
+ depth,
9595
+ event: {
9596
+ result: {
9597
+ invocationId: context.invocationId
9598
+ }
9599
+ }
9600
+ });
9601
+ if (existingOnGadgetExecutionComplete) {
9602
+ await existingOnGadgetExecutionComplete(context);
9603
+ }
9604
+ }
9605
+ }
9606
+ };
9607
+ }
9044
9608
  if (!this.trailingMessage) {
9045
- return this.hooks;
9609
+ return hooks;
9046
9610
  }
9047
9611
  const trailingMsg = this.trailingMessage;
9048
- const existingBeforeLLMCall = this.hooks?.controllers?.beforeLLMCall;
9612
+ const existingBeforeLLMCall = hooks?.controllers?.beforeLLMCall;
9049
9613
  const trailingMessageController = async (ctx) => {
9050
9614
  const result = existingBeforeLLMCall ? await existingBeforeLLMCall(ctx) : { action: "proceed" };
9051
9615
  if (result.action === "skip") {
@@ -9060,9 +9624,9 @@ ${endPrefix}`
9060
9624
  };
9061
9625
  };
9062
9626
  return {
9063
- ...this.hooks,
9627
+ ...hooks,
9064
9628
  controllers: {
9065
- ...this.hooks?.controllers,
9629
+ ...hooks?.controllers,
9066
9630
  beforeLLMCall: trailingMessageController
9067
9631
  }
9068
9632
  };
@@ -9123,6 +9687,19 @@ ${endPrefix}`
9123
9687
  this.client = new LLMistClass();
9124
9688
  }
9125
9689
  const registry = GadgetRegistry.from(this.gadgets);
9690
+ let onSubagentEvent = this.subagentEventCallback;
9691
+ if (this.parentContext) {
9692
+ const { invocationId, onSubagentEvent: parentCallback, depth } = this.parentContext;
9693
+ const existingCallback = this.subagentEventCallback;
9694
+ onSubagentEvent = (event) => {
9695
+ parentCallback({
9696
+ ...event,
9697
+ gadgetInvocationId: invocationId,
9698
+ depth: event.depth + depth
9699
+ });
9700
+ existingCallback?.(event);
9701
+ };
9702
+ }
9126
9703
  return {
9127
9704
  client: this.client,
9128
9705
  model: this.model ?? "openai:gpt-5-nano",
@@ -9148,7 +9725,8 @@ ${endPrefix}`
9148
9725
  gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
9149
9726
  compactionConfig: this.compactionConfig,
9150
9727
  signal: this.signal,
9151
- subagentConfig: this.subagentConfig
9728
+ subagentConfig: this.subagentConfig,
9729
+ onSubagentEvent
9152
9730
  };
9153
9731
  }
9154
9732
  ask(userPrompt) {
@@ -9305,6 +9883,19 @@ ${endPrefix}`
9305
9883
  this.client = new LLMistClass();
9306
9884
  }
9307
9885
  const registry = GadgetRegistry.from(this.gadgets);
9886
+ let onSubagentEvent = this.subagentEventCallback;
9887
+ if (this.parentContext) {
9888
+ const { invocationId, onSubagentEvent: parentCallback, depth } = this.parentContext;
9889
+ const existingCallback = this.subagentEventCallback;
9890
+ onSubagentEvent = (event) => {
9891
+ parentCallback({
9892
+ ...event,
9893
+ gadgetInvocationId: invocationId,
9894
+ depth: event.depth + depth
9895
+ });
9896
+ existingCallback?.(event);
9897
+ };
9898
+ }
9308
9899
  const options = {
9309
9900
  client: this.client,
9310
9901
  model: this.model ?? "openai:gpt-5-nano",
@@ -9330,7 +9921,8 @@ ${endPrefix}`
9330
9921
  gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
9331
9922
  compactionConfig: this.compactionConfig,
9332
9923
  signal: this.signal,
9333
- subagentConfig: this.subagentConfig
9924
+ subagentConfig: this.subagentConfig,
9925
+ onSubagentEvent
9334
9926
  };
9335
9927
  return new Agent(AGENT_INTERNAL_KEY, options);
9336
9928
  }