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/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, onSubagentEvent) {
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.onSubagentEvent = onSubagentEvent;
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
+ onSubagentEvent: this.onSubagentEvent
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.onSubagentEvent
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,12 @@ var init_agent = __esm({
4520
4851
  // Subagent configuration
4521
4852
  agentContextConfig;
4522
4853
  subagentConfig;
4854
+ // Subagent event callback for subagent gadgets
4855
+ userSubagentEventCallback;
4856
+ // Internal queue for yielding subagent events in run()
4857
+ pendingSubagentEvents = [];
4858
+ // Combined callback that queues events AND calls user callback
4859
+ onSubagentEvent;
4523
4860
  /**
4524
4861
  * Creates a new Agent instance.
4525
4862
  * @internal This constructor is private. Use LLMist.createAgent() or AgentBuilder instead.
@@ -4597,6 +4934,71 @@ var init_agent = __esm({
4597
4934
  temperature: this.temperature
4598
4935
  };
4599
4936
  this.subagentConfig = options.subagentConfig;
4937
+ this.userSubagentEventCallback = options.onSubagentEvent;
4938
+ this.onSubagentEvent = (event) => {
4939
+ this.pendingSubagentEvents.push(event);
4940
+ this.userSubagentEventCallback?.(event);
4941
+ const subagentContext = {
4942
+ parentGadgetInvocationId: event.gadgetInvocationId,
4943
+ depth: event.depth
4944
+ };
4945
+ if (event.type === "llm_call_start") {
4946
+ const info = event.event;
4947
+ void this.hooks?.observers?.onLLMCallStart?.({
4948
+ iteration: info.iteration,
4949
+ options: { model: info.model, messages: [] },
4950
+ logger: this.logger,
4951
+ subagentContext
4952
+ });
4953
+ } else if (event.type === "llm_call_end") {
4954
+ const info = event.event;
4955
+ void this.hooks?.observers?.onLLMCallComplete?.({
4956
+ iteration: info.iteration,
4957
+ options: { model: info.model, messages: [] },
4958
+ finishReason: info.finishReason ?? null,
4959
+ usage: info.outputTokens ? {
4960
+ inputTokens: info.inputTokens ?? 0,
4961
+ outputTokens: info.outputTokens,
4962
+ totalTokens: (info.inputTokens ?? 0) + info.outputTokens
4963
+ } : void 0,
4964
+ rawResponse: "",
4965
+ finalMessage: "",
4966
+ logger: this.logger,
4967
+ subagentContext
4968
+ });
4969
+ } else if (event.type === "gadget_call") {
4970
+ const gadgetEvent = event.event;
4971
+ void this.hooks?.observers?.onGadgetExecutionStart?.({
4972
+ iteration: 0,
4973
+ gadgetName: gadgetEvent.call.gadgetName,
4974
+ invocationId: gadgetEvent.call.invocationId,
4975
+ parameters: gadgetEvent.call.parameters ?? {},
4976
+ logger: this.logger,
4977
+ subagentContext
4978
+ });
4979
+ } else if (event.type === "gadget_result") {
4980
+ const resultEvent = event.event;
4981
+ void this.hooks?.observers?.onGadgetExecutionComplete?.({
4982
+ iteration: 0,
4983
+ gadgetName: resultEvent.result.gadgetName ?? "unknown",
4984
+ invocationId: resultEvent.result.invocationId,
4985
+ parameters: {},
4986
+ executionTimeMs: resultEvent.result.executionTimeMs ?? 0,
4987
+ logger: this.logger,
4988
+ subagentContext
4989
+ });
4990
+ }
4991
+ };
4992
+ }
4993
+ /**
4994
+ * Flush pending subagent events as StreamEvents.
4995
+ * Called from run() to yield queued subagent events from subagent gadgets.
4996
+ */
4997
+ *flushPendingSubagentEvents() {
4998
+ while (this.pendingSubagentEvents.length > 0) {
4999
+ const event = this.pendingSubagentEvents.shift();
5000
+ yield { type: "subagent_event", subagentEvent: event };
5001
+ }
4600
5002
  }
4601
5003
  /**
4602
5004
  * Get the gadget registry for this agent.
@@ -4827,12 +5229,31 @@ var init_agent = __esm({
4827
5229
  client: this.client,
4828
5230
  mediaStore: this.mediaStore,
4829
5231
  agentConfig: this.agentContextConfig,
4830
- subagentConfig: this.subagentConfig
5232
+ subagentConfig: this.subagentConfig,
5233
+ onSubagentEvent: this.onSubagentEvent
4831
5234
  });
4832
- const result = await processor.process(stream2);
4833
- for (const output of result.outputs) {
4834
- yield output;
5235
+ let streamMetadata = null;
5236
+ let gadgetCallCount = 0;
5237
+ const textOutputs = [];
5238
+ const gadgetResults = [];
5239
+ for await (const event of processor.process(stream2)) {
5240
+ if (event.type === "stream_complete") {
5241
+ streamMetadata = event;
5242
+ continue;
5243
+ }
5244
+ if (event.type === "text") {
5245
+ textOutputs.push(event.content);
5246
+ } else if (event.type === "gadget_result") {
5247
+ gadgetCallCount++;
5248
+ gadgetResults.push(event);
5249
+ }
5250
+ yield event;
5251
+ yield* this.flushPendingSubagentEvents();
5252
+ }
5253
+ if (!streamMetadata) {
5254
+ throw new Error("Stream processing completed without metadata event");
4835
5255
  }
5256
+ const result = streamMetadata;
4836
5257
  this.logger.info("LLM response completed", {
4837
5258
  finishReason: result.finishReason,
4838
5259
  usage: result.usage,
@@ -4857,9 +5278,6 @@ var init_agent = __esm({
4857
5278
  });
4858
5279
  let finalMessage = result.finalMessage;
4859
5280
  if (this.hooks.controllers?.afterLLMCall) {
4860
- const gadgetCallCount = result.outputs.filter(
4861
- (output) => output.type === "gadget_result"
4862
- ).length;
4863
5281
  const context = {
4864
5282
  iteration: currentIteration,
4865
5283
  maxIterations: this.maxIterations,
@@ -4889,9 +5307,7 @@ var init_agent = __esm({
4889
5307
  }
4890
5308
  if (result.didExecuteGadgets) {
4891
5309
  if (this.textWithGadgetsHandler) {
4892
- const textContent = result.outputs.filter(
4893
- (output) => output.type === "text"
4894
- ).map((output) => output.content).join("");
5310
+ const textContent = textOutputs.join("");
4895
5311
  if (textContent.trim()) {
4896
5312
  const { gadgetName, parameterMapping, resultMapping } = this.textWithGadgetsHandler;
4897
5313
  this.conversation.addGadgetCallResult(
@@ -4901,7 +5317,7 @@ var init_agent = __esm({
4901
5317
  );
4902
5318
  }
4903
5319
  }
4904
- for (const output of result.outputs) {
5320
+ for (const output of gadgetResults) {
4905
5321
  if (output.type === "gadget_result") {
4906
5322
  const gadgetResult = output.result;
4907
5323
  this.conversation.addGadgetCallResult(
@@ -8409,6 +8825,8 @@ var init_builder = __esm({
8409
8825
  signal;
8410
8826
  trailingMessage;
8411
8827
  subagentConfig;
8828
+ subagentEventCallback;
8829
+ parentContext;
8412
8830
  constructor(client) {
8413
8831
  this.client = client;
8414
8832
  }
@@ -8905,6 +9323,74 @@ var init_builder = __esm({
8905
9323
  this.subagentConfig = config;
8906
9324
  return this;
8907
9325
  }
9326
+ /**
9327
+ * Set the callback for subagent events.
9328
+ *
9329
+ * Subagent gadgets (like BrowseWeb) can use ExecutionContext.onSubagentEvent
9330
+ * to report their internal LLM calls and gadget executions in real-time.
9331
+ * This callback receives those events, enabling hierarchical progress display.
9332
+ *
9333
+ * @param callback - Function to handle subagent events
9334
+ * @returns This builder for chaining
9335
+ *
9336
+ * @example
9337
+ * ```typescript
9338
+ * .withSubagentEventCallback((event) => {
9339
+ * if (event.type === "llm_call_start") {
9340
+ * console.log(` Subagent LLM #${event.event.iteration} starting...`);
9341
+ * } else if (event.type === "gadget_call") {
9342
+ * console.log(` ⏵ ${event.event.call.gadgetName}...`);
9343
+ * }
9344
+ * })
9345
+ * ```
9346
+ */
9347
+ withSubagentEventCallback(callback) {
9348
+ this.subagentEventCallback = callback;
9349
+ return this;
9350
+ }
9351
+ /**
9352
+ * Enable automatic subagent event forwarding to parent agent.
9353
+ *
9354
+ * When building a subagent inside a gadget, call this method to automatically
9355
+ * forward all LLM calls and gadget events to the parent agent. This enables
9356
+ * hierarchical progress display without any manual event handling.
9357
+ *
9358
+ * The method extracts `invocationId` and `onSubagentEvent` from the execution
9359
+ * context and sets up automatic forwarding via hooks and event wrapping.
9360
+ *
9361
+ * @param ctx - ExecutionContext passed to the gadget's execute() method
9362
+ * @param depth - Nesting depth (default: 1 for direct child)
9363
+ * @returns This builder for chaining
9364
+ *
9365
+ * @example
9366
+ * ```typescript
9367
+ * // In a subagent gadget like BrowseWeb - ONE LINE enables auto-forwarding:
9368
+ * execute: async (params, ctx) => {
9369
+ * const agent = new AgentBuilder(client)
9370
+ * .withModel(model)
9371
+ * .withGadgets(Navigate, Click, Screenshot)
9372
+ * .withParentContext(ctx) // <-- This is all you need!
9373
+ * .ask(params.task);
9374
+ *
9375
+ * for await (const event of agent.run()) {
9376
+ * // Events automatically forwarded - just process normally
9377
+ * if (event.type === "text") {
9378
+ * result = event.content;
9379
+ * }
9380
+ * }
9381
+ * }
9382
+ * ```
9383
+ */
9384
+ withParentContext(ctx, depth = 1) {
9385
+ if (ctx.onSubagentEvent && ctx.invocationId) {
9386
+ this.parentContext = {
9387
+ invocationId: ctx.invocationId,
9388
+ onSubagentEvent: ctx.onSubagentEvent,
9389
+ depth
9390
+ };
9391
+ }
9392
+ return this;
9393
+ }
8908
9394
  /**
8909
9395
  * Add an ephemeral trailing message that appears at the end of each LLM request.
8910
9396
  *
@@ -8972,14 +9458,92 @@ ${endPrefix}`
8972
9458
  return this;
8973
9459
  }
8974
9460
  /**
8975
- * Compose the final hooks, including trailing message if configured.
9461
+ * Compose the final hooks, including:
9462
+ * - Trailing message injection (if configured)
9463
+ * - Subagent event forwarding for LLM calls (if parentContext is set)
8976
9464
  */
8977
9465
  composeHooks() {
9466
+ let hooks = this.hooks;
9467
+ if (this.parentContext) {
9468
+ const { invocationId, onSubagentEvent, depth } = this.parentContext;
9469
+ const existingOnLLMCallStart = hooks?.observers?.onLLMCallStart;
9470
+ const existingOnLLMCallComplete = hooks?.observers?.onLLMCallComplete;
9471
+ const existingOnGadgetExecutionStart = hooks?.observers?.onGadgetExecutionStart;
9472
+ const existingOnGadgetExecutionComplete = hooks?.observers?.onGadgetExecutionComplete;
9473
+ hooks = {
9474
+ ...hooks,
9475
+ observers: {
9476
+ ...hooks?.observers,
9477
+ onLLMCallStart: async (context) => {
9478
+ onSubagentEvent({
9479
+ type: "llm_call_start",
9480
+ gadgetInvocationId: invocationId,
9481
+ depth,
9482
+ event: {
9483
+ iteration: context.iteration,
9484
+ model: context.options.model
9485
+ }
9486
+ });
9487
+ if (existingOnLLMCallStart) {
9488
+ await existingOnLLMCallStart(context);
9489
+ }
9490
+ },
9491
+ onLLMCallComplete: async (context) => {
9492
+ onSubagentEvent({
9493
+ type: "llm_call_end",
9494
+ gadgetInvocationId: invocationId,
9495
+ depth,
9496
+ event: {
9497
+ iteration: context.iteration,
9498
+ model: context.options.model,
9499
+ outputTokens: context.usage?.outputTokens,
9500
+ finishReason: context.finishReason
9501
+ }
9502
+ });
9503
+ if (existingOnLLMCallComplete) {
9504
+ await existingOnLLMCallComplete(context);
9505
+ }
9506
+ },
9507
+ onGadgetExecutionStart: async (context) => {
9508
+ onSubagentEvent({
9509
+ type: "gadget_call",
9510
+ gadgetInvocationId: invocationId,
9511
+ depth,
9512
+ event: {
9513
+ call: {
9514
+ invocationId: context.invocationId,
9515
+ gadgetName: context.gadgetName,
9516
+ parameters: context.parameters
9517
+ }
9518
+ }
9519
+ });
9520
+ if (existingOnGadgetExecutionStart) {
9521
+ await existingOnGadgetExecutionStart(context);
9522
+ }
9523
+ },
9524
+ onGadgetExecutionComplete: async (context) => {
9525
+ onSubagentEvent({
9526
+ type: "gadget_result",
9527
+ gadgetInvocationId: invocationId,
9528
+ depth,
9529
+ event: {
9530
+ result: {
9531
+ invocationId: context.invocationId
9532
+ }
9533
+ }
9534
+ });
9535
+ if (existingOnGadgetExecutionComplete) {
9536
+ await existingOnGadgetExecutionComplete(context);
9537
+ }
9538
+ }
9539
+ }
9540
+ };
9541
+ }
8978
9542
  if (!this.trailingMessage) {
8979
- return this.hooks;
9543
+ return hooks;
8980
9544
  }
8981
9545
  const trailingMsg = this.trailingMessage;
8982
- const existingBeforeLLMCall = this.hooks?.controllers?.beforeLLMCall;
9546
+ const existingBeforeLLMCall = hooks?.controllers?.beforeLLMCall;
8983
9547
  const trailingMessageController = async (ctx) => {
8984
9548
  const result = existingBeforeLLMCall ? await existingBeforeLLMCall(ctx) : { action: "proceed" };
8985
9549
  if (result.action === "skip") {
@@ -8994,9 +9558,9 @@ ${endPrefix}`
8994
9558
  };
8995
9559
  };
8996
9560
  return {
8997
- ...this.hooks,
9561
+ ...hooks,
8998
9562
  controllers: {
8999
- ...this.hooks?.controllers,
9563
+ ...hooks?.controllers,
9000
9564
  beforeLLMCall: trailingMessageController
9001
9565
  }
9002
9566
  };
@@ -9057,6 +9621,19 @@ ${endPrefix}`
9057
9621
  this.client = new LLMistClass();
9058
9622
  }
9059
9623
  const registry = GadgetRegistry.from(this.gadgets);
9624
+ let onSubagentEvent = this.subagentEventCallback;
9625
+ if (this.parentContext) {
9626
+ const { invocationId, onSubagentEvent: parentCallback, depth } = this.parentContext;
9627
+ const existingCallback = this.subagentEventCallback;
9628
+ onSubagentEvent = (event) => {
9629
+ parentCallback({
9630
+ ...event,
9631
+ gadgetInvocationId: invocationId,
9632
+ depth: event.depth + depth
9633
+ });
9634
+ existingCallback?.(event);
9635
+ };
9636
+ }
9060
9637
  return {
9061
9638
  client: this.client,
9062
9639
  model: this.model ?? "openai:gpt-5-nano",
@@ -9082,7 +9659,8 @@ ${endPrefix}`
9082
9659
  gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
9083
9660
  compactionConfig: this.compactionConfig,
9084
9661
  signal: this.signal,
9085
- subagentConfig: this.subagentConfig
9662
+ subagentConfig: this.subagentConfig,
9663
+ onSubagentEvent
9086
9664
  };
9087
9665
  }
9088
9666
  ask(userPrompt) {
@@ -9239,6 +9817,19 @@ ${endPrefix}`
9239
9817
  this.client = new LLMistClass();
9240
9818
  }
9241
9819
  const registry = GadgetRegistry.from(this.gadgets);
9820
+ let onSubagentEvent = this.subagentEventCallback;
9821
+ if (this.parentContext) {
9822
+ const { invocationId, onSubagentEvent: parentCallback, depth } = this.parentContext;
9823
+ const existingCallback = this.subagentEventCallback;
9824
+ onSubagentEvent = (event) => {
9825
+ parentCallback({
9826
+ ...event,
9827
+ gadgetInvocationId: invocationId,
9828
+ depth: event.depth + depth
9829
+ });
9830
+ existingCallback?.(event);
9831
+ };
9832
+ }
9242
9833
  const options = {
9243
9834
  client: this.client,
9244
9835
  model: this.model ?? "openai:gpt-5-nano",
@@ -9264,7 +9855,8 @@ ${endPrefix}`
9264
9855
  gadgetOutputLimitPercent: this.gadgetOutputLimitPercent,
9265
9856
  compactionConfig: this.compactionConfig,
9266
9857
  signal: this.signal,
9267
- subagentConfig: this.subagentConfig
9858
+ subagentConfig: this.subagentConfig,
9859
+ onSubagentEvent
9268
9860
  };
9269
9861
  return new Agent(AGENT_INTERNAL_KEY, options);
9270
9862
  }
@@ -9359,7 +9951,7 @@ var import_commander2 = require("commander");
9359
9951
  // package.json
9360
9952
  var package_default = {
9361
9953
  name: "llmist",
9362
- version: "3.0.0",
9954
+ version: "3.1.0",
9363
9955
  description: "TypeScript LLM client with streaming tool execution. Tools fire mid-stream. Built-in function calling works with any model\u2014no structured outputs or native tool support required.",
9364
9956
  type: "module",
9365
9957
  main: "dist/index.cjs",
@@ -12370,20 +12962,53 @@ function renderOverallSummary(metadata) {
12370
12962
  }
12371
12963
  return parts.join(import_chalk3.default.dim(" | "));
12372
12964
  }
12373
- function formatParametersInline(params) {
12965
+ function getRawValue(value) {
12966
+ if (typeof value === "string") {
12967
+ return value;
12968
+ }
12969
+ if (typeof value === "boolean" || typeof value === "number") {
12970
+ return String(value);
12971
+ }
12972
+ return JSON.stringify(value);
12973
+ }
12974
+ function truncateValue(str, maxLen) {
12975
+ if (maxLen <= 0) return "";
12976
+ if (str.length <= maxLen) return str;
12977
+ return `${str.slice(0, maxLen)}\u2026`;
12978
+ }
12979
+ function formatParametersInline(params, maxWidth) {
12374
12980
  if (!params || Object.keys(params).length === 0) {
12375
12981
  return "";
12376
12982
  }
12377
- return Object.entries(params).map(([key, value]) => {
12378
- let formatted;
12379
- if (typeof value === "string") {
12380
- formatted = value.length > 30 ? `${value.slice(0, 30)}\u2026` : value;
12381
- } else if (typeof value === "boolean" || typeof value === "number") {
12382
- formatted = String(value);
12983
+ const entries = Object.entries(params);
12984
+ const defaultLimit = 30;
12985
+ const rawValues = entries.map(([, value]) => getRawValue(value));
12986
+ const overhead = entries.reduce((sum, [key], i) => {
12987
+ return sum + key.length + 1 + (i > 0 ? 2 : 0);
12988
+ }, 0);
12989
+ let limits;
12990
+ if (maxWidth && maxWidth > overhead) {
12991
+ const availableForValues = maxWidth - overhead;
12992
+ const totalRawLength = rawValues.reduce((sum, v) => sum + v.length, 0);
12993
+ if (totalRawLength <= availableForValues) {
12994
+ limits = rawValues.map(() => Infinity);
12383
12995
  } else {
12384
- const json = JSON.stringify(value);
12385
- formatted = json.length > 30 ? `${json.slice(0, 30)}\u2026` : json;
12996
+ const minPerValue = 10;
12997
+ const minTotal = entries.length * minPerValue;
12998
+ if (availableForValues <= minTotal) {
12999
+ limits = rawValues.map(() => Math.max(1, Math.floor(availableForValues / entries.length)));
13000
+ } else {
13001
+ limits = rawValues.map((v) => {
13002
+ const proportion = v.length / totalRawLength;
13003
+ return Math.max(minPerValue, Math.floor(proportion * availableForValues));
13004
+ });
13005
+ }
12386
13006
  }
13007
+ } else {
13008
+ limits = rawValues.map(() => defaultLimit);
13009
+ }
13010
+ return entries.map(([key, _], i) => {
13011
+ const formatted = truncateValue(rawValues[i], limits[i]);
12387
13012
  return `${import_chalk3.default.dim(key)}${import_chalk3.default.dim("=")}${import_chalk3.default.cyan(formatted)}`;
12388
13013
  }).join(import_chalk3.default.dim(", "));
12389
13014
  }
@@ -12419,23 +13044,28 @@ function formatMediaLine(media) {
12419
13044
  return `${import_chalk3.default.dim("[")}${icon} ${id} ${mimeType} ${size}${import_chalk3.default.dim("]")} ${import_chalk3.default.dim("\u2192")} ${path6}`;
12420
13045
  }
12421
13046
  function formatGadgetSummary2(result) {
13047
+ const terminalWidth = process.stdout.columns || 80;
12422
13048
  const gadgetLabel = import_chalk3.default.magenta.bold(result.gadgetName);
12423
- const timeLabel = import_chalk3.default.dim(`${Math.round(result.executionTimeMs)}ms`);
12424
- const paramsStr = formatParametersInline(result.parameters);
12425
- const paramsLabel = paramsStr ? `${import_chalk3.default.dim("(")}${paramsStr}${import_chalk3.default.dim(")")}` : "";
12426
- if (result.error) {
12427
- const errorMsg = result.error.length > 50 ? `${result.error.slice(0, 50)}\u2026` : result.error;
12428
- return `${import_chalk3.default.red("\u2717")} ${gadgetLabel}${paramsLabel} ${import_chalk3.default.red("error:")} ${errorMsg} ${timeLabel}`;
12429
- }
12430
- let outputLabel;
13049
+ const timeStr = result.executionTimeMs >= 1e3 ? `${(result.executionTimeMs / 1e3).toFixed(1)}s` : `${Math.round(result.executionTimeMs)}ms`;
13050
+ const timeLabel = import_chalk3.default.dim(timeStr);
13051
+ let outputStr;
12431
13052
  if (result.tokenCount !== void 0 && result.tokenCount > 0) {
12432
- outputLabel = import_chalk3.default.green(`${formatTokens(result.tokenCount)} tokens`);
13053
+ outputStr = `${formatTokens(result.tokenCount)} tokens`;
12433
13054
  } else if (result.result) {
12434
13055
  const outputBytes = Buffer.byteLength(result.result, "utf-8");
12435
- outputLabel = outputBytes > 0 ? import_chalk3.default.green(formatBytes(outputBytes)) : import_chalk3.default.dim("no output");
13056
+ outputStr = outputBytes > 0 ? formatBytes(outputBytes) : "no output";
12436
13057
  } else {
12437
- outputLabel = import_chalk3.default.dim("no output");
13058
+ outputStr = "no output";
12438
13059
  }
13060
+ const fixedLength = 2 + result.gadgetName.length + 2 + 3 + outputStr.length + 1 + timeStr.length;
13061
+ const availableForParams = Math.max(40, terminalWidth - fixedLength - 2);
13062
+ const paramsStr = formatParametersInline(result.parameters, availableForParams);
13063
+ const paramsLabel = paramsStr ? `${import_chalk3.default.dim("(")}${paramsStr}${import_chalk3.default.dim(")")}` : "";
13064
+ if (result.error) {
13065
+ const errorMsg = result.error.length > 50 ? `${result.error.slice(0, 50)}\u2026` : result.error;
13066
+ return `${import_chalk3.default.red("\u2717")} ${gadgetLabel}${paramsLabel} ${import_chalk3.default.red("error:")} ${errorMsg} ${timeLabel}`;
13067
+ }
13068
+ const outputLabel = outputStr === "no output" ? import_chalk3.default.dim(outputStr) : import_chalk3.default.green(outputStr);
12439
13069
  const icon = result.breaksLoop ? import_chalk3.default.yellow("\u23F9") : import_chalk3.default.green("\u2713");
12440
13070
  let summaryLine = `${icon} ${gadgetLabel}${paramsLabel} ${import_chalk3.default.dim("\u2192")} ${outputLabel} ${timeLabel}`;
12441
13071
  if (result.media && result.media.length > 0) {
@@ -12588,6 +13218,8 @@ var StreamProgress = class {
12588
13218
  delayTimeout = null;
12589
13219
  isRunning = false;
12590
13220
  hasRendered = false;
13221
+ lastRenderLineCount = 0;
13222
+ // Track lines rendered for multi-line clearing
12591
13223
  // Current call stats (streaming mode)
12592
13224
  mode = "cumulative";
12593
13225
  model = "";
@@ -12607,6 +13239,111 @@ var StreamProgress = class {
12607
13239
  totalCost = 0;
12608
13240
  iterations = 0;
12609
13241
  currentIteration = 0;
13242
+ // In-flight gadget tracking for concurrent status display
13243
+ inFlightGadgets = /* @__PURE__ */ new Map();
13244
+ // Nested agent tracking for hierarchical subagent display
13245
+ nestedAgents = /* @__PURE__ */ new Map();
13246
+ // Nested gadget tracking for hierarchical subagent display
13247
+ nestedGadgets = /* @__PURE__ */ new Map();
13248
+ /**
13249
+ * Add a gadget to the in-flight tracking (called when gadget_call event received).
13250
+ * Triggers re-render to show the gadget in the status display.
13251
+ */
13252
+ addGadget(invocationId, name, params) {
13253
+ this.inFlightGadgets.set(invocationId, { name, params, startTime: Date.now() });
13254
+ if (this.isRunning && this.isTTY) {
13255
+ this.render();
13256
+ }
13257
+ }
13258
+ /**
13259
+ * Remove a gadget from in-flight tracking (called when gadget_result event received).
13260
+ * Triggers re-render to update the status display.
13261
+ */
13262
+ removeGadget(invocationId) {
13263
+ this.inFlightGadgets.delete(invocationId);
13264
+ if (this.isRunning && this.isTTY) {
13265
+ this.render();
13266
+ }
13267
+ }
13268
+ /**
13269
+ * Check if there are any gadgets currently in flight.
13270
+ */
13271
+ hasInFlightGadgets() {
13272
+ return this.inFlightGadgets.size > 0;
13273
+ }
13274
+ /**
13275
+ * Add a nested agent LLM call (called when nested llm_call_start event received).
13276
+ * Used to display hierarchical progress for subagent gadgets.
13277
+ */
13278
+ addNestedAgent(id, parentInvocationId, depth, model, iteration, inputTokens) {
13279
+ this.nestedAgents.set(id, {
13280
+ parentInvocationId,
13281
+ depth,
13282
+ model,
13283
+ iteration,
13284
+ startTime: Date.now(),
13285
+ inputTokens
13286
+ });
13287
+ if (this.isRunning && this.isTTY) {
13288
+ this.render();
13289
+ }
13290
+ }
13291
+ /**
13292
+ * Update a nested agent with completion info (called when nested llm_call_end event received).
13293
+ */
13294
+ updateNestedAgent(id, outputTokens) {
13295
+ const agent = this.nestedAgents.get(id);
13296
+ if (agent) {
13297
+ agent.outputTokens = outputTokens;
13298
+ if (this.isRunning && this.isTTY) {
13299
+ this.render();
13300
+ }
13301
+ }
13302
+ }
13303
+ /**
13304
+ * Remove a nested agent (called when the nested LLM call completes).
13305
+ */
13306
+ removeNestedAgent(id) {
13307
+ this.nestedAgents.delete(id);
13308
+ if (this.isRunning && this.isTTY) {
13309
+ this.render();
13310
+ }
13311
+ }
13312
+ /**
13313
+ * Add a nested gadget call (called when nested gadget_call event received).
13314
+ */
13315
+ addNestedGadget(id, depth, parentInvocationId, name) {
13316
+ this.nestedGadgets.set(id, {
13317
+ depth,
13318
+ parentInvocationId,
13319
+ name,
13320
+ startTime: Date.now()
13321
+ });
13322
+ if (this.isRunning && this.isTTY) {
13323
+ this.render();
13324
+ }
13325
+ }
13326
+ /**
13327
+ * Remove a nested gadget (called when nested gadget_result event received).
13328
+ */
13329
+ removeNestedGadget(id) {
13330
+ this.nestedGadgets.delete(id);
13331
+ if (this.isRunning && this.isTTY) {
13332
+ this.render();
13333
+ }
13334
+ }
13335
+ /**
13336
+ * Mark a nested gadget as completed (keeps it visible with ✓ indicator).
13337
+ */
13338
+ completeNestedGadget(id) {
13339
+ const gadget = this.nestedGadgets.get(id);
13340
+ if (gadget) {
13341
+ gadget.completed = true;
13342
+ if (this.isRunning && this.isTTY) {
13343
+ this.render();
13344
+ }
13345
+ }
13346
+ }
12610
13347
  /**
12611
13348
  * Starts a new LLM call. Switches to streaming mode.
12612
13349
  * @param model - Model name being used
@@ -12733,15 +13470,58 @@ var StreamProgress = class {
12733
13470
  this.isStreaming = true;
12734
13471
  }
12735
13472
  render() {
13473
+ this.clearRenderedLines();
12736
13474
  const spinner = SPINNER_FRAMES[this.frameIndex++ % SPINNER_FRAMES.length];
13475
+ const lines = [];
12737
13476
  if (this.mode === "streaming") {
12738
- this.renderStreamingMode(spinner);
13477
+ lines.push(this.formatStreamingLine(spinner));
12739
13478
  } else {
12740
- this.renderCumulativeMode(spinner);
12741
- }
13479
+ lines.push(this.formatCumulativeLine(spinner));
13480
+ }
13481
+ if (this.isTTY) {
13482
+ for (const [gadgetId, gadget] of this.inFlightGadgets) {
13483
+ const elapsed = ((Date.now() - gadget.startTime) / 1e3).toFixed(1);
13484
+ const gadgetLine = ` ${import_chalk4.default.blue("\u23F5")} ${import_chalk4.default.magenta.bold(gadget.name)}${import_chalk4.default.dim("(...)")} ${import_chalk4.default.dim(elapsed + "s")}`;
13485
+ lines.push(gadgetLine);
13486
+ for (const [_agentId, nested] of this.nestedAgents) {
13487
+ if (nested.parentInvocationId !== gadgetId) continue;
13488
+ const indent = " ".repeat(nested.depth + 1);
13489
+ const nestedElapsed = ((Date.now() - nested.startTime) / 1e3).toFixed(1);
13490
+ const tokens = nested.inputTokens ? ` ${import_chalk4.default.dim("\u2191")}${import_chalk4.default.yellow(formatTokens(nested.inputTokens))}` : "";
13491
+ const outTokens = nested.outputTokens ? ` ${import_chalk4.default.dim("\u2193")}${import_chalk4.default.green(formatTokens(nested.outputTokens))}` : "";
13492
+ 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)}`;
13493
+ lines.push(nestedLine);
13494
+ }
13495
+ for (const [nestedId, nestedGadget] of this.nestedGadgets) {
13496
+ if (nestedGadget.parentInvocationId === gadgetId) {
13497
+ const indent = " ".repeat(nestedGadget.depth + 1);
13498
+ const nestedElapsed = ((Date.now() - nestedGadget.startTime) / 1e3).toFixed(1);
13499
+ const icon = nestedGadget.completed ? import_chalk4.default.green("\u2713") : import_chalk4.default.blue("\u23F5");
13500
+ const nestedGadgetLine = `${indent}${icon} ${import_chalk4.default.dim(nestedGadget.name + "(...)")} ${import_chalk4.default.dim(nestedElapsed + "s")}`;
13501
+ lines.push(nestedGadgetLine);
13502
+ }
13503
+ }
13504
+ }
13505
+ }
13506
+ this.lastRenderLineCount = lines.length;
13507
+ this.target.write("\r" + lines.join("\n"));
12742
13508
  this.hasRendered = true;
12743
13509
  }
12744
- renderStreamingMode(spinner) {
13510
+ /**
13511
+ * Clears the previously rendered lines (for multi-line status display).
13512
+ */
13513
+ clearRenderedLines() {
13514
+ if (!this.hasRendered || this.lastRenderLineCount === 0) return;
13515
+ this.target.write("\r\x1B[K");
13516
+ for (let i = 1; i < this.lastRenderLineCount; i++) {
13517
+ this.target.write("\x1B[1A\x1B[K");
13518
+ }
13519
+ this.target.write("\r");
13520
+ }
13521
+ /**
13522
+ * Format the streaming mode progress line (returns string, doesn't write).
13523
+ */
13524
+ formatStreamingLine(spinner) {
12745
13525
  const elapsed = ((Date.now() - this.callStartTime) / 1e3).toFixed(1);
12746
13526
  const outTokens = this.callOutputTokensEstimated ? Math.round(this.callOutputChars / FALLBACK_CHARS_PER_TOKEN) : this.callOutputTokens;
12747
13527
  const parts = [];
@@ -12775,7 +13555,7 @@ var StreamProgress = class {
12775
13555
  if (callCost > 0) {
12776
13556
  parts.push(import_chalk4.default.cyan(`$${formatCost(callCost)}`));
12777
13557
  }
12778
- this.target.write(`\r${parts.join(import_chalk4.default.dim(" | "))} ${import_chalk4.default.cyan(spinner)}`);
13558
+ return `${parts.join(import_chalk4.default.dim(" | "))} ${import_chalk4.default.cyan(spinner)}`;
12779
13559
  }
12780
13560
  /**
12781
13561
  * Calculates live cost estimate for the current streaming call.
@@ -12812,7 +13592,10 @@ var StreamProgress = class {
12812
13592
  }
12813
13593
  return this.callInputTokens / limits.contextWindow * 100;
12814
13594
  }
12815
- renderCumulativeMode(spinner) {
13595
+ /**
13596
+ * Format the cumulative mode progress line (returns string, doesn't write).
13597
+ */
13598
+ formatCumulativeLine(spinner) {
12816
13599
  const elapsed = ((Date.now() - this.totalStartTime) / 1e3).toFixed(1);
12817
13600
  const parts = [];
12818
13601
  if (this.model) {
@@ -12828,10 +13611,10 @@ var StreamProgress = class {
12828
13611
  parts.push(import_chalk4.default.dim("cost:") + import_chalk4.default.cyan(` $${formatCost(this.totalCost)}`));
12829
13612
  }
12830
13613
  parts.push(import_chalk4.default.dim(`${elapsed}s`));
12831
- this.target.write(`\r${parts.join(import_chalk4.default.dim(" | "))} ${import_chalk4.default.cyan(spinner)}`);
13614
+ return `${parts.join(import_chalk4.default.dim(" | "))} ${import_chalk4.default.cyan(spinner)}`;
12832
13615
  }
12833
13616
  /**
12834
- * Pauses the progress indicator and clears the line.
13617
+ * Pauses the progress indicator and clears all rendered lines.
12835
13618
  * Can be resumed with start().
12836
13619
  */
12837
13620
  pause() {
@@ -12845,10 +13628,9 @@ var StreamProgress = class {
12845
13628
  this.interval = null;
12846
13629
  }
12847
13630
  this.isRunning = false;
12848
- if (this.hasRendered) {
12849
- this.target.write("\r\x1B[K\x1B[0G");
12850
- this.hasRendered = false;
12851
- }
13631
+ this.clearRenderedLines();
13632
+ this.hasRendered = false;
13633
+ this.lastRenderLineCount = 0;
12852
13634
  }
12853
13635
  /**
12854
13636
  * Completes the progress indicator and clears the line.
@@ -13425,6 +14207,38 @@ Denied: ${result.reason ?? "by user"}`
13425
14207
  "Maximize efficiency by batching independent operations in a single response."
13426
14208
  ].join(" ")
13427
14209
  );
14210
+ if (!options.quiet) {
14211
+ builder.withSubagentEventCallback((subagentEvent) => {
14212
+ if (subagentEvent.type === "llm_call_start") {
14213
+ const info = subagentEvent.event;
14214
+ const subagentId = `${subagentEvent.gadgetInvocationId}:${info.iteration}`;
14215
+ progress.addNestedAgent(
14216
+ subagentId,
14217
+ subagentEvent.gadgetInvocationId,
14218
+ subagentEvent.depth,
14219
+ info.model,
14220
+ info.iteration,
14221
+ info.inputTokens
14222
+ );
14223
+ } else if (subagentEvent.type === "llm_call_end") {
14224
+ const info = subagentEvent.event;
14225
+ const subagentId = `${subagentEvent.gadgetInvocationId}:${info.iteration}`;
14226
+ progress.updateNestedAgent(subagentId, info.outputTokens);
14227
+ setTimeout(() => progress.removeNestedAgent(subagentId), 100);
14228
+ } else if (subagentEvent.type === "gadget_call") {
14229
+ const gadgetEvent = subagentEvent.event;
14230
+ progress.addNestedGadget(
14231
+ gadgetEvent.call.invocationId,
14232
+ subagentEvent.depth,
14233
+ subagentEvent.gadgetInvocationId,
14234
+ gadgetEvent.call.gadgetName
14235
+ );
14236
+ } else if (subagentEvent.type === "gadget_result") {
14237
+ const resultEvent = subagentEvent.event;
14238
+ progress.completeNestedGadget(resultEvent.result.invocationId);
14239
+ }
14240
+ });
14241
+ }
13428
14242
  let agent;
13429
14243
  if (options.image || options.audio) {
13430
14244
  const parts = [text(prompt)];
@@ -13449,10 +14263,22 @@ Denied: ${result.reason ?? "by user"}`
13449
14263
  try {
13450
14264
  for await (const event of agent.run()) {
13451
14265
  if (event.type === "text") {
13452
- progress.pause();
13453
14266
  textBuffer += event.content;
14267
+ } else if (event.type === "gadget_call") {
14268
+ flushTextBuffer();
14269
+ if (!options.quiet) {
14270
+ progress.addGadget(
14271
+ event.call.invocationId,
14272
+ event.call.gadgetName,
14273
+ event.call.parameters
14274
+ );
14275
+ progress.start();
14276
+ }
13454
14277
  } else if (event.type === "gadget_result") {
13455
14278
  flushTextBuffer();
14279
+ if (!options.quiet) {
14280
+ progress.removeGadget(event.result.invocationId);
14281
+ }
13456
14282
  progress.pause();
13457
14283
  if (options.quiet) {
13458
14284
  if (event.result.gadgetName === "TellUser" && event.result.parameters?.message) {
@@ -13467,6 +14293,10 @@ Denied: ${result.reason ?? "by user"}`
13467
14293
  `
13468
14294
  );
13469
14295
  }
14296
+ if (progress.hasInFlightGadgets()) {
14297
+ progress.start();
14298
+ }
14299
+ } else if (event.type === "subagent_event") {
13470
14300
  }
13471
14301
  }
13472
14302
  } catch (error) {