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/cli.cjs CHANGED
@@ -3496,7 +3496,7 @@ var init_executor = __esm({
3496
3496
  init_exceptions();
3497
3497
  init_parser();
3498
3498
  GadgetExecutor = class {
3499
- constructor(registry, requestHumanInput, logger, defaultGadgetTimeoutMs, errorFormatterOptions, client, mediaStore, agentConfig, subagentConfig) {
3499
+ constructor(registry, requestHumanInput, logger, defaultGadgetTimeoutMs, errorFormatterOptions, client, mediaStore, agentConfig, subagentConfig, onNestedEvent) {
3500
3500
  this.registry = registry;
3501
3501
  this.requestHumanInput = requestHumanInput;
3502
3502
  this.defaultGadgetTimeoutMs = defaultGadgetTimeoutMs;
@@ -3504,6 +3504,7 @@ var init_executor = __esm({
3504
3504
  this.mediaStore = mediaStore;
3505
3505
  this.agentConfig = agentConfig;
3506
3506
  this.subagentConfig = subagentConfig;
3507
+ this.onNestedEvent = onNestedEvent;
3507
3508
  this.logger = logger ?? createLogger({ name: "llmist:executor" });
3508
3509
  this.errorFormatter = new GadgetExecutionErrorFormatter(errorFormatterOptions);
3509
3510
  this.argPrefix = errorFormatterOptions?.argPrefix ?? GADGET_ARG_PREFIX;
@@ -3649,7 +3650,9 @@ var init_executor = __esm({
3649
3650
  llmist: this.client ? new CostReportingLLMistWrapper(this.client, reportCost) : void 0,
3650
3651
  signal: abortController.signal,
3651
3652
  agentConfig: this.agentConfig,
3652
- subagentConfig: this.subagentConfig
3653
+ subagentConfig: this.subagentConfig,
3654
+ invocationId: call.invocationId,
3655
+ onNestedEvent: this.onNestedEvent
3653
3656
  };
3654
3657
  let rawResult;
3655
3658
  if (timeoutMs && timeoutMs > 0) {
@@ -3888,14 +3891,21 @@ var init_stream_processor = __esm({
3888
3891
  options.client,
3889
3892
  options.mediaStore,
3890
3893
  options.agentConfig,
3891
- options.subagentConfig
3894
+ options.subagentConfig,
3895
+ options.onNestedEvent
3892
3896
  );
3893
3897
  }
3894
3898
  /**
3895
- * Process an LLM stream and return structured results.
3899
+ * Process an LLM stream and yield events in real-time.
3900
+ *
3901
+ * This is an async generator that yields events immediately as they occur:
3902
+ * - Text events are yielded as text is streamed from the LLM
3903
+ * - gadget_call events are yielded immediately when a gadget call is parsed
3904
+ * - gadget_result events are yielded when gadget execution completes
3905
+ *
3906
+ * The final event is always a StreamCompletionEvent containing metadata.
3896
3907
  */
3897
- async process(stream2) {
3898
- const outputs = [];
3908
+ async *process(stream2) {
3899
3909
  let finishReason = null;
3900
3910
  let usage;
3901
3911
  let didExecuteGadgets = false;
@@ -3941,14 +3951,13 @@ var init_stream_processor = __esm({
3941
3951
  continue;
3942
3952
  }
3943
3953
  for (const event of this.parser.feed(processedChunk)) {
3944
- const processedEvents = await this.processEvent(event);
3945
- outputs.push(...processedEvents);
3946
- if (processedEvents.some((e) => e.type === "gadget_result")) {
3947
- didExecuteGadgets = true;
3948
- }
3949
- for (const evt of processedEvents) {
3950
- if (evt.type === "gadget_result" && evt.result.breaksLoop) {
3951
- shouldBreakLoop = true;
3954
+ for await (const processedEvent of this.processEventGenerator(event)) {
3955
+ yield processedEvent;
3956
+ if (processedEvent.type === "gadget_result") {
3957
+ didExecuteGadgets = true;
3958
+ if (processedEvent.result.breaksLoop) {
3959
+ shouldBreakLoop = true;
3960
+ }
3952
3961
  }
3953
3962
  }
3954
3963
  }
@@ -3959,25 +3968,23 @@ var init_stream_processor = __esm({
3959
3968
  }
3960
3969
  if (!this.executionHalted) {
3961
3970
  for (const event of this.parser.finalize()) {
3962
- const processedEvents = await this.processEvent(event);
3963
- outputs.push(...processedEvents);
3964
- if (processedEvents.some((e) => e.type === "gadget_result")) {
3965
- didExecuteGadgets = true;
3966
- }
3967
- for (const evt of processedEvents) {
3968
- if (evt.type === "gadget_result" && evt.result.breaksLoop) {
3969
- shouldBreakLoop = true;
3971
+ for await (const processedEvent of this.processEventGenerator(event)) {
3972
+ yield processedEvent;
3973
+ if (processedEvent.type === "gadget_result") {
3974
+ didExecuteGadgets = true;
3975
+ if (processedEvent.result.breaksLoop) {
3976
+ shouldBreakLoop = true;
3977
+ }
3970
3978
  }
3971
3979
  }
3972
3980
  }
3973
- const finalPendingEvents = await this.processPendingGadgets();
3974
- outputs.push(...finalPendingEvents);
3975
- if (finalPendingEvents.some((e) => e.type === "gadget_result")) {
3976
- didExecuteGadgets = true;
3977
- }
3978
- for (const evt of finalPendingEvents) {
3979
- if (evt.type === "gadget_result" && evt.result.breaksLoop) {
3980
- shouldBreakLoop = true;
3981
+ for await (const evt of this.processPendingGadgetsGenerator()) {
3982
+ yield evt;
3983
+ if (evt.type === "gadget_result") {
3984
+ didExecuteGadgets = true;
3985
+ if (evt.result.breaksLoop) {
3986
+ shouldBreakLoop = true;
3987
+ }
3981
3988
  }
3982
3989
  }
3983
3990
  }
@@ -3990,8 +3997,8 @@ var init_stream_processor = __esm({
3990
3997
  };
3991
3998
  finalMessage = this.hooks.interceptors.interceptAssistantMessage(finalMessage, context);
3992
3999
  }
3993
- return {
3994
- outputs,
4000
+ const completionEvent = {
4001
+ type: "stream_complete",
3995
4002
  shouldBreakLoop,
3996
4003
  didExecuteGadgets,
3997
4004
  finishReason,
@@ -3999,9 +4006,11 @@ var init_stream_processor = __esm({
3999
4006
  rawResponse: this.responseText,
4000
4007
  finalMessage
4001
4008
  };
4009
+ yield completionEvent;
4002
4010
  }
4003
4011
  /**
4004
4012
  * Process a single parsed event (text or gadget call).
4013
+ * @deprecated Use processEventGenerator for real-time streaming
4005
4014
  */
4006
4015
  async processEvent(event) {
4007
4016
  if (event.type === "text") {
@@ -4011,6 +4020,23 @@ var init_stream_processor = __esm({
4011
4020
  }
4012
4021
  return [event];
4013
4022
  }
4023
+ /**
4024
+ * Process a single parsed event, yielding events in real-time.
4025
+ * Generator version of processEvent for streaming support.
4026
+ */
4027
+ async *processEventGenerator(event) {
4028
+ if (event.type === "text") {
4029
+ for (const e of await this.processTextEvent(event)) {
4030
+ yield e;
4031
+ }
4032
+ } else if (event.type === "gadget_call") {
4033
+ for await (const e of this.processGadgetCallGenerator(event.call)) {
4034
+ yield e;
4035
+ }
4036
+ } else {
4037
+ yield event;
4038
+ }
4039
+ }
4014
4040
  /**
4015
4041
  * Process a text event through interceptors.
4016
4042
  */
@@ -4087,9 +4113,68 @@ var init_stream_processor = __esm({
4087
4113
  events.push(...triggeredEvents);
4088
4114
  return events;
4089
4115
  }
4116
+ /**
4117
+ * Process a gadget call, yielding events in real-time.
4118
+ *
4119
+ * Key difference from processGadgetCall: yields gadget_call event IMMEDIATELY
4120
+ * when parsed (before execution), enabling real-time UI feedback.
4121
+ */
4122
+ async *processGadgetCallGenerator(call) {
4123
+ if (this.executionHalted) {
4124
+ this.logger.debug("Skipping gadget execution due to previous error", {
4125
+ gadgetName: call.gadgetName
4126
+ });
4127
+ return;
4128
+ }
4129
+ yield { type: "gadget_call", call };
4130
+ if (call.dependencies.length > 0) {
4131
+ if (call.dependencies.includes(call.invocationId)) {
4132
+ this.logger.warn("Gadget has self-referential dependency (depends on itself)", {
4133
+ gadgetName: call.gadgetName,
4134
+ invocationId: call.invocationId
4135
+ });
4136
+ this.failedInvocations.add(call.invocationId);
4137
+ const skipEvent = {
4138
+ type: "gadget_skipped",
4139
+ gadgetName: call.gadgetName,
4140
+ invocationId: call.invocationId,
4141
+ parameters: call.parameters ?? {},
4142
+ failedDependency: call.invocationId,
4143
+ failedDependencyError: `Gadget "${call.invocationId}" cannot depend on itself (self-referential dependency)`
4144
+ };
4145
+ yield skipEvent;
4146
+ return;
4147
+ }
4148
+ const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
4149
+ if (failedDep) {
4150
+ const skipEvents = await this.handleFailedDependency(call, failedDep);
4151
+ for (const evt of skipEvents) {
4152
+ yield evt;
4153
+ }
4154
+ return;
4155
+ }
4156
+ const unsatisfied = call.dependencies.filter((dep) => !this.completedResults.has(dep));
4157
+ if (unsatisfied.length > 0) {
4158
+ this.logger.debug("Queueing gadget for later - waiting on dependencies", {
4159
+ gadgetName: call.gadgetName,
4160
+ invocationId: call.invocationId,
4161
+ waitingOn: unsatisfied
4162
+ });
4163
+ this.gadgetsAwaitingDependencies.set(call.invocationId, call);
4164
+ return;
4165
+ }
4166
+ }
4167
+ for await (const evt of this.executeGadgetGenerator(call)) {
4168
+ yield evt;
4169
+ }
4170
+ for await (const evt of this.processPendingGadgetsGenerator()) {
4171
+ yield evt;
4172
+ }
4173
+ }
4090
4174
  /**
4091
4175
  * Execute a gadget through the full hook lifecycle.
4092
4176
  * This is the core execution logic, extracted from processGadgetCall.
4177
+ * @deprecated Use executeGadgetGenerator for real-time streaming
4093
4178
  */
4094
4179
  async executeGadgetWithHooks(call) {
4095
4180
  const events = [];
@@ -4242,6 +4327,159 @@ var init_stream_processor = __esm({
4242
4327
  }
4243
4328
  return events;
4244
4329
  }
4330
+ /**
4331
+ * Execute a gadget and yield the result event.
4332
+ * Generator version that yields gadget_result immediately when execution completes.
4333
+ */
4334
+ async *executeGadgetGenerator(call) {
4335
+ if (call.parseError) {
4336
+ this.logger.warn("Gadget has parse error", {
4337
+ gadgetName: call.gadgetName,
4338
+ error: call.parseError,
4339
+ rawParameters: call.parametersRaw
4340
+ });
4341
+ const shouldContinue = await this.checkCanRecoverFromError(
4342
+ call.parseError,
4343
+ call.gadgetName,
4344
+ "parse",
4345
+ call.parameters
4346
+ );
4347
+ if (!shouldContinue) {
4348
+ this.executionHalted = true;
4349
+ }
4350
+ }
4351
+ let parameters = call.parameters ?? {};
4352
+ if (this.hooks.interceptors?.interceptGadgetParameters) {
4353
+ const context = {
4354
+ iteration: this.iteration,
4355
+ gadgetName: call.gadgetName,
4356
+ invocationId: call.invocationId,
4357
+ logger: this.logger
4358
+ };
4359
+ parameters = this.hooks.interceptors.interceptGadgetParameters(parameters, context);
4360
+ }
4361
+ call.parameters = parameters;
4362
+ let shouldSkip = false;
4363
+ let syntheticResult;
4364
+ if (this.hooks.controllers?.beforeGadgetExecution) {
4365
+ const context = {
4366
+ iteration: this.iteration,
4367
+ gadgetName: call.gadgetName,
4368
+ invocationId: call.invocationId,
4369
+ parameters,
4370
+ logger: this.logger
4371
+ };
4372
+ const action = await this.hooks.controllers.beforeGadgetExecution(context);
4373
+ validateBeforeGadgetExecutionAction(action);
4374
+ if (action.action === "skip") {
4375
+ shouldSkip = true;
4376
+ syntheticResult = action.syntheticResult;
4377
+ this.logger.info("Controller skipped gadget execution", {
4378
+ gadgetName: call.gadgetName
4379
+ });
4380
+ }
4381
+ }
4382
+ const startObservers = [];
4383
+ if (this.hooks.observers?.onGadgetExecutionStart) {
4384
+ startObservers.push(async () => {
4385
+ const context = {
4386
+ iteration: this.iteration,
4387
+ gadgetName: call.gadgetName,
4388
+ invocationId: call.invocationId,
4389
+ parameters,
4390
+ logger: this.logger
4391
+ };
4392
+ await this.hooks.observers.onGadgetExecutionStart(context);
4393
+ });
4394
+ }
4395
+ await this.runObserversInParallel(startObservers);
4396
+ let result;
4397
+ if (shouldSkip) {
4398
+ result = {
4399
+ gadgetName: call.gadgetName,
4400
+ invocationId: call.invocationId,
4401
+ parameters,
4402
+ result: syntheticResult ?? "Execution skipped",
4403
+ executionTimeMs: 0
4404
+ };
4405
+ } else {
4406
+ result = await this.executor.execute(call);
4407
+ }
4408
+ const originalResult = result.result;
4409
+ if (result.result && this.hooks.interceptors?.interceptGadgetResult) {
4410
+ const context = {
4411
+ iteration: this.iteration,
4412
+ gadgetName: result.gadgetName,
4413
+ invocationId: result.invocationId,
4414
+ parameters,
4415
+ executionTimeMs: result.executionTimeMs,
4416
+ logger: this.logger
4417
+ };
4418
+ result.result = this.hooks.interceptors.interceptGadgetResult(result.result, context);
4419
+ }
4420
+ if (this.hooks.controllers?.afterGadgetExecution) {
4421
+ const context = {
4422
+ iteration: this.iteration,
4423
+ gadgetName: result.gadgetName,
4424
+ invocationId: result.invocationId,
4425
+ parameters,
4426
+ result: result.result,
4427
+ error: result.error,
4428
+ executionTimeMs: result.executionTimeMs,
4429
+ logger: this.logger
4430
+ };
4431
+ const action = await this.hooks.controllers.afterGadgetExecution(context);
4432
+ validateAfterGadgetExecutionAction(action);
4433
+ if (action.action === "recover" && result.error) {
4434
+ this.logger.info("Controller recovered from gadget error", {
4435
+ gadgetName: result.gadgetName,
4436
+ originalError: result.error
4437
+ });
4438
+ result = {
4439
+ ...result,
4440
+ error: void 0,
4441
+ result: action.fallbackResult
4442
+ };
4443
+ }
4444
+ }
4445
+ const completeObservers = [];
4446
+ if (this.hooks.observers?.onGadgetExecutionComplete) {
4447
+ completeObservers.push(async () => {
4448
+ const context = {
4449
+ iteration: this.iteration,
4450
+ gadgetName: result.gadgetName,
4451
+ invocationId: result.invocationId,
4452
+ parameters,
4453
+ originalResult,
4454
+ finalResult: result.result,
4455
+ error: result.error,
4456
+ executionTimeMs: result.executionTimeMs,
4457
+ breaksLoop: result.breaksLoop,
4458
+ cost: result.cost,
4459
+ logger: this.logger
4460
+ };
4461
+ await this.hooks.observers.onGadgetExecutionComplete(context);
4462
+ });
4463
+ }
4464
+ await this.runObserversInParallel(completeObservers);
4465
+ this.completedResults.set(result.invocationId, result);
4466
+ if (result.error) {
4467
+ this.failedInvocations.add(result.invocationId);
4468
+ }
4469
+ yield { type: "gadget_result", result };
4470
+ if (result.error) {
4471
+ const errorType = this.determineErrorType(call, result);
4472
+ const shouldContinue = await this.checkCanRecoverFromError(
4473
+ result.error,
4474
+ result.gadgetName,
4475
+ errorType,
4476
+ result.parameters
4477
+ );
4478
+ if (!shouldContinue) {
4479
+ this.executionHalted = true;
4480
+ }
4481
+ }
4482
+ }
4245
4483
  /**
4246
4484
  * Handle a gadget that cannot execute because a dependency failed.
4247
4485
  * Calls the onDependencySkipped controller to allow customization.
@@ -4398,6 +4636,99 @@ var init_stream_processor = __esm({
4398
4636
  }
4399
4637
  return events;
4400
4638
  }
4639
+ /**
4640
+ * Process pending gadgets, yielding events in real-time.
4641
+ * Generator version that yields events as gadgets complete.
4642
+ *
4643
+ * Note: Gadgets are still executed in parallel for efficiency,
4644
+ * but results are yielded as they become available.
4645
+ */
4646
+ async *processPendingGadgetsGenerator() {
4647
+ let progress = true;
4648
+ while (progress && this.gadgetsAwaitingDependencies.size > 0) {
4649
+ progress = false;
4650
+ const readyToExecute = [];
4651
+ const readyToSkip = [];
4652
+ for (const [_invocationId, call] of this.gadgetsAwaitingDependencies) {
4653
+ const failedDep = call.dependencies.find((dep) => this.failedInvocations.has(dep));
4654
+ if (failedDep) {
4655
+ readyToSkip.push({ call, failedDep });
4656
+ continue;
4657
+ }
4658
+ const allSatisfied = call.dependencies.every((dep) => this.completedResults.has(dep));
4659
+ if (allSatisfied) {
4660
+ readyToExecute.push(call);
4661
+ }
4662
+ }
4663
+ for (const { call, failedDep } of readyToSkip) {
4664
+ this.gadgetsAwaitingDependencies.delete(call.invocationId);
4665
+ const skipEvents = await this.handleFailedDependency(call, failedDep);
4666
+ for (const evt of skipEvents) {
4667
+ yield evt;
4668
+ }
4669
+ progress = true;
4670
+ }
4671
+ if (readyToExecute.length > 0) {
4672
+ this.logger.debug("Executing ready gadgets in parallel", {
4673
+ count: readyToExecute.length,
4674
+ invocationIds: readyToExecute.map((c) => c.invocationId)
4675
+ });
4676
+ for (const call of readyToExecute) {
4677
+ this.gadgetsAwaitingDependencies.delete(call.invocationId);
4678
+ }
4679
+ const eventSets = await Promise.all(
4680
+ readyToExecute.map(async (call) => {
4681
+ const events = [];
4682
+ for await (const evt of this.executeGadgetGenerator(call)) {
4683
+ events.push(evt);
4684
+ }
4685
+ return events;
4686
+ })
4687
+ );
4688
+ for (const events of eventSets) {
4689
+ for (const evt of events) {
4690
+ yield evt;
4691
+ }
4692
+ }
4693
+ progress = true;
4694
+ }
4695
+ }
4696
+ if (this.gadgetsAwaitingDependencies.size > 0) {
4697
+ const pendingIds = new Set(this.gadgetsAwaitingDependencies.keys());
4698
+ for (const [invocationId, call] of this.gadgetsAwaitingDependencies) {
4699
+ const missingDeps = call.dependencies.filter((dep) => !this.completedResults.has(dep));
4700
+ const circularDeps = missingDeps.filter((dep) => pendingIds.has(dep));
4701
+ const trulyMissingDeps = missingDeps.filter((dep) => !pendingIds.has(dep));
4702
+ let errorMessage;
4703
+ let logLevel = "warn";
4704
+ if (circularDeps.length > 0 && trulyMissingDeps.length > 0) {
4705
+ errorMessage = `Dependencies unresolvable: circular=[${circularDeps.join(", ")}], missing=[${trulyMissingDeps.join(", ")}]`;
4706
+ logLevel = "error";
4707
+ } else if (circularDeps.length > 0) {
4708
+ errorMessage = `Circular dependency detected: "${invocationId}" depends on "${circularDeps[0]}" which also depends on "${invocationId}" (directly or indirectly)`;
4709
+ } else {
4710
+ errorMessage = `Dependency "${missingDeps[0]}" was never executed - check that the invocation ID exists and is spelled correctly`;
4711
+ }
4712
+ this.logger[logLevel]("Gadget has unresolvable dependencies", {
4713
+ gadgetName: call.gadgetName,
4714
+ invocationId,
4715
+ circularDependencies: circularDeps,
4716
+ missingDependencies: trulyMissingDeps
4717
+ });
4718
+ this.failedInvocations.add(invocationId);
4719
+ const skipEvent = {
4720
+ type: "gadget_skipped",
4721
+ gadgetName: call.gadgetName,
4722
+ invocationId,
4723
+ parameters: call.parameters ?? {},
4724
+ failedDependency: missingDeps[0],
4725
+ failedDependencyError: errorMessage
4726
+ };
4727
+ yield skipEvent;
4728
+ }
4729
+ this.gadgetsAwaitingDependencies.clear();
4730
+ }
4731
+ }
4401
4732
  /**
4402
4733
  * Safely execute an observer, catching and logging any errors.
4403
4734
  * Observers are non-critical, so errors are logged but don't crash the system.
@@ -4520,6 +4851,8 @@ var init_agent = __esm({
4520
4851
  // Subagent configuration
4521
4852
  agentContextConfig;
4522
4853
  subagentConfig;
4854
+ // Nested event callback for subagent gadgets
4855
+ onNestedEvent;
4523
4856
  /**
4524
4857
  * Creates a new Agent instance.
4525
4858
  * @internal This constructor is private. Use LLMist.createAgent() or AgentBuilder instead.
@@ -4597,6 +4930,7 @@ var init_agent = __esm({
4597
4930
  temperature: this.temperature
4598
4931
  };
4599
4932
  this.subagentConfig = options.subagentConfig;
4933
+ this.onNestedEvent = options.onNestedEvent;
4600
4934
  }
4601
4935
  /**
4602
4936
  * Get the gadget registry for this agent.
@@ -4827,12 +5161,30 @@ var init_agent = __esm({
4827
5161
  client: this.client,
4828
5162
  mediaStore: this.mediaStore,
4829
5163
  agentConfig: this.agentContextConfig,
4830
- subagentConfig: this.subagentConfig
5164
+ subagentConfig: this.subagentConfig,
5165
+ onNestedEvent: this.onNestedEvent
4831
5166
  });
4832
- const result = await processor.process(stream2);
4833
- for (const output of result.outputs) {
4834
- yield output;
5167
+ let streamMetadata = null;
5168
+ let gadgetCallCount = 0;
5169
+ const textOutputs = [];
5170
+ const gadgetResults = [];
5171
+ for await (const event of processor.process(stream2)) {
5172
+ if (event.type === "stream_complete") {
5173
+ streamMetadata = event;
5174
+ continue;
5175
+ }
5176
+ if (event.type === "text") {
5177
+ textOutputs.push(event.content);
5178
+ } else if (event.type === "gadget_result") {
5179
+ gadgetCallCount++;
5180
+ gadgetResults.push(event);
5181
+ }
5182
+ yield event;
5183
+ }
5184
+ if (!streamMetadata) {
5185
+ throw new Error("Stream processing completed without metadata event");
4835
5186
  }
5187
+ const result = streamMetadata;
4836
5188
  this.logger.info("LLM response completed", {
4837
5189
  finishReason: result.finishReason,
4838
5190
  usage: result.usage,
@@ -4857,9 +5209,6 @@ var init_agent = __esm({
4857
5209
  });
4858
5210
  let finalMessage = result.finalMessage;
4859
5211
  if (this.hooks.controllers?.afterLLMCall) {
4860
- const gadgetCallCount = result.outputs.filter(
4861
- (output) => output.type === "gadget_result"
4862
- ).length;
4863
5212
  const context = {
4864
5213
  iteration: currentIteration,
4865
5214
  maxIterations: this.maxIterations,
@@ -4889,9 +5238,7 @@ var init_agent = __esm({
4889
5238
  }
4890
5239
  if (result.didExecuteGadgets) {
4891
5240
  if (this.textWithGadgetsHandler) {
4892
- const textContent = result.outputs.filter(
4893
- (output) => output.type === "text"
4894
- ).map((output) => output.content).join("");
5241
+ const textContent = textOutputs.join("");
4895
5242
  if (textContent.trim()) {
4896
5243
  const { gadgetName, parameterMapping, resultMapping } = this.textWithGadgetsHandler;
4897
5244
  this.conversation.addGadgetCallResult(
@@ -4901,7 +5248,7 @@ var init_agent = __esm({
4901
5248
  );
4902
5249
  }
4903
5250
  }
4904
- for (const output of result.outputs) {
5251
+ for (const output of gadgetResults) {
4905
5252
  if (output.type === "gadget_result") {
4906
5253
  const gadgetResult = output.result;
4907
5254
  this.conversation.addGadgetCallResult(
@@ -8409,6 +8756,8 @@ var init_builder = __esm({
8409
8756
  signal;
8410
8757
  trailingMessage;
8411
8758
  subagentConfig;
8759
+ nestedEventCallback;
8760
+ parentContext;
8412
8761
  constructor(client) {
8413
8762
  this.client = client;
8414
8763
  }
@@ -8905,6 +9254,74 @@ var init_builder = __esm({
8905
9254
  this.subagentConfig = config;
8906
9255
  return this;
8907
9256
  }
9257
+ /**
9258
+ * Set the callback for nested subagent events.
9259
+ *
9260
+ * Subagent gadgets (like BrowseWeb) can use ExecutionContext.onNestedEvent
9261
+ * to report their internal LLM calls and gadget executions in real-time.
9262
+ * This callback receives those events, enabling hierarchical progress display.
9263
+ *
9264
+ * @param callback - Function to handle nested agent events
9265
+ * @returns This builder for chaining
9266
+ *
9267
+ * @example
9268
+ * ```typescript
9269
+ * .withNestedEventCallback((event) => {
9270
+ * if (event.type === "llm_call_start") {
9271
+ * console.log(` Nested LLM #${event.event.iteration} starting...`);
9272
+ * } else if (event.type === "gadget_call") {
9273
+ * console.log(` ⏵ ${event.event.call.gadgetName}...`);
9274
+ * }
9275
+ * })
9276
+ * ```
9277
+ */
9278
+ withNestedEventCallback(callback) {
9279
+ this.nestedEventCallback = callback;
9280
+ return this;
9281
+ }
9282
+ /**
9283
+ * Enable automatic nested event forwarding to parent agent.
9284
+ *
9285
+ * When building a subagent inside a gadget, call this method to automatically
9286
+ * forward all LLM calls and gadget events to the parent agent. This enables
9287
+ * hierarchical progress display without any manual event handling.
9288
+ *
9289
+ * The method extracts `invocationId` and `onNestedEvent` from the execution
9290
+ * context and sets up automatic forwarding via hooks and event wrapping.
9291
+ *
9292
+ * @param ctx - ExecutionContext passed to the gadget's execute() method
9293
+ * @param depth - Nesting depth (default: 1 for direct child)
9294
+ * @returns This builder for chaining
9295
+ *
9296
+ * @example
9297
+ * ```typescript
9298
+ * // In a subagent gadget like BrowseWeb - ONE LINE enables auto-forwarding:
9299
+ * execute: async (params, ctx) => {
9300
+ * const agent = new AgentBuilder(client)
9301
+ * .withModel(model)
9302
+ * .withGadgets(Navigate, Click, Screenshot)
9303
+ * .withParentContext(ctx) // <-- This is all you need!
9304
+ * .ask(params.task);
9305
+ *
9306
+ * for await (const event of agent.run()) {
9307
+ * // Events automatically forwarded - just process normally
9308
+ * if (event.type === "text") {
9309
+ * result = event.content;
9310
+ * }
9311
+ * }
9312
+ * }
9313
+ * ```
9314
+ */
9315
+ withParentContext(ctx, depth = 1) {
9316
+ if (ctx.onNestedEvent && ctx.invocationId) {
9317
+ this.parentContext = {
9318
+ invocationId: ctx.invocationId,
9319
+ onNestedEvent: ctx.onNestedEvent,
9320
+ depth
9321
+ };
9322
+ }
9323
+ return this;
9324
+ }
8908
9325
  /**
8909
9326
  * Add an ephemeral trailing message that appears at the end of each LLM request.
8910
9327
  *
@@ -8972,14 +9389,58 @@ ${endPrefix}`
8972
9389
  return this;
8973
9390
  }
8974
9391
  /**
8975
- * Compose the final hooks, including trailing message if configured.
9392
+ * Compose the final hooks, including:
9393
+ * - Trailing message injection (if configured)
9394
+ * - Nested event forwarding for LLM calls (if parentContext is set)
8976
9395
  */
8977
9396
  composeHooks() {
9397
+ let hooks = this.hooks;
9398
+ if (this.parentContext) {
9399
+ const { invocationId, onNestedEvent, depth } = this.parentContext;
9400
+ const existingOnLLMCallStart = hooks?.observers?.onLLMCallStart;
9401
+ const existingOnLLMCallComplete = hooks?.observers?.onLLMCallComplete;
9402
+ hooks = {
9403
+ ...hooks,
9404
+ observers: {
9405
+ ...hooks?.observers,
9406
+ onLLMCallStart: async (context) => {
9407
+ onNestedEvent({
9408
+ type: "llm_call_start",
9409
+ gadgetInvocationId: invocationId,
9410
+ depth,
9411
+ event: {
9412
+ iteration: context.iteration,
9413
+ model: context.options.model
9414
+ }
9415
+ });
9416
+ if (existingOnLLMCallStart) {
9417
+ await existingOnLLMCallStart(context);
9418
+ }
9419
+ },
9420
+ onLLMCallComplete: async (context) => {
9421
+ onNestedEvent({
9422
+ type: "llm_call_end",
9423
+ gadgetInvocationId: invocationId,
9424
+ depth,
9425
+ event: {
9426
+ iteration: context.iteration,
9427
+ model: context.options.model,
9428
+ outputTokens: context.usage?.outputTokens,
9429
+ finishReason: context.finishReason
9430
+ }
9431
+ });
9432
+ if (existingOnLLMCallComplete) {
9433
+ await existingOnLLMCallComplete(context);
9434
+ }
9435
+ }
9436
+ }
9437
+ };
9438
+ }
8978
9439
  if (!this.trailingMessage) {
8979
- return this.hooks;
9440
+ return hooks;
8980
9441
  }
8981
9442
  const trailingMsg = this.trailingMessage;
8982
- const existingBeforeLLMCall = this.hooks?.controllers?.beforeLLMCall;
9443
+ const existingBeforeLLMCall = hooks?.controllers?.beforeLLMCall;
8983
9444
  const trailingMessageController = async (ctx) => {
8984
9445
  const result = existingBeforeLLMCall ? await existingBeforeLLMCall(ctx) : { action: "proceed" };
8985
9446
  if (result.action === "skip") {
@@ -8994,9 +9455,9 @@ ${endPrefix}`
8994
9455
  };
8995
9456
  };
8996
9457
  return {
8997
- ...this.hooks,
9458
+ ...hooks,
8998
9459
  controllers: {
8999
- ...this.hooks?.controllers,
9460
+ ...hooks?.controllers,
9000
9461
  beforeLLMCall: trailingMessageController
9001
9462
  }
9002
9463
  };
@@ -9057,6 +9518,19 @@ ${endPrefix}`
9057
9518
  this.client = new LLMistClass();
9058
9519
  }
9059
9520
  const registry = GadgetRegistry.from(this.gadgets);
9521
+ let onNestedEvent = this.nestedEventCallback;
9522
+ if (this.parentContext) {
9523
+ const { invocationId, onNestedEvent: parentCallback, depth } = this.parentContext;
9524
+ const existingCallback = this.nestedEventCallback;
9525
+ onNestedEvent = (event) => {
9526
+ parentCallback({
9527
+ ...event,
9528
+ gadgetInvocationId: invocationId,
9529
+ depth: event.depth + depth
9530
+ });
9531
+ existingCallback?.(event);
9532
+ };
9533
+ }
9060
9534
  return {
9061
9535
  client: this.client,
9062
9536
  model: this.model ?? "openai:gpt-5-nano",
@@ -9082,7 +9556,8 @@ ${endPrefix}`
9082
9556
  gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
9083
9557
  compactionConfig: this.compactionConfig,
9084
9558
  signal: this.signal,
9085
- subagentConfig: this.subagentConfig
9559
+ subagentConfig: this.subagentConfig,
9560
+ onNestedEvent
9086
9561
  };
9087
9562
  }
9088
9563
  ask(userPrompt) {
@@ -9239,6 +9714,19 @@ ${endPrefix}`
9239
9714
  this.client = new LLMistClass();
9240
9715
  }
9241
9716
  const registry = GadgetRegistry.from(this.gadgets);
9717
+ let onNestedEvent = this.nestedEventCallback;
9718
+ if (this.parentContext) {
9719
+ const { invocationId, onNestedEvent: parentCallback, depth } = this.parentContext;
9720
+ const existingCallback = this.nestedEventCallback;
9721
+ onNestedEvent = (event) => {
9722
+ parentCallback({
9723
+ ...event,
9724
+ gadgetInvocationId: invocationId,
9725
+ depth: event.depth + depth
9726
+ });
9727
+ existingCallback?.(event);
9728
+ };
9729
+ }
9242
9730
  const options = {
9243
9731
  client: this.client,
9244
9732
  model: this.model ?? "openai:gpt-5-nano",
@@ -9264,7 +9752,8 @@ ${endPrefix}`
9264
9752
  gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
9265
9753
  compactionConfig: this.compactionConfig,
9266
9754
  signal: this.signal,
9267
- subagentConfig: this.subagentConfig
9755
+ subagentConfig: this.subagentConfig,
9756
+ onNestedEvent
9268
9757
  };
9269
9758
  return new Agent(AGENT_INTERNAL_KEY, options);
9270
9759
  }
@@ -12420,7 +12909,9 @@ function formatMediaLine(media) {
12420
12909
  }
12421
12910
  function formatGadgetSummary2(result) {
12422
12911
  const gadgetLabel = import_chalk3.default.magenta.bold(result.gadgetName);
12423
- const timeLabel = import_chalk3.default.dim(`${Math.round(result.executionTimeMs)}ms`);
12912
+ const timeLabel = import_chalk3.default.dim(
12913
+ result.executionTimeMs >= 1e3 ? `${(result.executionTimeMs / 1e3).toFixed(1)}s` : `${Math.round(result.executionTimeMs)}ms`
12914
+ );
12424
12915
  const paramsStr = formatParametersInline(result.parameters);
12425
12916
  const paramsLabel = paramsStr ? `${import_chalk3.default.dim("(")}${paramsStr}${import_chalk3.default.dim(")")}` : "";
12426
12917
  if (result.error) {
@@ -12588,6 +13079,8 @@ var StreamProgress = class {
12588
13079
  delayTimeout = null;
12589
13080
  isRunning = false;
12590
13081
  hasRendered = false;
13082
+ lastRenderLineCount = 0;
13083
+ // Track lines rendered for multi-line clearing
12591
13084
  // Current call stats (streaming mode)
12592
13085
  mode = "cumulative";
12593
13086
  model = "";
@@ -12607,6 +13100,99 @@ var StreamProgress = class {
12607
13100
  totalCost = 0;
12608
13101
  iterations = 0;
12609
13102
  currentIteration = 0;
13103
+ // In-flight gadget tracking for concurrent status display
13104
+ inFlightGadgets = /* @__PURE__ */ new Map();
13105
+ // Nested agent tracking for hierarchical subagent display
13106
+ nestedAgents = /* @__PURE__ */ new Map();
13107
+ // Nested gadget tracking for hierarchical subagent display
13108
+ nestedGadgets = /* @__PURE__ */ new Map();
13109
+ /**
13110
+ * Add a gadget to the in-flight tracking (called when gadget_call event received).
13111
+ * Triggers re-render to show the gadget in the status display.
13112
+ */
13113
+ addGadget(invocationId, name, params) {
13114
+ this.inFlightGadgets.set(invocationId, { name, params, startTime: Date.now() });
13115
+ if (this.isRunning && this.isTTY) {
13116
+ this.render();
13117
+ }
13118
+ }
13119
+ /**
13120
+ * Remove a gadget from in-flight tracking (called when gadget_result event received).
13121
+ * Triggers re-render to update the status display.
13122
+ */
13123
+ removeGadget(invocationId) {
13124
+ this.inFlightGadgets.delete(invocationId);
13125
+ if (this.isRunning && this.isTTY) {
13126
+ this.render();
13127
+ }
13128
+ }
13129
+ /**
13130
+ * Check if there are any gadgets currently in flight.
13131
+ */
13132
+ hasInFlightGadgets() {
13133
+ return this.inFlightGadgets.size > 0;
13134
+ }
13135
+ /**
13136
+ * Add a nested agent LLM call (called when nested llm_call_start event received).
13137
+ * Used to display hierarchical progress for subagent gadgets.
13138
+ */
13139
+ addNestedAgent(id, parentInvocationId, depth, model, iteration, inputTokens) {
13140
+ this.nestedAgents.set(id, {
13141
+ parentInvocationId,
13142
+ depth,
13143
+ model,
13144
+ iteration,
13145
+ startTime: Date.now(),
13146
+ inputTokens
13147
+ });
13148
+ if (this.isRunning && this.isTTY) {
13149
+ this.render();
13150
+ }
13151
+ }
13152
+ /**
13153
+ * Update a nested agent with completion info (called when nested llm_call_end event received).
13154
+ */
13155
+ updateNestedAgent(id, outputTokens) {
13156
+ const agent = this.nestedAgents.get(id);
13157
+ if (agent) {
13158
+ agent.outputTokens = outputTokens;
13159
+ if (this.isRunning && this.isTTY) {
13160
+ this.render();
13161
+ }
13162
+ }
13163
+ }
13164
+ /**
13165
+ * Remove a nested agent (called when the nested LLM call completes).
13166
+ */
13167
+ removeNestedAgent(id) {
13168
+ this.nestedAgents.delete(id);
13169
+ if (this.isRunning && this.isTTY) {
13170
+ this.render();
13171
+ }
13172
+ }
13173
+ /**
13174
+ * Add a nested gadget call (called when nested gadget_call event received).
13175
+ */
13176
+ addNestedGadget(id, depth, parentInvocationId, name) {
13177
+ this.nestedGadgets.set(id, {
13178
+ depth,
13179
+ parentInvocationId,
13180
+ name,
13181
+ startTime: Date.now()
13182
+ });
13183
+ if (this.isRunning && this.isTTY) {
13184
+ this.render();
13185
+ }
13186
+ }
13187
+ /**
13188
+ * Remove a nested gadget (called when nested gadget_result event received).
13189
+ */
13190
+ removeNestedGadget(id) {
13191
+ this.nestedGadgets.delete(id);
13192
+ if (this.isRunning && this.isTTY) {
13193
+ this.render();
13194
+ }
13195
+ }
12610
13196
  /**
12611
13197
  * Starts a new LLM call. Switches to streaming mode.
12612
13198
  * @param model - Model name being used
@@ -12733,15 +13319,57 @@ var StreamProgress = class {
12733
13319
  this.isStreaming = true;
12734
13320
  }
12735
13321
  render() {
13322
+ this.clearRenderedLines();
12736
13323
  const spinner = SPINNER_FRAMES[this.frameIndex++ % SPINNER_FRAMES.length];
13324
+ const lines = [];
12737
13325
  if (this.mode === "streaming") {
12738
- this.renderStreamingMode(spinner);
13326
+ lines.push(this.formatStreamingLine(spinner));
12739
13327
  } else {
12740
- this.renderCumulativeMode(spinner);
12741
- }
13328
+ lines.push(this.formatCumulativeLine(spinner));
13329
+ }
13330
+ if (this.isTTY) {
13331
+ for (const [gadgetId, gadget] of this.inFlightGadgets) {
13332
+ const elapsed = ((Date.now() - gadget.startTime) / 1e3).toFixed(1);
13333
+ const gadgetLine = ` ${import_chalk4.default.blue("\u23F5")} ${import_chalk4.default.magenta.bold(gadget.name)}${import_chalk4.default.dim("(...)")} ${import_chalk4.default.dim(elapsed + "s")}`;
13334
+ lines.push(gadgetLine);
13335
+ for (const [_agentId, nested] of this.nestedAgents) {
13336
+ if (nested.parentInvocationId !== gadgetId) continue;
13337
+ const indent = " ".repeat(nested.depth + 1);
13338
+ const nestedElapsed = ((Date.now() - nested.startTime) / 1e3).toFixed(1);
13339
+ const tokens = nested.inputTokens ? ` ${import_chalk4.default.dim("\u2191")}${import_chalk4.default.yellow(formatTokens(nested.inputTokens))}` : "";
13340
+ const outTokens = nested.outputTokens ? ` ${import_chalk4.default.dim("\u2193")}${import_chalk4.default.green(formatTokens(nested.outputTokens))}` : "";
13341
+ const nestedLine = `${indent}${import_chalk4.default.cyan(`#${nested.iteration}`)} ${import_chalk4.default.dim(nested.model)}${tokens}${outTokens} ${import_chalk4.default.dim(nestedElapsed + "s")} ${import_chalk4.default.cyan(spinner)}`;
13342
+ lines.push(nestedLine);
13343
+ }
13344
+ for (const [_nestedId, nestedGadget] of this.nestedGadgets) {
13345
+ if (nestedGadget.parentInvocationId === gadgetId) {
13346
+ const indent = " ".repeat(nestedGadget.depth + 1);
13347
+ const nestedElapsed = ((Date.now() - nestedGadget.startTime) / 1e3).toFixed(1);
13348
+ const nestedGadgetLine = `${indent}${import_chalk4.default.blue("\u23F5")} ${import_chalk4.default.dim(nestedGadget.name + "(...)")} ${import_chalk4.default.dim(nestedElapsed + "s")}`;
13349
+ lines.push(nestedGadgetLine);
13350
+ }
13351
+ }
13352
+ }
13353
+ }
13354
+ this.lastRenderLineCount = lines.length;
13355
+ this.target.write("\r" + lines.join("\n"));
12742
13356
  this.hasRendered = true;
12743
13357
  }
12744
- renderStreamingMode(spinner) {
13358
+ /**
13359
+ * Clears the previously rendered lines (for multi-line status display).
13360
+ */
13361
+ clearRenderedLines() {
13362
+ if (!this.hasRendered || this.lastRenderLineCount === 0) return;
13363
+ this.target.write("\r\x1B[K");
13364
+ for (let i = 1; i < this.lastRenderLineCount; i++) {
13365
+ this.target.write("\x1B[1A\x1B[K");
13366
+ }
13367
+ this.target.write("\r");
13368
+ }
13369
+ /**
13370
+ * Format the streaming mode progress line (returns string, doesn't write).
13371
+ */
13372
+ formatStreamingLine(spinner) {
12745
13373
  const elapsed = ((Date.now() - this.callStartTime) / 1e3).toFixed(1);
12746
13374
  const outTokens = this.callOutputTokensEstimated ? Math.round(this.callOutputChars / FALLBACK_CHARS_PER_TOKEN) : this.callOutputTokens;
12747
13375
  const parts = [];
@@ -12775,7 +13403,7 @@ var StreamProgress = class {
12775
13403
  if (callCost > 0) {
12776
13404
  parts.push(import_chalk4.default.cyan(`$${formatCost(callCost)}`));
12777
13405
  }
12778
- this.target.write(`\r${parts.join(import_chalk4.default.dim(" | "))} ${import_chalk4.default.cyan(spinner)}`);
13406
+ return `${parts.join(import_chalk4.default.dim(" | "))} ${import_chalk4.default.cyan(spinner)}`;
12779
13407
  }
12780
13408
  /**
12781
13409
  * Calculates live cost estimate for the current streaming call.
@@ -12812,7 +13440,10 @@ var StreamProgress = class {
12812
13440
  }
12813
13441
  return this.callInputTokens / limits.contextWindow * 100;
12814
13442
  }
12815
- renderCumulativeMode(spinner) {
13443
+ /**
13444
+ * Format the cumulative mode progress line (returns string, doesn't write).
13445
+ */
13446
+ formatCumulativeLine(spinner) {
12816
13447
  const elapsed = ((Date.now() - this.totalStartTime) / 1e3).toFixed(1);
12817
13448
  const parts = [];
12818
13449
  if (this.model) {
@@ -12828,10 +13459,10 @@ var StreamProgress = class {
12828
13459
  parts.push(import_chalk4.default.dim("cost:") + import_chalk4.default.cyan(` $${formatCost(this.totalCost)}`));
12829
13460
  }
12830
13461
  parts.push(import_chalk4.default.dim(`${elapsed}s`));
12831
- this.target.write(`\r${parts.join(import_chalk4.default.dim(" | "))} ${import_chalk4.default.cyan(spinner)}`);
13462
+ return `${parts.join(import_chalk4.default.dim(" | "))} ${import_chalk4.default.cyan(spinner)}`;
12832
13463
  }
12833
13464
  /**
12834
- * Pauses the progress indicator and clears the line.
13465
+ * Pauses the progress indicator and clears all rendered lines.
12835
13466
  * Can be resumed with start().
12836
13467
  */
12837
13468
  pause() {
@@ -12845,10 +13476,9 @@ var StreamProgress = class {
12845
13476
  this.interval = null;
12846
13477
  }
12847
13478
  this.isRunning = false;
12848
- if (this.hasRendered) {
12849
- this.target.write("\r\x1B[K\x1B[0G");
12850
- this.hasRendered = false;
12851
- }
13479
+ this.clearRenderedLines();
13480
+ this.hasRendered = false;
13481
+ this.lastRenderLineCount = 0;
12852
13482
  }
12853
13483
  /**
12854
13484
  * Completes the progress indicator and clears the line.
@@ -13425,6 +14055,38 @@ Denied: ${result.reason ?? "by user"}`
13425
14055
  "Maximize efficiency by batching independent operations in a single response."
13426
14056
  ].join(" ")
13427
14057
  );
14058
+ if (!options.quiet) {
14059
+ builder.withNestedEventCallback((event) => {
14060
+ if (event.type === "llm_call_start") {
14061
+ const info = event.event;
14062
+ const nestedId = `${event.gadgetInvocationId}:${info.iteration}`;
14063
+ progress.addNestedAgent(
14064
+ nestedId,
14065
+ event.gadgetInvocationId,
14066
+ event.depth,
14067
+ info.model,
14068
+ info.iteration,
14069
+ info.inputTokens
14070
+ );
14071
+ } else if (event.type === "llm_call_end") {
14072
+ const info = event.event;
14073
+ const nestedId = `${event.gadgetInvocationId}:${info.iteration}`;
14074
+ progress.updateNestedAgent(nestedId, info.outputTokens);
14075
+ setTimeout(() => progress.removeNestedAgent(nestedId), 100);
14076
+ } else if (event.type === "gadget_call") {
14077
+ const gadgetEvent = event.event;
14078
+ progress.addNestedGadget(
14079
+ gadgetEvent.call.invocationId,
14080
+ event.depth,
14081
+ event.gadgetInvocationId,
14082
+ gadgetEvent.call.gadgetName
14083
+ );
14084
+ } else if (event.type === "gadget_result") {
14085
+ const resultEvent = event.event;
14086
+ progress.removeNestedGadget(resultEvent.result.invocationId);
14087
+ }
14088
+ });
14089
+ }
13428
14090
  let agent;
13429
14091
  if (options.image || options.audio) {
13430
14092
  const parts = [text(prompt)];
@@ -13449,10 +14111,22 @@ Denied: ${result.reason ?? "by user"}`
13449
14111
  try {
13450
14112
  for await (const event of agent.run()) {
13451
14113
  if (event.type === "text") {
13452
- progress.pause();
13453
14114
  textBuffer += event.content;
14115
+ } else if (event.type === "gadget_call") {
14116
+ flushTextBuffer();
14117
+ if (!options.quiet) {
14118
+ progress.addGadget(
14119
+ event.call.invocationId,
14120
+ event.call.gadgetName,
14121
+ event.call.parameters
14122
+ );
14123
+ progress.start();
14124
+ }
13454
14125
  } else if (event.type === "gadget_result") {
13455
14126
  flushTextBuffer();
14127
+ if (!options.quiet) {
14128
+ progress.removeGadget(event.result.invocationId);
14129
+ }
13456
14130
  progress.pause();
13457
14131
  if (options.quiet) {
13458
14132
  if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
@@ -13467,6 +14141,9 @@ Denied: ${result.reason ?? "by user"}`
13467
14141
  `
13468
14142
  );
13469
14143
  }
14144
+ if (progress.hasInFlightGadgets()) {
14145
+ progress.start();
14146
+ }
13470
14147
  }
13471
14148
  }
13472
14149
  } catch (error) {