llmist 1.7.0 → 2.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
@@ -644,6 +644,44 @@ ${this.endPrefix}`
644
644
  }
645
645
  });
646
646
 
647
+ // src/gadgets/exceptions.ts
648
+ var BreakLoopException, HumanInputException, TimeoutException, AbortError;
649
+ var init_exceptions = __esm({
650
+ "src/gadgets/exceptions.ts"() {
651
+ "use strict";
652
+ BreakLoopException = class extends Error {
653
+ constructor(message) {
654
+ super(message ?? "Agent loop terminated by gadget");
655
+ this.name = "BreakLoopException";
656
+ }
657
+ };
658
+ HumanInputException = class extends Error {
659
+ question;
660
+ constructor(question) {
661
+ super(`Human input required: ${question}`);
662
+ this.name = "HumanInputException";
663
+ this.question = question;
664
+ }
665
+ };
666
+ TimeoutException = class extends Error {
667
+ timeoutMs;
668
+ gadgetName;
669
+ constructor(gadgetName, timeoutMs) {
670
+ super(`Gadget '${gadgetName}' execution exceeded timeout of ${timeoutMs}ms`);
671
+ this.name = "TimeoutException";
672
+ this.gadgetName = gadgetName;
673
+ this.timeoutMs = timeoutMs;
674
+ }
675
+ };
676
+ AbortError = class extends Error {
677
+ constructor(message) {
678
+ super(message || "Gadget execution was aborted");
679
+ this.name = "AbortError";
680
+ }
681
+ };
682
+ }
683
+ });
684
+
647
685
  // src/logging/logger.ts
648
686
  function parseLogLevel(value) {
649
687
  if (!value) {
@@ -916,6 +954,7 @@ var init_gadget = __esm({
916
954
  "src/gadgets/gadget.ts"() {
917
955
  "use strict";
918
956
  init_constants();
957
+ init_exceptions();
919
958
  init_schema_to_json();
920
959
  init_schema_validator();
921
960
  BaseGadget = class {
@@ -945,6 +984,136 @@ var init_gadget = __esm({
945
984
  * while maintaining runtime compatibility.
946
985
  */
947
986
  examples;
987
+ /**
988
+ * Throws an AbortError if the execution has been aborted.
989
+ *
990
+ * Call this at key checkpoints in long-running gadgets to allow early exit
991
+ * when the gadget has been cancelled (e.g., due to timeout). This enables
992
+ * resource cleanup and prevents unnecessary work after cancellation.
993
+ *
994
+ * @param ctx - The execution context containing the abort signal
995
+ * @throws AbortError if ctx.signal.aborted is true
996
+ *
997
+ * @example
998
+ * ```typescript
999
+ * class DataProcessor extends Gadget({
1000
+ * description: 'Processes data in multiple steps',
1001
+ * schema: z.object({ items: z.array(z.string()) }),
1002
+ * }) {
1003
+ * async execute(params: this['params'], ctx?: ExecutionContext): Promise<string> {
1004
+ * const results: string[] = [];
1005
+ *
1006
+ * for (const item of params.items) {
1007
+ * // Check before each expensive operation
1008
+ * this.throwIfAborted(ctx);
1009
+ *
1010
+ * results.push(await this.processItem(item));
1011
+ * }
1012
+ *
1013
+ * return results.join(', ');
1014
+ * }
1015
+ * }
1016
+ * ```
1017
+ */
1018
+ throwIfAborted(ctx) {
1019
+ if (ctx?.signal?.aborted) {
1020
+ throw new AbortError();
1021
+ }
1022
+ }
1023
+ /**
1024
+ * Register a cleanup function to run when execution is aborted (timeout or cancellation).
1025
+ * The cleanup function is called immediately if the signal is already aborted.
1026
+ * Errors thrown by the cleanup function are silently ignored.
1027
+ *
1028
+ * Use this to clean up resources like browser instances, database connections,
1029
+ * or child processes when the gadget is cancelled due to timeout.
1030
+ *
1031
+ * @param ctx - The execution context containing the abort signal
1032
+ * @param cleanup - Function to run on abort (can be sync or async)
1033
+ *
1034
+ * @example
1035
+ * ```typescript
1036
+ * class BrowserGadget extends Gadget({
1037
+ * description: 'Fetches web page content',
1038
+ * schema: z.object({ url: z.string() }),
1039
+ * }) {
1040
+ * async execute(params: this['params'], ctx?: ExecutionContext): Promise<string> {
1041
+ * const browser = await chromium.launch();
1042
+ * this.onAbort(ctx, () => browser.close());
1043
+ *
1044
+ * const page = await browser.newPage();
1045
+ * this.onAbort(ctx, () => page.close());
1046
+ *
1047
+ * await page.goto(params.url);
1048
+ * const content = await page.content();
1049
+ *
1050
+ * await browser.close();
1051
+ * return content;
1052
+ * }
1053
+ * }
1054
+ * ```
1055
+ */
1056
+ onAbort(ctx, cleanup) {
1057
+ if (!ctx?.signal) return;
1058
+ const safeCleanup = () => {
1059
+ try {
1060
+ const result = cleanup();
1061
+ if (result && typeof result === "object" && "catch" in result) {
1062
+ result.catch(() => {
1063
+ });
1064
+ }
1065
+ } catch {
1066
+ }
1067
+ };
1068
+ if (ctx.signal.aborted) {
1069
+ safeCleanup();
1070
+ return;
1071
+ }
1072
+ ctx.signal.addEventListener("abort", safeCleanup, { once: true });
1073
+ }
1074
+ /**
1075
+ * Create an AbortController linked to the execution context's signal.
1076
+ * When the parent signal aborts, the returned controller also aborts with the same reason.
1077
+ *
1078
+ * Useful for passing abort signals to child operations like fetch() while still
1079
+ * being able to abort them independently if needed.
1080
+ *
1081
+ * @param ctx - The execution context containing the parent abort signal
1082
+ * @returns A new AbortController linked to the parent signal
1083
+ *
1084
+ * @example
1085
+ * ```typescript
1086
+ * class FetchGadget extends Gadget({
1087
+ * description: 'Fetches data from URL',
1088
+ * schema: z.object({ url: z.string() }),
1089
+ * }) {
1090
+ * async execute(params: this['params'], ctx?: ExecutionContext): Promise<string> {
1091
+ * const controller = this.createLinkedAbortController(ctx);
1092
+ *
1093
+ * // fetch() will automatically abort when parent times out
1094
+ * const response = await fetch(params.url, { signal: controller.signal });
1095
+ * return response.text();
1096
+ * }
1097
+ * }
1098
+ * ```
1099
+ */
1100
+ createLinkedAbortController(ctx) {
1101
+ const controller = new AbortController();
1102
+ if (ctx?.signal) {
1103
+ if (ctx.signal.aborted) {
1104
+ controller.abort(ctx.signal.reason);
1105
+ } else {
1106
+ ctx.signal.addEventListener(
1107
+ "abort",
1108
+ () => {
1109
+ controller.abort(ctx.signal.reason);
1110
+ },
1111
+ { once: true }
1112
+ );
1113
+ }
1114
+ }
1115
+ return controller;
1116
+ }
948
1117
  /**
949
1118
  * Auto-generated instruction text for the LLM.
950
1119
  * Combines name, description, and parameter schema into a formatted instruction.
@@ -1012,8 +1181,8 @@ function createGadget(config) {
1012
1181
  parameterSchema = config.schema;
1013
1182
  timeoutMs = config.timeoutMs;
1014
1183
  examples = config.examples;
1015
- execute(params) {
1016
- return config.execute(params);
1184
+ execute(params, ctx) {
1185
+ return config.execute(params, ctx);
1017
1186
  }
1018
1187
  }
1019
1188
  return new DynamicGadget();
@@ -2319,6 +2488,162 @@ var init_block_params = __esm({
2319
2488
  }
2320
2489
  });
2321
2490
 
2491
+ // src/gadgets/cost-reporting-client.ts
2492
+ var CostReportingLLMistWrapper;
2493
+ var init_cost_reporting_client = __esm({
2494
+ "src/gadgets/cost-reporting-client.ts"() {
2495
+ "use strict";
2496
+ init_model_shortcuts();
2497
+ CostReportingLLMistWrapper = class {
2498
+ constructor(client, reportCost) {
2499
+ this.client = client;
2500
+ this.reportCost = reportCost;
2501
+ }
2502
+ /**
2503
+ * Access to model registry for cost estimation.
2504
+ */
2505
+ get modelRegistry() {
2506
+ return this.client.modelRegistry;
2507
+ }
2508
+ /**
2509
+ * Quick completion with automatic cost reporting.
2510
+ *
2511
+ * Streams internally to track token usage, then reports the calculated cost.
2512
+ *
2513
+ * @param prompt - User prompt
2514
+ * @param options - Optional configuration (model, temperature, etc.)
2515
+ * @returns Complete text response
2516
+ */
2517
+ async complete(prompt, options) {
2518
+ const model = resolveModel(options?.model ?? "haiku");
2519
+ let result = "";
2520
+ let inputTokens = 0;
2521
+ let outputTokens = 0;
2522
+ let cachedInputTokens = 0;
2523
+ let cacheCreationInputTokens = 0;
2524
+ const messages = [
2525
+ ...options?.systemPrompt ? [{ role: "system", content: options.systemPrompt }] : [],
2526
+ { role: "user", content: prompt }
2527
+ ];
2528
+ for await (const chunk of this.client.stream({
2529
+ model,
2530
+ messages,
2531
+ temperature: options?.temperature,
2532
+ maxTokens: options?.maxTokens
2533
+ })) {
2534
+ result += chunk.text ?? "";
2535
+ if (chunk.usage) {
2536
+ inputTokens = chunk.usage.inputTokens;
2537
+ outputTokens = chunk.usage.outputTokens;
2538
+ cachedInputTokens = chunk.usage.cachedInputTokens ?? 0;
2539
+ cacheCreationInputTokens = chunk.usage.cacheCreationInputTokens ?? 0;
2540
+ }
2541
+ }
2542
+ this.reportCostFromUsage(model, inputTokens, outputTokens, cachedInputTokens, cacheCreationInputTokens);
2543
+ return result;
2544
+ }
2545
+ /**
2546
+ * Quick streaming with automatic cost reporting when stream completes.
2547
+ *
2548
+ * Yields text chunks as they arrive, then reports cost in finally block.
2549
+ *
2550
+ * @param prompt - User prompt
2551
+ * @param options - Optional configuration (model, temperature, etc.)
2552
+ * @returns Async generator yielding text chunks
2553
+ */
2554
+ async *streamText(prompt, options) {
2555
+ const model = resolveModel(options?.model ?? "haiku");
2556
+ let inputTokens = 0;
2557
+ let outputTokens = 0;
2558
+ let cachedInputTokens = 0;
2559
+ let cacheCreationInputTokens = 0;
2560
+ const messages = [
2561
+ ...options?.systemPrompt ? [{ role: "system", content: options.systemPrompt }] : [],
2562
+ { role: "user", content: prompt }
2563
+ ];
2564
+ try {
2565
+ for await (const chunk of this.client.stream({
2566
+ model,
2567
+ messages,
2568
+ temperature: options?.temperature,
2569
+ maxTokens: options?.maxTokens
2570
+ })) {
2571
+ if (chunk.text) {
2572
+ yield chunk.text;
2573
+ }
2574
+ if (chunk.usage) {
2575
+ inputTokens = chunk.usage.inputTokens;
2576
+ outputTokens = chunk.usage.outputTokens;
2577
+ cachedInputTokens = chunk.usage.cachedInputTokens ?? 0;
2578
+ cacheCreationInputTokens = chunk.usage.cacheCreationInputTokens ?? 0;
2579
+ }
2580
+ }
2581
+ } finally {
2582
+ this.reportCostFromUsage(model, inputTokens, outputTokens, cachedInputTokens, cacheCreationInputTokens);
2583
+ }
2584
+ }
2585
+ /**
2586
+ * Low-level stream access with automatic cost reporting.
2587
+ *
2588
+ * Returns a wrapped stream that reports costs when iteration completes.
2589
+ *
2590
+ * @param options - Full LLM generation options
2591
+ * @returns Wrapped LLM stream that auto-reports costs
2592
+ */
2593
+ stream(options) {
2594
+ return this.createCostReportingStream(options);
2595
+ }
2596
+ /**
2597
+ * Creates a wrapped stream that tracks usage and reports costs on completion.
2598
+ */
2599
+ createCostReportingStream(options) {
2600
+ const innerStream = this.client.stream(options);
2601
+ const reportCostFromUsage = this.reportCostFromUsage.bind(this);
2602
+ const model = options.model;
2603
+ async function* costReportingWrapper() {
2604
+ let inputTokens = 0;
2605
+ let outputTokens = 0;
2606
+ let cachedInputTokens = 0;
2607
+ let cacheCreationInputTokens = 0;
2608
+ try {
2609
+ for await (const chunk of innerStream) {
2610
+ if (chunk.usage) {
2611
+ inputTokens = chunk.usage.inputTokens;
2612
+ outputTokens = chunk.usage.outputTokens;
2613
+ cachedInputTokens = chunk.usage.cachedInputTokens ?? 0;
2614
+ cacheCreationInputTokens = chunk.usage.cacheCreationInputTokens ?? 0;
2615
+ }
2616
+ yield chunk;
2617
+ }
2618
+ } finally {
2619
+ if (inputTokens > 0 || outputTokens > 0) {
2620
+ reportCostFromUsage(model, inputTokens, outputTokens, cachedInputTokens, cacheCreationInputTokens);
2621
+ }
2622
+ }
2623
+ }
2624
+ return costReportingWrapper();
2625
+ }
2626
+ /**
2627
+ * Calculates and reports cost from token usage.
2628
+ */
2629
+ reportCostFromUsage(model, inputTokens, outputTokens, cachedInputTokens = 0, cacheCreationInputTokens = 0) {
2630
+ if (inputTokens === 0 && outputTokens === 0) return;
2631
+ const modelName = model.includes(":") ? model.split(":")[1] : model;
2632
+ const estimate = this.client.modelRegistry.estimateCost(
2633
+ modelName,
2634
+ inputTokens,
2635
+ outputTokens,
2636
+ cachedInputTokens,
2637
+ cacheCreationInputTokens
2638
+ );
2639
+ if (estimate && estimate.totalCost > 0) {
2640
+ this.reportCost(estimate.totalCost);
2641
+ }
2642
+ }
2643
+ };
2644
+ }
2645
+ });
2646
+
2322
2647
  // src/gadgets/error-formatter.ts
2323
2648
  var GadgetErrorFormatter;
2324
2649
  var init_error_formatter = __esm({
@@ -2402,38 +2727,6 @@ var init_error_formatter = __esm({
2402
2727
  }
2403
2728
  });
2404
2729
 
2405
- // src/gadgets/exceptions.ts
2406
- var BreakLoopException, HumanInputException, TimeoutException;
2407
- var init_exceptions = __esm({
2408
- "src/gadgets/exceptions.ts"() {
2409
- "use strict";
2410
- BreakLoopException = class extends Error {
2411
- constructor(message) {
2412
- super(message ?? "Agent loop terminated by gadget");
2413
- this.name = "BreakLoopException";
2414
- }
2415
- };
2416
- HumanInputException = class extends Error {
2417
- question;
2418
- constructor(question) {
2419
- super(`Human input required: ${question}`);
2420
- this.name = "HumanInputException";
2421
- this.question = question;
2422
- }
2423
- };
2424
- TimeoutException = class extends Error {
2425
- timeoutMs;
2426
- gadgetName;
2427
- constructor(gadgetName, timeoutMs) {
2428
- super(`Gadget '${gadgetName}' execution exceeded timeout of ${timeoutMs}ms`);
2429
- this.name = "TimeoutException";
2430
- this.gadgetName = gadgetName;
2431
- this.timeoutMs = timeoutMs;
2432
- }
2433
- };
2434
- }
2435
- });
2436
-
2437
2730
  // src/gadgets/parser.ts
2438
2731
  function stripMarkdownFences(content) {
2439
2732
  let cleaned = content.trim();
@@ -2619,14 +2912,16 @@ var init_executor = __esm({
2619
2912
  init_constants();
2620
2913
  init_logger();
2621
2914
  init_block_params();
2915
+ init_cost_reporting_client();
2622
2916
  init_error_formatter();
2623
2917
  init_exceptions();
2624
2918
  init_parser();
2625
2919
  GadgetExecutor = class {
2626
- constructor(registry, onHumanInputRequired, logger, defaultGadgetTimeoutMs, errorFormatterOptions) {
2920
+ constructor(registry, onHumanInputRequired, logger, defaultGadgetTimeoutMs, errorFormatterOptions, client) {
2627
2921
  this.registry = registry;
2628
2922
  this.onHumanInputRequired = onHumanInputRequired;
2629
2923
  this.defaultGadgetTimeoutMs = defaultGadgetTimeoutMs;
2924
+ this.client = client;
2630
2925
  this.logger = logger ?? createLogger({ name: "llmist:executor" });
2631
2926
  this.errorFormatter = new GadgetErrorFormatter(errorFormatterOptions);
2632
2927
  this.argPrefix = errorFormatterOptions?.argPrefix ?? GADGET_ARG_PREFIX;
@@ -2636,14 +2931,27 @@ var init_executor = __esm({
2636
2931
  argPrefix;
2637
2932
  /**
2638
2933
  * Creates a promise that rejects with a TimeoutException after the specified timeout.
2934
+ * Aborts the provided AbortController before rejecting, allowing gadgets to clean up.
2639
2935
  */
2640
- createTimeoutPromise(gadgetName, timeoutMs) {
2936
+ createTimeoutPromise(gadgetName, timeoutMs, abortController) {
2641
2937
  return new Promise((_, reject) => {
2642
2938
  setTimeout(() => {
2643
- reject(new TimeoutException(gadgetName, timeoutMs));
2939
+ const timeoutError = new TimeoutException(gadgetName, timeoutMs);
2940
+ abortController.abort(timeoutError.message);
2941
+ reject(timeoutError);
2644
2942
  }, timeoutMs);
2645
2943
  });
2646
2944
  }
2945
+ /**
2946
+ * Normalizes gadget execute result to consistent format.
2947
+ * Handles both string returns (backwards compat) and object returns with cost.
2948
+ */
2949
+ normalizeExecuteResult(raw) {
2950
+ if (typeof raw === "string") {
2951
+ return { result: raw, cost: 0 };
2952
+ }
2953
+ return { result: raw.result, cost: raw.cost ?? 0 };
2954
+ }
2647
2955
  // Execute a gadget call asynchronously
2648
2956
  async execute(call) {
2649
2957
  const startTime = Date.now();
@@ -2738,30 +3046,53 @@ var init_executor = __esm({
2738
3046
  validatedParameters = schemaAwareParameters;
2739
3047
  }
2740
3048
  const timeoutMs = gadget.timeoutMs ?? this.defaultGadgetTimeoutMs;
2741
- let result;
3049
+ const abortController = new AbortController();
3050
+ let callbackCost = 0;
3051
+ const reportCost = (amount) => {
3052
+ if (amount > 0) {
3053
+ callbackCost += amount;
3054
+ this.logger.debug("Gadget reported cost via callback", {
3055
+ gadgetName: call.gadgetName,
3056
+ amount,
3057
+ totalCallbackCost: callbackCost
3058
+ });
3059
+ }
3060
+ };
3061
+ const ctx = {
3062
+ reportCost,
3063
+ llmist: this.client ? new CostReportingLLMistWrapper(this.client, reportCost) : void 0,
3064
+ signal: abortController.signal
3065
+ };
3066
+ let rawResult;
2742
3067
  if (timeoutMs && timeoutMs > 0) {
2743
3068
  this.logger.debug("Executing gadget with timeout", {
2744
3069
  gadgetName: call.gadgetName,
2745
3070
  timeoutMs
2746
3071
  });
2747
- result = await Promise.race([
2748
- Promise.resolve(gadget.execute(validatedParameters)),
2749
- this.createTimeoutPromise(call.gadgetName, timeoutMs)
3072
+ rawResult = await Promise.race([
3073
+ Promise.resolve(gadget.execute(validatedParameters, ctx)),
3074
+ this.createTimeoutPromise(call.gadgetName, timeoutMs, abortController)
2750
3075
  ]);
2751
3076
  } else {
2752
- result = await Promise.resolve(gadget.execute(validatedParameters));
3077
+ rawResult = await Promise.resolve(gadget.execute(validatedParameters, ctx));
2753
3078
  }
3079
+ const { result, cost: returnCost } = this.normalizeExecuteResult(rawResult);
3080
+ const totalCost = callbackCost + returnCost;
2754
3081
  const executionTimeMs = Date.now() - startTime;
2755
3082
  this.logger.info("Gadget executed successfully", {
2756
3083
  gadgetName: call.gadgetName,
2757
3084
  invocationId: call.invocationId,
2758
- executionTimeMs
3085
+ executionTimeMs,
3086
+ cost: totalCost > 0 ? totalCost : void 0,
3087
+ callbackCost: callbackCost > 0 ? callbackCost : void 0,
3088
+ returnCost: returnCost > 0 ? returnCost : void 0
2759
3089
  });
2760
3090
  this.logger.debug("Gadget result", {
2761
3091
  gadgetName: call.gadgetName,
2762
3092
  invocationId: call.invocationId,
2763
3093
  parameters: validatedParameters,
2764
3094
  result,
3095
+ cost: totalCost,
2765
3096
  executionTimeMs
2766
3097
  });
2767
3098
  return {
@@ -2769,7 +3100,8 @@ var init_executor = __esm({
2769
3100
  invocationId: call.invocationId,
2770
3101
  parameters: validatedParameters,
2771
3102
  result,
2772
- executionTimeMs
3103
+ executionTimeMs,
3104
+ cost: totalCost
2773
3105
  };
2774
3106
  } catch (error) {
2775
3107
  if (error instanceof BreakLoopException) {
@@ -2800,6 +3132,19 @@ var init_executor = __esm({
2800
3132
  executionTimeMs: Date.now() - startTime
2801
3133
  };
2802
3134
  }
3135
+ if (error instanceof AbortError) {
3136
+ this.logger.info("Gadget execution was aborted", {
3137
+ gadgetName: call.gadgetName,
3138
+ executionTimeMs: Date.now() - startTime
3139
+ });
3140
+ return {
3141
+ gadgetName: call.gadgetName,
3142
+ invocationId: call.invocationId,
3143
+ parameters: validatedParameters,
3144
+ error: error.message,
3145
+ executionTimeMs: Date.now() - startTime
3146
+ };
3147
+ }
2803
3148
  if (error instanceof HumanInputException) {
2804
3149
  this.logger.info("Gadget requested human input", {
2805
3150
  gadgetName: call.gadgetName,
@@ -2926,7 +3271,8 @@ var init_stream_processor = __esm({
2926
3271
  options.onHumanInputRequired,
2927
3272
  this.logger.getSubLogger({ name: "executor" }),
2928
3273
  options.defaultGadgetTimeoutMs,
2929
- { argPrefix: options.gadgetArgPrefix }
3274
+ { argPrefix: options.gadgetArgPrefix },
3275
+ options.client
2930
3276
  );
2931
3277
  }
2932
3278
  /**
@@ -3193,6 +3539,7 @@ var init_stream_processor = __esm({
3193
3539
  error: result.error,
3194
3540
  executionTimeMs: result.executionTimeMs,
3195
3541
  breaksLoop: result.breaksLoop,
3542
+ cost: result.cost,
3196
3543
  logger: this.logger
3197
3544
  };
3198
3545
  await this.hooks.observers.onGadgetExecutionComplete(context);
@@ -3549,6 +3896,17 @@ var init_agent = __esm({
3549
3896
  llmOptions = { ...llmOptions, ...action.modifiedOptions };
3550
3897
  }
3551
3898
  }
3899
+ await this.safeObserve(async () => {
3900
+ if (this.hooks.observers?.onLLMCallReady) {
3901
+ const context = {
3902
+ iteration: currentIteration,
3903
+ maxIterations: this.maxIterations,
3904
+ options: llmOptions,
3905
+ logger: this.logger
3906
+ };
3907
+ await this.hooks.observers.onLLMCallReady(context);
3908
+ }
3909
+ });
3552
3910
  this.logger.info("Calling LLM", { model: this.model });
3553
3911
  this.logger.silly("LLM request details", {
3554
3912
  model: llmOptions.model,
@@ -3569,7 +3927,8 @@ var init_agent = __esm({
3569
3927
  onHumanInputRequired: this.onHumanInputRequired,
3570
3928
  stopOnGadgetError: this.stopOnGadgetError,
3571
3929
  shouldContinueAfterError: this.shouldContinueAfterError,
3572
- defaultGadgetTimeoutMs: this.defaultGadgetTimeoutMs
3930
+ defaultGadgetTimeoutMs: this.defaultGadgetTimeoutMs,
3931
+ client: this.client
3573
3932
  });
3574
3933
  const result = await processor.process(stream2);
3575
3934
  for (const output of result.outputs) {
@@ -6634,7 +6993,6 @@ var OPTION_FLAGS = {
6634
6993
  logFile: "--log-file <path>",
6635
6994
  logReset: "--log-reset",
6636
6995
  logLlmRequests: "--log-llm-requests [dir]",
6637
- logLlmResponses: "--log-llm-responses [dir]",
6638
6996
  noBuiltins: "--no-builtins",
6639
6997
  noBuiltinInteraction: "--no-builtin-interaction",
6640
6998
  quiet: "-q, --quiet",
@@ -6653,8 +7011,7 @@ var OPTION_DESCRIPTIONS = {
6653
7011
  logLevel: "Log level: silly, trace, debug, info, warn, error, fatal.",
6654
7012
  logFile: "Path to log file. When set, logs are written to file instead of stderr.",
6655
7013
  logReset: "Reset (truncate) the log file at session start instead of appending.",
6656
- logLlmRequests: "Save raw LLM requests as plain text. Optional dir, defaults to ~/.llmist/logs/requests/",
6657
- logLlmResponses: "Save raw LLM responses as plain text. Optional dir, defaults to ~/.llmist/logs/responses/",
7014
+ logLlmRequests: "Save LLM requests/responses to session directories. Optional dir, defaults to ~/.llmist/logs/requests/",
6658
7015
  noBuiltins: "Disable built-in gadgets (AskUser, TellUser).",
6659
7016
  noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser.",
6660
7017
  quiet: "Suppress all output except content (text and TellUser messages).",
@@ -6671,7 +7028,7 @@ var import_commander2 = require("commander");
6671
7028
  // package.json
6672
7029
  var package_default = {
6673
7030
  name: "llmist",
6674
- version: "1.6.2",
7031
+ version: "2.0.0",
6675
7032
  description: "Universal TypeScript LLM client with streaming-first agent framework. Works with any model - no structured outputs or native tool calling required. Implements its own flexible grammar for function calling.",
6676
7033
  type: "module",
6677
7034
  main: "dist/index.cjs",
@@ -6844,6 +7201,14 @@ ${addedLines}`;
6844
7201
  }
6845
7202
 
6846
7203
  // src/cli/approval/context-providers.ts
7204
+ function formatGadgetSummary(gadgetName, params) {
7205
+ const paramEntries = Object.entries(params);
7206
+ if (paramEntries.length === 0) {
7207
+ return `${gadgetName}()`;
7208
+ }
7209
+ const paramStr = paramEntries.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
7210
+ return `${gadgetName}(${paramStr})`;
7211
+ }
6847
7212
  var WriteFileContextProvider = class {
6848
7213
  gadgetName = "WriteFile";
6849
7214
  async getContext(params) {
@@ -6852,14 +7217,14 @@ var WriteFileContextProvider = class {
6852
7217
  const resolvedPath = (0, import_node_path2.resolve)(process.cwd(), filePath);
6853
7218
  if (!(0, import_node_fs2.existsSync)(resolvedPath)) {
6854
7219
  return {
6855
- summary: `Create new file: ${filePath}`,
7220
+ summary: formatGadgetSummary(this.gadgetName, params),
6856
7221
  details: formatNewFileDiff(filePath, newContent)
6857
7222
  };
6858
7223
  }
6859
7224
  const oldContent = (0, import_node_fs2.readFileSync)(resolvedPath, "utf-8");
6860
7225
  const diff = (0, import_diff.createPatch)(filePath, oldContent, newContent, "original", "modified");
6861
7226
  return {
6862
- summary: `Modify: ${filePath}`,
7227
+ summary: formatGadgetSummary(this.gadgetName, params),
6863
7228
  details: diff
6864
7229
  };
6865
7230
  }
@@ -6873,37 +7238,27 @@ var EditFileContextProvider = class {
6873
7238
  const newContent = String(params.content);
6874
7239
  if (!(0, import_node_fs2.existsSync)(resolvedPath)) {
6875
7240
  return {
6876
- summary: `Create new file: ${filePath}`,
7241
+ summary: formatGadgetSummary(this.gadgetName, params),
6877
7242
  details: formatNewFileDiff(filePath, newContent)
6878
7243
  };
6879
7244
  }
6880
7245
  const oldContent = (0, import_node_fs2.readFileSync)(resolvedPath, "utf-8");
6881
7246
  const diff = (0, import_diff.createPatch)(filePath, oldContent, newContent, "original", "modified");
6882
7247
  return {
6883
- summary: `Modify: ${filePath}`,
7248
+ summary: formatGadgetSummary(this.gadgetName, params),
6884
7249
  details: diff
6885
7250
  };
6886
7251
  }
6887
7252
  if ("commands" in params) {
6888
7253
  const commands = String(params.commands);
6889
7254
  return {
6890
- summary: `Edit: ${filePath}`,
7255
+ summary: formatGadgetSummary(this.gadgetName, params),
6891
7256
  details: `Commands:
6892
7257
  ${commands}`
6893
7258
  };
6894
7259
  }
6895
7260
  return {
6896
- summary: `Edit: ${filePath}`
6897
- };
6898
- }
6899
- };
6900
- var RunCommandContextProvider = class {
6901
- gadgetName = "RunCommand";
6902
- async getContext(params) {
6903
- const command = String(params.command ?? "");
6904
- const cwd = params.cwd ? ` (in ${params.cwd})` : "";
6905
- return {
6906
- summary: `Execute: ${command}${cwd}`
7261
+ summary: formatGadgetSummary(this.gadgetName, params)
6907
7262
  };
6908
7263
  }
6909
7264
  };
@@ -6912,27 +7267,15 @@ var DefaultContextProvider = class {
6912
7267
  this.gadgetName = gadgetName;
6913
7268
  }
6914
7269
  async getContext(params) {
6915
- const paramEntries = Object.entries(params);
6916
- if (paramEntries.length === 0) {
6917
- return {
6918
- summary: `${this.gadgetName}()`
6919
- };
6920
- }
6921
- const formatValue = (value) => {
6922
- const MAX_LEN = 50;
6923
- const str = JSON.stringify(value);
6924
- return str.length > MAX_LEN ? `${str.slice(0, MAX_LEN - 3)}...` : str;
6925
- };
6926
- const paramStr = paramEntries.map(([k, v]) => `${k}=${formatValue(v)}`).join(", ");
6927
7270
  return {
6928
- summary: `${this.gadgetName}(${paramStr})`
7271
+ summary: formatGadgetSummary(this.gadgetName, params)
6929
7272
  };
6930
7273
  }
6931
7274
  };
6932
7275
  var builtinContextProviders = [
6933
7276
  new WriteFileContextProvider(),
6934
- new EditFileContextProvider(),
6935
- new RunCommandContextProvider()
7277
+ new EditFileContextProvider()
7278
+ // Note: RunCommand uses DefaultContextProvider - no custom details needed
6936
7279
  ];
6937
7280
 
6938
7281
  // src/cli/approval/manager.ts
@@ -6943,11 +7286,13 @@ var ApprovalManager = class {
6943
7286
  * @param config - Approval configuration with per-gadget modes
6944
7287
  * @param env - CLI environment for I/O operations
6945
7288
  * @param progress - Optional progress indicator to pause during prompts
7289
+ * @param keyboard - Optional keyboard coordinator to disable ESC listener during prompts
6946
7290
  */
6947
- constructor(config, env, progress) {
7291
+ constructor(config, env, progress, keyboard) {
6948
7292
  this.config = config;
6949
7293
  this.env = env;
6950
7294
  this.progress = progress;
7295
+ this.keyboard = keyboard;
6951
7296
  for (const provider of builtinContextProviders) {
6952
7297
  this.registerProvider(provider);
6953
7298
  }
@@ -7016,26 +7361,34 @@ var ApprovalManager = class {
7016
7361
  const provider = this.providers.get(gadgetName.toLowerCase()) ?? new DefaultContextProvider(gadgetName);
7017
7362
  const context = await provider.getContext(params);
7018
7363
  this.progress?.pause();
7019
- this.env.stderr.write(`
7364
+ if (this.keyboard?.cleanupEsc) {
7365
+ this.keyboard.cleanupEsc();
7366
+ this.keyboard.cleanupEsc = null;
7367
+ }
7368
+ try {
7369
+ this.env.stderr.write(`
7020
7370
  ${import_chalk2.default.yellow("\u{1F512} Approval required:")} ${context.summary}
7021
7371
  `);
7022
- if (context.details) {
7023
- this.env.stderr.write(`
7372
+ if (context.details) {
7373
+ this.env.stderr.write(`
7024
7374
  ${renderColoredDiff(context.details)}
7025
7375
  `);
7026
- }
7027
- const response = await this.prompt(" \u23CE approve, or type to reject: ");
7028
- const isApproved = response === "" || response.toLowerCase() === "y";
7029
- if (isApproved) {
7030
- this.env.stderr.write(` ${import_chalk2.default.green("\u2713 Approved")}
7376
+ }
7377
+ const response = await this.prompt(" \u23CE approve, or type to reject: ");
7378
+ const isApproved = response === "" || response.toLowerCase() === "y";
7379
+ if (isApproved) {
7380
+ this.env.stderr.write(` ${import_chalk2.default.green("\u2713 Approved")}
7031
7381
 
7032
7382
  `);
7033
- return { approved: true };
7034
- }
7035
- this.env.stderr.write(` ${import_chalk2.default.red("\u2717 Denied")}
7383
+ return { approved: true };
7384
+ }
7385
+ this.env.stderr.write(` ${import_chalk2.default.red("\u2717 Denied")}
7036
7386
 
7037
7387
  `);
7038
- return { approved: false, reason: response || "Rejected by user" };
7388
+ return { approved: false, reason: response || "Rejected by user" };
7389
+ } finally {
7390
+ this.keyboard?.restore();
7391
+ }
7039
7392
  }
7040
7393
  /**
7041
7394
  * Prompts for user input.
@@ -7534,49 +7887,77 @@ error: ${message}`;
7534
7887
  var import_zod8 = require("zod");
7535
7888
  var runCommand = createGadget({
7536
7889
  name: "RunCommand",
7537
- description: "Execute a shell command and return its output. Returns both stdout and stderr combined with the exit status.",
7890
+ description: "Execute a command with arguments and return its output. Uses argv array to bypass shell - arguments are passed directly without interpretation. Returns stdout/stderr combined with exit status.",
7538
7891
  schema: import_zod8.z.object({
7539
- command: import_zod8.z.string().describe("The shell command to execute"),
7892
+ argv: import_zod8.z.array(import_zod8.z.string()).describe("Command and arguments as array (e.g., ['git', 'commit', '-m', 'message'])"),
7540
7893
  cwd: import_zod8.z.string().optional().describe("Working directory for the command (default: current directory)"),
7541
7894
  timeout: import_zod8.z.number().default(3e4).describe("Timeout in milliseconds (default: 30000)")
7542
7895
  }),
7543
7896
  examples: [
7544
7897
  {
7545
- params: { command: "ls -la", timeout: 3e4 },
7898
+ params: { argv: ["ls", "-la"], timeout: 3e4 },
7546
7899
  output: "status=0\n\ntotal 24\ndrwxr-xr-x 5 user staff 160 Nov 27 10:00 .\ndrwxr-xr-x 3 user staff 96 Nov 27 09:00 ..\n-rw-r--r-- 1 user staff 1024 Nov 27 10:00 package.json",
7547
7900
  comment: "List directory contents with details"
7548
7901
  },
7549
7902
  {
7550
- params: { command: "echo 'Hello World'", timeout: 3e4 },
7903
+ params: { argv: ["echo", "Hello World"], timeout: 3e4 },
7551
7904
  output: "status=0\n\nHello World",
7552
- comment: "Simple echo command"
7905
+ comment: "Echo without shell - argument passed directly"
7553
7906
  },
7554
7907
  {
7555
- params: { command: "cat nonexistent.txt", timeout: 3e4 },
7908
+ params: { argv: ["cat", "nonexistent.txt"], timeout: 3e4 },
7556
7909
  output: "status=1\n\ncat: nonexistent.txt: No such file or directory",
7557
7910
  comment: "Command that fails returns non-zero status"
7558
7911
  },
7559
7912
  {
7560
- params: { command: "pwd", cwd: "/tmp", timeout: 3e4 },
7913
+ params: { argv: ["pwd"], cwd: "/tmp", timeout: 3e4 },
7561
7914
  output: "status=0\n\n/tmp",
7562
7915
  comment: "Execute command in a specific directory"
7916
+ },
7917
+ {
7918
+ params: { argv: ["gh", "pr", "review", "123", "--comment", "--body", "Review with `backticks` and 'quotes'"], timeout: 3e4 },
7919
+ output: "status=0\n\n(no output)",
7920
+ comment: "Complex arguments with special characters - no escaping needed"
7921
+ },
7922
+ {
7923
+ params: {
7924
+ argv: [
7925
+ "gh",
7926
+ "pr",
7927
+ "review",
7928
+ "123",
7929
+ "--approve",
7930
+ "--body",
7931
+ "## Review Summary\n\n**Looks good!**\n\n- Clean code\n- Tests pass"
7932
+ ],
7933
+ timeout: 3e4
7934
+ },
7935
+ output: "status=0\n\nApproving pull request #123",
7936
+ comment: "Multiline body: --body flag and content must be SEPARATE array elements"
7563
7937
  }
7564
7938
  ],
7565
- execute: async ({ command, cwd, timeout }) => {
7939
+ execute: async ({ argv, cwd, timeout }) => {
7566
7940
  const workingDir = cwd ?? process.cwd();
7941
+ if (argv.length === 0) {
7942
+ return "status=1\n\nerror: argv array cannot be empty";
7943
+ }
7944
+ let timeoutId;
7567
7945
  try {
7568
- const proc = Bun.spawn(["sh", "-c", command], {
7946
+ const proc = Bun.spawn(argv, {
7569
7947
  cwd: workingDir,
7570
7948
  stdout: "pipe",
7571
7949
  stderr: "pipe"
7572
7950
  });
7573
7951
  const timeoutPromise = new Promise((_, reject) => {
7574
- setTimeout(() => {
7952
+ timeoutId = setTimeout(() => {
7575
7953
  proc.kill();
7576
7954
  reject(new Error(`Command timed out after ${timeout}ms`));
7577
7955
  }, timeout);
7578
7956
  });
7579
7957
  const exitCode = await Promise.race([proc.exited, timeoutPromise]);
7958
+ if (timeoutId) {
7959
+ clearTimeout(timeoutId);
7960
+ }
7580
7961
  const stdout = await new Response(proc.stdout).text();
7581
7962
  const stderr = await new Response(proc.stderr).text();
7582
7963
  const output = [stdout, stderr].filter(Boolean).join("\n").trim();
@@ -7584,6 +7965,9 @@ var runCommand = createGadget({
7584
7965
 
7585
7966
  ${output || "(no output)"}`;
7586
7967
  } catch (error) {
7968
+ if (timeoutId) {
7969
+ clearTimeout(timeoutId);
7970
+ }
7587
7971
  const message = error instanceof Error ? error.message : String(error);
7588
7972
  return `status=1
7589
7973
 
@@ -7756,6 +8140,30 @@ async function writeLogFile(dir, filename, content) {
7756
8140
  await (0, import_promises2.mkdir)(dir, { recursive: true });
7757
8141
  await (0, import_promises2.writeFile)((0, import_node_path7.join)(dir, filename), content, "utf-8");
7758
8142
  }
8143
+ function formatSessionTimestamp(date = /* @__PURE__ */ new Date()) {
8144
+ const pad = (n) => n.toString().padStart(2, "0");
8145
+ const year = date.getFullYear();
8146
+ const month = pad(date.getMonth() + 1);
8147
+ const day = pad(date.getDate());
8148
+ const hours = pad(date.getHours());
8149
+ const minutes = pad(date.getMinutes());
8150
+ const seconds = pad(date.getSeconds());
8151
+ return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
8152
+ }
8153
+ async function createSessionDir(baseDir) {
8154
+ const timestamp = formatSessionTimestamp();
8155
+ const sessionDir = (0, import_node_path7.join)(baseDir, timestamp);
8156
+ try {
8157
+ await (0, import_promises2.mkdir)(sessionDir, { recursive: true });
8158
+ return sessionDir;
8159
+ } catch (error) {
8160
+ console.warn(`[llmist] Failed to create log session directory: ${sessionDir}`, error);
8161
+ return void 0;
8162
+ }
8163
+ }
8164
+ function formatCallNumber(n) {
8165
+ return n.toString().padStart(4, "0");
8166
+ }
7759
8167
 
7760
8168
  // src/cli/utils.ts
7761
8169
  var import_chalk4 = __toESM(require("chalk"), 1);
@@ -7919,7 +8327,7 @@ function formatBytes(bytes) {
7919
8327
  }
7920
8328
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
7921
8329
  }
7922
- function formatGadgetSummary(result) {
8330
+ function formatGadgetSummary2(result) {
7923
8331
  const gadgetLabel = import_chalk3.default.magenta.bold(result.gadgetName);
7924
8332
  const timeLabel = import_chalk3.default.dim(`${Math.round(result.executionTimeMs)}ms`);
7925
8333
  const paramsStr = formatParametersInline(result.parameters);
@@ -8004,12 +8412,21 @@ function isInteractive(stream2) {
8004
8412
  }
8005
8413
  var ESC_KEY = 27;
8006
8414
  var ESC_TIMEOUT_MS = 50;
8007
- function createEscKeyListener(stdin, onEsc) {
8415
+ var CTRL_C = 3;
8416
+ function createEscKeyListener(stdin, onEsc, onCtrlC) {
8008
8417
  if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
8009
8418
  return null;
8010
8419
  }
8011
8420
  let escTimeout = null;
8012
8421
  const handleData = (data) => {
8422
+ if (data[0] === CTRL_C && onCtrlC) {
8423
+ if (escTimeout) {
8424
+ clearTimeout(escTimeout);
8425
+ escTimeout = null;
8426
+ }
8427
+ onCtrlC();
8428
+ return;
8429
+ }
8013
8430
  if (data[0] === ESC_KEY) {
8014
8431
  if (data.length === 1) {
8015
8432
  escTimeout = setTimeout(() => {
@@ -8457,7 +8874,7 @@ function addCompleteOptions(cmd, defaults) {
8457
8874
  OPTION_DESCRIPTIONS.maxTokens,
8458
8875
  createNumericParser({ label: "Max tokens", integer: true, min: 1 }),
8459
8876
  defaults?.["max-tokens"]
8460
- ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet).option(OPTION_FLAGS.logLlmRequests, OPTION_DESCRIPTIONS.logLlmRequests, defaults?.["log-llm-requests"]).option(OPTION_FLAGS.logLlmResponses, OPTION_DESCRIPTIONS.logLlmResponses, defaults?.["log-llm-responses"]);
8877
+ ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet).option(OPTION_FLAGS.logLlmRequests, OPTION_DESCRIPTIONS.logLlmRequests, defaults?.["log-llm-requests"]);
8461
8878
  }
8462
8879
  function addAgentOptions(cmd, defaults) {
8463
8880
  const gadgetAccumulator = (value, previous = []) => [
@@ -8481,7 +8898,7 @@ function addAgentOptions(cmd, defaults) {
8481
8898
  OPTION_FLAGS.noBuiltinInteraction,
8482
8899
  OPTION_DESCRIPTIONS.noBuiltinInteraction,
8483
8900
  defaults?.["builtin-interaction"] !== false
8484
- ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet).option(OPTION_FLAGS.logLlmRequests, OPTION_DESCRIPTIONS.logLlmRequests, defaults?.["log-llm-requests"]).option(OPTION_FLAGS.logLlmResponses, OPTION_DESCRIPTIONS.logLlmResponses, defaults?.["log-llm-responses"]).option(OPTION_FLAGS.docker, OPTION_DESCRIPTIONS.docker).option(OPTION_FLAGS.dockerRo, OPTION_DESCRIPTIONS.dockerRo).option(OPTION_FLAGS.noDocker, OPTION_DESCRIPTIONS.noDocker).option(OPTION_FLAGS.dockerDev, OPTION_DESCRIPTIONS.dockerDev);
8901
+ ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet).option(OPTION_FLAGS.logLlmRequests, OPTION_DESCRIPTIONS.logLlmRequests, defaults?.["log-llm-requests"]).option(OPTION_FLAGS.docker, OPTION_DESCRIPTIONS.docker).option(OPTION_FLAGS.dockerRo, OPTION_DESCRIPTIONS.dockerRo).option(OPTION_FLAGS.noDocker, OPTION_DESCRIPTIONS.noDocker).option(OPTION_FLAGS.dockerDev, OPTION_DESCRIPTIONS.dockerDev);
8485
8902
  }
8486
8903
  function configToCompleteOptions(config) {
8487
8904
  const result = {};
@@ -8491,7 +8908,6 @@ function configToCompleteOptions(config) {
8491
8908
  if (config["max-tokens"] !== void 0) result.maxTokens = config["max-tokens"];
8492
8909
  if (config.quiet !== void 0) result.quiet = config.quiet;
8493
8910
  if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
8494
- if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
8495
8911
  return result;
8496
8912
  }
8497
8913
  function configToAgentOptions(config) {
@@ -8515,7 +8931,6 @@ function configToAgentOptions(config) {
8515
8931
  result.gadgetApproval = config["gadget-approval"];
8516
8932
  if (config.quiet !== void 0) result.quiet = config.quiet;
8517
8933
  if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
8518
- if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
8519
8934
  if (config.docker !== void 0) result.docker = config.docker;
8520
8935
  if (config["docker-cwd-permission"] !== void 0)
8521
8936
  result.dockerCwdPermission = config["docker-cwd-permission"];
@@ -8533,7 +8948,8 @@ var DOCKER_CONFIG_KEYS = /* @__PURE__ */ new Set([
8533
8948
  "env-vars",
8534
8949
  "image-name",
8535
8950
  "dev-mode",
8536
- "dev-source"
8951
+ "dev-source",
8952
+ "docker-args"
8537
8953
  ]);
8538
8954
  var DEFAULT_IMAGE_NAME = "llmist-sandbox";
8539
8955
  var DEFAULT_CWD_PERMISSION = "rw";
@@ -8648,7 +9064,6 @@ var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
8648
9064
  "log-file",
8649
9065
  "log-reset",
8650
9066
  "log-llm-requests",
8651
- "log-llm-responses",
8652
9067
  "type",
8653
9068
  // Allowed for inheritance compatibility, ignored for built-in commands
8654
9069
  "docker",
@@ -8681,7 +9096,6 @@ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
8681
9096
  "log-file",
8682
9097
  "log-reset",
8683
9098
  "log-llm-requests",
8684
- "log-llm-responses",
8685
9099
  "type",
8686
9100
  // Allowed for inheritance compatibility, ignored for built-in commands
8687
9101
  "docker",
@@ -8869,13 +9283,6 @@ function validateCompleteConfig(raw, section) {
8869
9283
  section
8870
9284
  );
8871
9285
  }
8872
- if ("log-llm-responses" in rawObj) {
8873
- result["log-llm-responses"] = validateStringOrBoolean(
8874
- rawObj["log-llm-responses"],
8875
- "log-llm-responses",
8876
- section
8877
- );
8878
- }
8879
9286
  return result;
8880
9287
  }
8881
9288
  function validateAgentConfig(raw, section) {
@@ -8954,13 +9361,6 @@ function validateAgentConfig(raw, section) {
8954
9361
  section
8955
9362
  );
8956
9363
  }
8957
- if ("log-llm-responses" in rawObj) {
8958
- result["log-llm-responses"] = validateStringOrBoolean(
8959
- rawObj["log-llm-responses"],
8960
- "log-llm-responses",
8961
- section
8962
- );
8963
- }
8964
9364
  return result;
8965
9365
  }
8966
9366
  function validateStringOrBoolean(value, field, section) {
@@ -9402,6 +9802,9 @@ function validateDockerConfig(raw, section) {
9402
9802
  if ("dev-source" in rawObj) {
9403
9803
  result["dev-source"] = validateString2(rawObj["dev-source"], "dev-source", section);
9404
9804
  }
9805
+ if ("docker-args" in rawObj) {
9806
+ result["docker-args"] = validateStringArray2(rawObj["docker-args"], "docker-args", section);
9807
+ }
9405
9808
  return result;
9406
9809
  }
9407
9810
 
@@ -9413,6 +9816,8 @@ FROM oven/bun:1-debian
9413
9816
 
9414
9817
  # Install essential tools
9415
9818
  RUN apt-get update && apt-get install -y --no-install-recommends \\
9819
+ # ed for EditFile gadget (line-oriented editor)
9820
+ ed \\
9416
9821
  # ripgrep for fast file searching
9417
9822
  ripgrep \\
9418
9823
  # git for version control operations
@@ -9445,6 +9850,7 @@ FROM oven/bun:1-debian
9445
9850
 
9446
9851
  # Install essential tools (same as production)
9447
9852
  RUN apt-get update && apt-get install -y --no-install-recommends \\
9853
+ ed \\
9448
9854
  ripgrep \\
9449
9855
  git \\
9450
9856
  curl \\
@@ -9679,6 +10085,9 @@ function buildDockerRunArgs(ctx, imageName, devMode) {
9679
10085
  }
9680
10086
  }
9681
10087
  }
10088
+ if (ctx.config["docker-args"]) {
10089
+ args.push(...ctx.config["docker-args"]);
10090
+ }
9682
10091
  args.push(imageName);
9683
10092
  args.push(...ctx.forwardArgs);
9684
10093
  return args;
@@ -9845,6 +10254,8 @@ async function executeAgent(promptArg, options, env) {
9845
10254
  env.stderr.write(import_chalk5.default.yellow(`
9846
10255
  [Cancelled] ${progress.formatStats()}
9847
10256
  `));
10257
+ } else {
10258
+ handleQuit();
9848
10259
  }
9849
10260
  };
9850
10261
  const keyboard = {
@@ -9852,7 +10263,7 @@ async function executeAgent(promptArg, options, env) {
9852
10263
  cleanupSigint: null,
9853
10264
  restore: () => {
9854
10265
  if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
9855
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
10266
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel, handleCancel);
9856
10267
  }
9857
10268
  }
9858
10269
  };
@@ -9877,7 +10288,7 @@ async function executeAgent(promptArg, options, env) {
9877
10288
  process.exit(130);
9878
10289
  };
9879
10290
  if (stdinIsInteractive && stdinStream.isTTY) {
9880
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
10291
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel, handleCancel);
9881
10292
  }
9882
10293
  keyboard.cleanupSigint = createSigintListener(
9883
10294
  handleCancel,
@@ -9903,11 +10314,11 @@ async function executeAgent(promptArg, options, env) {
9903
10314
  gadgetApprovals,
9904
10315
  defaultMode: "allowed"
9905
10316
  };
9906
- const approvalManager = new ApprovalManager(approvalConfig, env, progress);
10317
+ const approvalManager = new ApprovalManager(approvalConfig, env, progress, keyboard);
9907
10318
  let usage;
9908
10319
  let iterations = 0;
9909
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
9910
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
10320
+ const llmLogsBaseDir = resolveLogDir(options.logLlmRequests, "requests");
10321
+ let llmSessionDir;
9911
10322
  let llmCallCounter = 0;
9912
10323
  const countMessagesTokens = async (model, messages) => {
9913
10324
  try {
@@ -9939,10 +10350,19 @@ async function executeAgent(promptArg, options, env) {
9939
10350
  );
9940
10351
  progress.startCall(context.options.model, inputTokens);
9941
10352
  progress.setInputTokens(inputTokens, false);
9942
- if (llmRequestsDir) {
9943
- const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
9944
- const content = formatLlmRequest(context.options.messages);
9945
- await writeLogFile(llmRequestsDir, filename, content);
10353
+ },
10354
+ // onLLMCallReady: Log the exact request being sent to the LLM
10355
+ // This fires AFTER controller modifications (e.g., trailing messages)
10356
+ onLLMCallReady: async (context) => {
10357
+ if (llmLogsBaseDir) {
10358
+ if (!llmSessionDir) {
10359
+ llmSessionDir = await createSessionDir(llmLogsBaseDir);
10360
+ }
10361
+ if (llmSessionDir) {
10362
+ const filename = `${formatCallNumber(llmCallCounter)}.request`;
10363
+ const content = formatLlmRequest(context.options.messages);
10364
+ await writeLogFile(llmSessionDir, filename, content);
10365
+ }
9946
10366
  }
9947
10367
  },
9948
10368
  // onStreamChunk: Real-time updates as LLM generates tokens
@@ -10007,9 +10427,9 @@ async function executeAgent(promptArg, options, env) {
10007
10427
  `);
10008
10428
  }
10009
10429
  }
10010
- if (llmResponsesDir) {
10011
- const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
10012
- await writeLogFile(llmResponsesDir, filename, context.rawResponse);
10430
+ if (llmSessionDir) {
10431
+ const filename = `${formatCallNumber(llmCallCounter)}.response`;
10432
+ await writeLogFile(llmSessionDir, filename, context.rawResponse);
10013
10433
  }
10014
10434
  }
10015
10435
  },
@@ -10106,6 +10526,13 @@ Denied: ${result.reason ?? "by user"}`
10106
10526
  parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
10107
10527
  resultMapping: (text) => `\u2139\uFE0F ${text}`
10108
10528
  });
10529
+ builder.withTrailingMessage(
10530
+ (ctx) => [
10531
+ `[Iteration ${ctx.iteration + 1}/${ctx.maxIterations}]`,
10532
+ "Think carefully: what gadget invocations can you make in parallel right now?",
10533
+ "Maximize efficiency by batching independent operations in a single response."
10534
+ ].join(" ")
10535
+ );
10109
10536
  const agent = builder.ask(prompt);
10110
10537
  let textBuffer = "";
10111
10538
  const flushTextBuffer = () => {
@@ -10131,7 +10558,7 @@ Denied: ${result.reason ?? "by user"}`
10131
10558
  }
10132
10559
  } else {
10133
10560
  const tokenCount = await countGadgetOutputTokens(event.result.result);
10134
- env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
10561
+ env.stderr.write(`${formatGadgetSummary2({ ...event.result, tokenCount })}
10135
10562
  `);
10136
10563
  }
10137
10564
  }
@@ -10143,7 +10570,10 @@ Denied: ${result.reason ?? "by user"}`
10143
10570
  } finally {
10144
10571
  isStreaming = false;
10145
10572
  keyboard.cleanupEsc?.();
10146
- keyboard.cleanupSigint?.();
10573
+ if (keyboard.cleanupSigint) {
10574
+ keyboard.cleanupSigint();
10575
+ process.once("SIGINT", () => process.exit(130));
10576
+ }
10147
10577
  }
10148
10578
  flushTextBuffer();
10149
10579
  progress.complete();
@@ -10191,13 +10621,15 @@ async function executeComplete(promptArg, options, env) {
10191
10621
  }
10192
10622
  builder.addUser(prompt);
10193
10623
  const messages = builder.build();
10194
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
10195
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
10196
- const timestamp = Date.now();
10197
- if (llmRequestsDir) {
10198
- const filename = `${timestamp}_complete.request.txt`;
10199
- const content = formatLlmRequest(messages);
10200
- await writeLogFile(llmRequestsDir, filename, content);
10624
+ const llmLogsBaseDir = resolveLogDir(options.logLlmRequests, "requests");
10625
+ let llmSessionDir;
10626
+ if (llmLogsBaseDir) {
10627
+ llmSessionDir = await createSessionDir(llmLogsBaseDir);
10628
+ if (llmSessionDir) {
10629
+ const filename = "0001.request";
10630
+ const content = formatLlmRequest(messages);
10631
+ await writeLogFile(llmSessionDir, filename, content);
10632
+ }
10201
10633
  }
10202
10634
  const stream2 = client.stream({
10203
10635
  model,
@@ -10236,9 +10668,9 @@ async function executeComplete(promptArg, options, env) {
10236
10668
  progress.endCall(usage);
10237
10669
  progress.complete();
10238
10670
  printer.ensureNewline();
10239
- if (llmResponsesDir) {
10240
- const filename = `${timestamp}_complete.response.txt`;
10241
- await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
10671
+ if (llmSessionDir) {
10672
+ const filename = "0001.response";
10673
+ await writeLogFile(llmSessionDir, filename, accumulatedResponse);
10242
10674
  }
10243
10675
  if (stderrTTY && !options.quiet) {
10244
10676
  const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });
@@ -10473,9 +10905,11 @@ ${issues}`);
10473
10905
  env.stderr.write(import_chalk7.default.dim("\nExecuting...\n"));
10474
10906
  const startTime = Date.now();
10475
10907
  let result;
10908
+ let cost;
10476
10909
  try {
10910
+ let rawResult;
10477
10911
  if (gadget.timeoutMs && gadget.timeoutMs > 0) {
10478
- result = await Promise.race([
10912
+ rawResult = await Promise.race([
10479
10913
  Promise.resolve(gadget.execute(params)),
10480
10914
  new Promise(
10481
10915
  (_, reject) => setTimeout(
@@ -10485,15 +10919,18 @@ ${issues}`);
10485
10919
  )
10486
10920
  ]);
10487
10921
  } else {
10488
- result = await Promise.resolve(gadget.execute(params));
10922
+ rawResult = await Promise.resolve(gadget.execute(params));
10489
10923
  }
10924
+ result = typeof rawResult === "string" ? rawResult : rawResult.result;
10925
+ cost = typeof rawResult === "object" ? rawResult.cost : void 0;
10490
10926
  } catch (error) {
10491
10927
  const message = error instanceof Error ? error.message : String(error);
10492
10928
  throw new Error(`Execution failed: ${message}`);
10493
10929
  }
10494
10930
  const elapsed = Date.now() - startTime;
10931
+ const costInfo = cost !== void 0 && cost > 0 ? ` (Cost: $${cost.toFixed(6)})` : "";
10495
10932
  env.stderr.write(import_chalk7.default.green(`
10496
- \u2713 Completed in ${elapsed}ms
10933
+ \u2713 Completed in ${elapsed}ms${costInfo}
10497
10934
 
10498
10935
  `));
10499
10936
  formatOutput(result, options, env.stdout);