llmist 2.0.0 → 2.2.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
@@ -1020,6 +1020,100 @@ var init_gadget = __esm({
1020
1020
  throw new AbortError();
1021
1021
  }
1022
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
+ }
1023
1117
  /**
1024
1118
  * Auto-generated instruction text for the LLM.
1025
1119
  * Combines name, description, and parameter schema into a formatted instruction.
@@ -3741,6 +3835,23 @@ var init_agent = __esm({
3741
3835
  maxIterations: this.maxIterations
3742
3836
  });
3743
3837
  while (currentIteration < this.maxIterations) {
3838
+ if (this.signal?.aborted) {
3839
+ this.logger.info("Agent loop terminated by abort signal", {
3840
+ iteration: currentIteration,
3841
+ reason: this.signal.reason
3842
+ });
3843
+ await this.safeObserve(async () => {
3844
+ if (this.hooks.observers?.onAbort) {
3845
+ const context = {
3846
+ iteration: currentIteration,
3847
+ reason: this.signal?.reason,
3848
+ logger: this.logger
3849
+ };
3850
+ await this.hooks.observers.onAbort(context);
3851
+ }
3852
+ });
3853
+ return;
3854
+ }
3744
3855
  this.logger.debug("Starting iteration", { iteration: currentIteration });
3745
3856
  try {
3746
3857
  if (this.compactionManager) {
@@ -3802,6 +3913,17 @@ var init_agent = __esm({
3802
3913
  llmOptions = { ...llmOptions, ...action.modifiedOptions };
3803
3914
  }
3804
3915
  }
3916
+ await this.safeObserve(async () => {
3917
+ if (this.hooks.observers?.onLLMCallReady) {
3918
+ const context = {
3919
+ iteration: currentIteration,
3920
+ maxIterations: this.maxIterations,
3921
+ options: llmOptions,
3922
+ logger: this.logger
3923
+ };
3924
+ await this.hooks.observers.onLLMCallReady(context);
3925
+ }
3926
+ });
3805
3927
  this.logger.info("Calling LLM", { model: this.model });
3806
3928
  this.logger.silly("LLM request details", {
3807
3929
  model: llmOptions.model,
@@ -6888,7 +7010,6 @@ var OPTION_FLAGS = {
6888
7010
  logFile: "--log-file <path>",
6889
7011
  logReset: "--log-reset",
6890
7012
  logLlmRequests: "--log-llm-requests [dir]",
6891
- logLlmResponses: "--log-llm-responses [dir]",
6892
7013
  noBuiltins: "--no-builtins",
6893
7014
  noBuiltinInteraction: "--no-builtin-interaction",
6894
7015
  quiet: "-q, --quiet",
@@ -6907,8 +7028,7 @@ var OPTION_DESCRIPTIONS = {
6907
7028
  logLevel: "Log level: silly, trace, debug, info, warn, error, fatal.",
6908
7029
  logFile: "Path to log file. When set, logs are written to file instead of stderr.",
6909
7030
  logReset: "Reset (truncate) the log file at session start instead of appending.",
6910
- logLlmRequests: "Save raw LLM requests as plain text. Optional dir, defaults to ~/.llmist/logs/requests/",
6911
- logLlmResponses: "Save raw LLM responses as plain text. Optional dir, defaults to ~/.llmist/logs/responses/",
7031
+ logLlmRequests: "Save LLM requests/responses to session directories. Optional dir, defaults to ~/.llmist/logs/requests/",
6912
7032
  noBuiltins: "Disable built-in gadgets (AskUser, TellUser).",
6913
7033
  noBuiltinInteraction: "Disable interactive gadgets (AskUser) while keeping TellUser.",
6914
7034
  quiet: "Suppress all output except content (text and TellUser messages).",
@@ -6925,7 +7045,7 @@ var import_commander2 = require("commander");
6925
7045
  // package.json
6926
7046
  var package_default = {
6927
7047
  name: "llmist",
6928
- version: "1.7.0",
7048
+ version: "2.1.0",
6929
7049
  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.",
6930
7050
  type: "module",
6931
7051
  main: "dist/index.cjs",
@@ -7098,6 +7218,14 @@ ${addedLines}`;
7098
7218
  }
7099
7219
 
7100
7220
  // src/cli/approval/context-providers.ts
7221
+ function formatGadgetSummary(gadgetName, params) {
7222
+ const paramEntries = Object.entries(params);
7223
+ if (paramEntries.length === 0) {
7224
+ return `${gadgetName}()`;
7225
+ }
7226
+ const paramStr = paramEntries.map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
7227
+ return `${gadgetName}(${paramStr})`;
7228
+ }
7101
7229
  var WriteFileContextProvider = class {
7102
7230
  gadgetName = "WriteFile";
7103
7231
  async getContext(params) {
@@ -7106,14 +7234,14 @@ var WriteFileContextProvider = class {
7106
7234
  const resolvedPath = (0, import_node_path2.resolve)(process.cwd(), filePath);
7107
7235
  if (!(0, import_node_fs2.existsSync)(resolvedPath)) {
7108
7236
  return {
7109
- summary: `Create new file: ${filePath}`,
7237
+ summary: formatGadgetSummary(this.gadgetName, params),
7110
7238
  details: formatNewFileDiff(filePath, newContent)
7111
7239
  };
7112
7240
  }
7113
7241
  const oldContent = (0, import_node_fs2.readFileSync)(resolvedPath, "utf-8");
7114
7242
  const diff = (0, import_diff.createPatch)(filePath, oldContent, newContent, "original", "modified");
7115
7243
  return {
7116
- summary: `Modify: ${filePath}`,
7244
+ summary: formatGadgetSummary(this.gadgetName, params),
7117
7245
  details: diff
7118
7246
  };
7119
7247
  }
@@ -7127,37 +7255,27 @@ var EditFileContextProvider = class {
7127
7255
  const newContent = String(params.content);
7128
7256
  if (!(0, import_node_fs2.existsSync)(resolvedPath)) {
7129
7257
  return {
7130
- summary: `Create new file: ${filePath}`,
7258
+ summary: formatGadgetSummary(this.gadgetName, params),
7131
7259
  details: formatNewFileDiff(filePath, newContent)
7132
7260
  };
7133
7261
  }
7134
7262
  const oldContent = (0, import_node_fs2.readFileSync)(resolvedPath, "utf-8");
7135
7263
  const diff = (0, import_diff.createPatch)(filePath, oldContent, newContent, "original", "modified");
7136
7264
  return {
7137
- summary: `Modify: ${filePath}`,
7265
+ summary: formatGadgetSummary(this.gadgetName, params),
7138
7266
  details: diff
7139
7267
  };
7140
7268
  }
7141
7269
  if ("commands" in params) {
7142
7270
  const commands = String(params.commands);
7143
7271
  return {
7144
- summary: `Edit: ${filePath}`,
7272
+ summary: formatGadgetSummary(this.gadgetName, params),
7145
7273
  details: `Commands:
7146
7274
  ${commands}`
7147
7275
  };
7148
7276
  }
7149
7277
  return {
7150
- summary: `Edit: ${filePath}`
7151
- };
7152
- }
7153
- };
7154
- var RunCommandContextProvider = class {
7155
- gadgetName = "RunCommand";
7156
- async getContext(params) {
7157
- const command = String(params.command ?? "");
7158
- const cwd = params.cwd ? ` (in ${params.cwd})` : "";
7159
- return {
7160
- summary: `Execute: ${command}${cwd}`
7278
+ summary: formatGadgetSummary(this.gadgetName, params)
7161
7279
  };
7162
7280
  }
7163
7281
  };
@@ -7166,27 +7284,15 @@ var DefaultContextProvider = class {
7166
7284
  this.gadgetName = gadgetName;
7167
7285
  }
7168
7286
  async getContext(params) {
7169
- const paramEntries = Object.entries(params);
7170
- if (paramEntries.length === 0) {
7171
- return {
7172
- summary: `${this.gadgetName}()`
7173
- };
7174
- }
7175
- const formatValue = (value) => {
7176
- const MAX_LEN = 50;
7177
- const str = JSON.stringify(value);
7178
- return str.length > MAX_LEN ? `${str.slice(0, MAX_LEN - 3)}...` : str;
7179
- };
7180
- const paramStr = paramEntries.map(([k, v]) => `${k}=${formatValue(v)}`).join(", ");
7181
7287
  return {
7182
- summary: `${this.gadgetName}(${paramStr})`
7288
+ summary: formatGadgetSummary(this.gadgetName, params)
7183
7289
  };
7184
7290
  }
7185
7291
  };
7186
7292
  var builtinContextProviders = [
7187
7293
  new WriteFileContextProvider(),
7188
- new EditFileContextProvider(),
7189
- new RunCommandContextProvider()
7294
+ new EditFileContextProvider()
7295
+ // Note: RunCommand uses DefaultContextProvider - no custom details needed
7190
7296
  ];
7191
7297
 
7192
7298
  // src/cli/approval/manager.ts
@@ -7197,11 +7303,13 @@ var ApprovalManager = class {
7197
7303
  * @param config - Approval configuration with per-gadget modes
7198
7304
  * @param env - CLI environment for I/O operations
7199
7305
  * @param progress - Optional progress indicator to pause during prompts
7306
+ * @param keyboard - Optional keyboard coordinator to disable ESC listener during prompts
7200
7307
  */
7201
- constructor(config, env, progress) {
7308
+ constructor(config, env, progress, keyboard) {
7202
7309
  this.config = config;
7203
7310
  this.env = env;
7204
7311
  this.progress = progress;
7312
+ this.keyboard = keyboard;
7205
7313
  for (const provider of builtinContextProviders) {
7206
7314
  this.registerProvider(provider);
7207
7315
  }
@@ -7270,26 +7378,34 @@ var ApprovalManager = class {
7270
7378
  const provider = this.providers.get(gadgetName.toLowerCase()) ?? new DefaultContextProvider(gadgetName);
7271
7379
  const context = await provider.getContext(params);
7272
7380
  this.progress?.pause();
7273
- this.env.stderr.write(`
7381
+ if (this.keyboard?.cleanupEsc) {
7382
+ this.keyboard.cleanupEsc();
7383
+ this.keyboard.cleanupEsc = null;
7384
+ }
7385
+ try {
7386
+ this.env.stderr.write(`
7274
7387
  ${import_chalk2.default.yellow("\u{1F512} Approval required:")} ${context.summary}
7275
7388
  `);
7276
- if (context.details) {
7277
- this.env.stderr.write(`
7389
+ if (context.details) {
7390
+ this.env.stderr.write(`
7278
7391
  ${renderColoredDiff(context.details)}
7279
7392
  `);
7280
- }
7281
- const response = await this.prompt(" \u23CE approve, or type to reject: ");
7282
- const isApproved = response === "" || response.toLowerCase() === "y";
7283
- if (isApproved) {
7284
- this.env.stderr.write(` ${import_chalk2.default.green("\u2713 Approved")}
7393
+ }
7394
+ const response = await this.prompt(" \u23CE approve, or type to reject: ");
7395
+ const isApproved = response === "" || response.toLowerCase() === "y";
7396
+ if (isApproved) {
7397
+ this.env.stderr.write(` ${import_chalk2.default.green("\u2713 Approved")}
7285
7398
 
7286
7399
  `);
7287
- return { approved: true };
7288
- }
7289
- this.env.stderr.write(` ${import_chalk2.default.red("\u2717 Denied")}
7400
+ return { approved: true };
7401
+ }
7402
+ this.env.stderr.write(` ${import_chalk2.default.red("\u2717 Denied")}
7290
7403
 
7291
7404
  `);
7292
- return { approved: false, reason: response || "Rejected by user" };
7405
+ return { approved: false, reason: response || "Rejected by user" };
7406
+ } finally {
7407
+ this.keyboard?.restore();
7408
+ }
7293
7409
  }
7294
7410
  /**
7295
7411
  * Prompts for user input.
@@ -7819,6 +7935,22 @@ var runCommand = createGadget({
7819
7935
  params: { argv: ["gh", "pr", "review", "123", "--comment", "--body", "Review with `backticks` and 'quotes'"], timeout: 3e4 },
7820
7936
  output: "status=0\n\n(no output)",
7821
7937
  comment: "Complex arguments with special characters - no escaping needed"
7938
+ },
7939
+ {
7940
+ params: {
7941
+ argv: [
7942
+ "gh",
7943
+ "pr",
7944
+ "review",
7945
+ "123",
7946
+ "--approve",
7947
+ "--body",
7948
+ "## Review Summary\n\n**Looks good!**\n\n- Clean code\n- Tests pass"
7949
+ ],
7950
+ timeout: 3e4
7951
+ },
7952
+ output: "status=0\n\nApproving pull request #123",
7953
+ comment: "Multiline body: --body flag and content must be SEPARATE array elements"
7822
7954
  }
7823
7955
  ],
7824
7956
  execute: async ({ argv, cwd, timeout }) => {
@@ -7826,6 +7958,7 @@ var runCommand = createGadget({
7826
7958
  if (argv.length === 0) {
7827
7959
  return "status=1\n\nerror: argv array cannot be empty";
7828
7960
  }
7961
+ let timeoutId;
7829
7962
  try {
7830
7963
  const proc = Bun.spawn(argv, {
7831
7964
  cwd: workingDir,
@@ -7833,12 +7966,15 @@ var runCommand = createGadget({
7833
7966
  stderr: "pipe"
7834
7967
  });
7835
7968
  const timeoutPromise = new Promise((_, reject) => {
7836
- setTimeout(() => {
7969
+ timeoutId = setTimeout(() => {
7837
7970
  proc.kill();
7838
7971
  reject(new Error(`Command timed out after ${timeout}ms`));
7839
7972
  }, timeout);
7840
7973
  });
7841
7974
  const exitCode = await Promise.race([proc.exited, timeoutPromise]);
7975
+ if (timeoutId) {
7976
+ clearTimeout(timeoutId);
7977
+ }
7842
7978
  const stdout = await new Response(proc.stdout).text();
7843
7979
  const stderr = await new Response(proc.stderr).text();
7844
7980
  const output = [stdout, stderr].filter(Boolean).join("\n").trim();
@@ -7846,6 +7982,9 @@ var runCommand = createGadget({
7846
7982
 
7847
7983
  ${output || "(no output)"}`;
7848
7984
  } catch (error) {
7985
+ if (timeoutId) {
7986
+ clearTimeout(timeoutId);
7987
+ }
7849
7988
  const message = error instanceof Error ? error.message : String(error);
7850
7989
  return `status=1
7851
7990
 
@@ -8018,6 +8157,30 @@ async function writeLogFile(dir, filename, content) {
8018
8157
  await (0, import_promises2.mkdir)(dir, { recursive: true });
8019
8158
  await (0, import_promises2.writeFile)((0, import_node_path7.join)(dir, filename), content, "utf-8");
8020
8159
  }
8160
+ function formatSessionTimestamp(date = /* @__PURE__ */ new Date()) {
8161
+ const pad = (n) => n.toString().padStart(2, "0");
8162
+ const year = date.getFullYear();
8163
+ const month = pad(date.getMonth() + 1);
8164
+ const day = pad(date.getDate());
8165
+ const hours = pad(date.getHours());
8166
+ const minutes = pad(date.getMinutes());
8167
+ const seconds = pad(date.getSeconds());
8168
+ return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
8169
+ }
8170
+ async function createSessionDir(baseDir) {
8171
+ const timestamp = formatSessionTimestamp();
8172
+ const sessionDir = (0, import_node_path7.join)(baseDir, timestamp);
8173
+ try {
8174
+ await (0, import_promises2.mkdir)(sessionDir, { recursive: true });
8175
+ return sessionDir;
8176
+ } catch (error) {
8177
+ console.warn(`[llmist] Failed to create log session directory: ${sessionDir}`, error);
8178
+ return void 0;
8179
+ }
8180
+ }
8181
+ function formatCallNumber(n) {
8182
+ return n.toString().padStart(4, "0");
8183
+ }
8021
8184
 
8022
8185
  // src/cli/utils.ts
8023
8186
  var import_chalk4 = __toESM(require("chalk"), 1);
@@ -8181,7 +8344,7 @@ function formatBytes(bytes) {
8181
8344
  }
8182
8345
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
8183
8346
  }
8184
- function formatGadgetSummary(result) {
8347
+ function formatGadgetSummary2(result) {
8185
8348
  const gadgetLabel = import_chalk3.default.magenta.bold(result.gadgetName);
8186
8349
  const timeLabel = import_chalk3.default.dim(`${Math.round(result.executionTimeMs)}ms`);
8187
8350
  const paramsStr = formatParametersInline(result.parameters);
@@ -8266,12 +8429,21 @@ function isInteractive(stream2) {
8266
8429
  }
8267
8430
  var ESC_KEY = 27;
8268
8431
  var ESC_TIMEOUT_MS = 50;
8269
- function createEscKeyListener(stdin, onEsc) {
8432
+ var CTRL_C = 3;
8433
+ function createEscKeyListener(stdin, onEsc, onCtrlC) {
8270
8434
  if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
8271
8435
  return null;
8272
8436
  }
8273
8437
  let escTimeout = null;
8274
8438
  const handleData = (data) => {
8439
+ if (data[0] === CTRL_C && onCtrlC) {
8440
+ if (escTimeout) {
8441
+ clearTimeout(escTimeout);
8442
+ escTimeout = null;
8443
+ }
8444
+ onCtrlC();
8445
+ return;
8446
+ }
8275
8447
  if (data[0] === ESC_KEY) {
8276
8448
  if (data.length === 1) {
8277
8449
  escTimeout = setTimeout(() => {
@@ -8719,7 +8891,7 @@ function addCompleteOptions(cmd, defaults) {
8719
8891
  OPTION_DESCRIPTIONS.maxTokens,
8720
8892
  createNumericParser({ label: "Max tokens", integer: true, min: 1 }),
8721
8893
  defaults?.["max-tokens"]
8722
- ).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"]);
8894
+ ).option(OPTION_FLAGS.quiet, OPTION_DESCRIPTIONS.quiet, defaults?.quiet).option(OPTION_FLAGS.logLlmRequests, OPTION_DESCRIPTIONS.logLlmRequests, defaults?.["log-llm-requests"]);
8723
8895
  }
8724
8896
  function addAgentOptions(cmd, defaults) {
8725
8897
  const gadgetAccumulator = (value, previous = []) => [
@@ -8743,7 +8915,7 @@ function addAgentOptions(cmd, defaults) {
8743
8915
  OPTION_FLAGS.noBuiltinInteraction,
8744
8916
  OPTION_DESCRIPTIONS.noBuiltinInteraction,
8745
8917
  defaults?.["builtin-interaction"] !== false
8746
- ).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);
8918
+ ).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);
8747
8919
  }
8748
8920
  function configToCompleteOptions(config) {
8749
8921
  const result = {};
@@ -8753,7 +8925,6 @@ function configToCompleteOptions(config) {
8753
8925
  if (config["max-tokens"] !== void 0) result.maxTokens = config["max-tokens"];
8754
8926
  if (config.quiet !== void 0) result.quiet = config.quiet;
8755
8927
  if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
8756
- if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
8757
8928
  return result;
8758
8929
  }
8759
8930
  function configToAgentOptions(config) {
@@ -8777,7 +8948,6 @@ function configToAgentOptions(config) {
8777
8948
  result.gadgetApproval = config["gadget-approval"];
8778
8949
  if (config.quiet !== void 0) result.quiet = config.quiet;
8779
8950
  if (config["log-llm-requests"] !== void 0) result.logLlmRequests = config["log-llm-requests"];
8780
- if (config["log-llm-responses"] !== void 0) result.logLlmResponses = config["log-llm-responses"];
8781
8951
  if (config.docker !== void 0) result.docker = config.docker;
8782
8952
  if (config["docker-cwd-permission"] !== void 0)
8783
8953
  result.dockerCwdPermission = config["docker-cwd-permission"];
@@ -8795,7 +8965,8 @@ var DOCKER_CONFIG_KEYS = /* @__PURE__ */ new Set([
8795
8965
  "env-vars",
8796
8966
  "image-name",
8797
8967
  "dev-mode",
8798
- "dev-source"
8968
+ "dev-source",
8969
+ "docker-args"
8799
8970
  ]);
8800
8971
  var DEFAULT_IMAGE_NAME = "llmist-sandbox";
8801
8972
  var DEFAULT_CWD_PERMISSION = "rw";
@@ -8910,7 +9081,6 @@ var COMPLETE_CONFIG_KEYS = /* @__PURE__ */ new Set([
8910
9081
  "log-file",
8911
9082
  "log-reset",
8912
9083
  "log-llm-requests",
8913
- "log-llm-responses",
8914
9084
  "type",
8915
9085
  // Allowed for inheritance compatibility, ignored for built-in commands
8916
9086
  "docker",
@@ -8943,7 +9113,6 @@ var AGENT_CONFIG_KEYS = /* @__PURE__ */ new Set([
8943
9113
  "log-file",
8944
9114
  "log-reset",
8945
9115
  "log-llm-requests",
8946
- "log-llm-responses",
8947
9116
  "type",
8948
9117
  // Allowed for inheritance compatibility, ignored for built-in commands
8949
9118
  "docker",
@@ -9131,13 +9300,6 @@ function validateCompleteConfig(raw, section) {
9131
9300
  section
9132
9301
  );
9133
9302
  }
9134
- if ("log-llm-responses" in rawObj) {
9135
- result["log-llm-responses"] = validateStringOrBoolean(
9136
- rawObj["log-llm-responses"],
9137
- "log-llm-responses",
9138
- section
9139
- );
9140
- }
9141
9303
  return result;
9142
9304
  }
9143
9305
  function validateAgentConfig(raw, section) {
@@ -9216,13 +9378,6 @@ function validateAgentConfig(raw, section) {
9216
9378
  section
9217
9379
  );
9218
9380
  }
9219
- if ("log-llm-responses" in rawObj) {
9220
- result["log-llm-responses"] = validateStringOrBoolean(
9221
- rawObj["log-llm-responses"],
9222
- "log-llm-responses",
9223
- section
9224
- );
9225
- }
9226
9381
  return result;
9227
9382
  }
9228
9383
  function validateStringOrBoolean(value, field, section) {
@@ -9664,6 +9819,9 @@ function validateDockerConfig(raw, section) {
9664
9819
  if ("dev-source" in rawObj) {
9665
9820
  result["dev-source"] = validateString2(rawObj["dev-source"], "dev-source", section);
9666
9821
  }
9822
+ if ("docker-args" in rawObj) {
9823
+ result["docker-args"] = validateStringArray2(rawObj["docker-args"], "docker-args", section);
9824
+ }
9667
9825
  return result;
9668
9826
  }
9669
9827
 
@@ -9675,6 +9833,8 @@ FROM oven/bun:1-debian
9675
9833
 
9676
9834
  # Install essential tools
9677
9835
  RUN apt-get update && apt-get install -y --no-install-recommends \\
9836
+ # ed for EditFile gadget (line-oriented editor)
9837
+ ed \\
9678
9838
  # ripgrep for fast file searching
9679
9839
  ripgrep \\
9680
9840
  # git for version control operations
@@ -9707,6 +9867,7 @@ FROM oven/bun:1-debian
9707
9867
 
9708
9868
  # Install essential tools (same as production)
9709
9869
  RUN apt-get update && apt-get install -y --no-install-recommends \\
9870
+ ed \\
9710
9871
  ripgrep \\
9711
9872
  git \\
9712
9873
  curl \\
@@ -9941,6 +10102,9 @@ function buildDockerRunArgs(ctx, imageName, devMode) {
9941
10102
  }
9942
10103
  }
9943
10104
  }
10105
+ if (ctx.config["docker-args"]) {
10106
+ args.push(...ctx.config["docker-args"]);
10107
+ }
9944
10108
  args.push(imageName);
9945
10109
  args.push(...ctx.forwardArgs);
9946
10110
  return args;
@@ -10107,6 +10271,8 @@ async function executeAgent(promptArg, options, env) {
10107
10271
  env.stderr.write(import_chalk5.default.yellow(`
10108
10272
  [Cancelled] ${progress.formatStats()}
10109
10273
  `));
10274
+ } else {
10275
+ handleQuit();
10110
10276
  }
10111
10277
  };
10112
10278
  const keyboard = {
@@ -10114,7 +10280,7 @@ async function executeAgent(promptArg, options, env) {
10114
10280
  cleanupSigint: null,
10115
10281
  restore: () => {
10116
10282
  if (stdinIsInteractive && stdinStream.isTTY && !wasCancelled) {
10117
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
10283
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel, handleCancel);
10118
10284
  }
10119
10285
  }
10120
10286
  };
@@ -10139,7 +10305,7 @@ async function executeAgent(promptArg, options, env) {
10139
10305
  process.exit(130);
10140
10306
  };
10141
10307
  if (stdinIsInteractive && stdinStream.isTTY) {
10142
- keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel);
10308
+ keyboard.cleanupEsc = createEscKeyListener(stdinStream, handleCancel, handleCancel);
10143
10309
  }
10144
10310
  keyboard.cleanupSigint = createSigintListener(
10145
10311
  handleCancel,
@@ -10165,11 +10331,11 @@ async function executeAgent(promptArg, options, env) {
10165
10331
  gadgetApprovals,
10166
10332
  defaultMode: "allowed"
10167
10333
  };
10168
- const approvalManager = new ApprovalManager(approvalConfig, env, progress);
10334
+ const approvalManager = new ApprovalManager(approvalConfig, env, progress, keyboard);
10169
10335
  let usage;
10170
10336
  let iterations = 0;
10171
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
10172
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
10337
+ const llmLogsBaseDir = resolveLogDir(options.logLlmRequests, "requests");
10338
+ let llmSessionDir;
10173
10339
  let llmCallCounter = 0;
10174
10340
  const countMessagesTokens = async (model, messages) => {
10175
10341
  try {
@@ -10201,10 +10367,19 @@ async function executeAgent(promptArg, options, env) {
10201
10367
  );
10202
10368
  progress.startCall(context.options.model, inputTokens);
10203
10369
  progress.setInputTokens(inputTokens, false);
10204
- if (llmRequestsDir) {
10205
- const filename = `${Date.now()}_call_${llmCallCounter}.request.txt`;
10206
- const content = formatLlmRequest(context.options.messages);
10207
- await writeLogFile(llmRequestsDir, filename, content);
10370
+ },
10371
+ // onLLMCallReady: Log the exact request being sent to the LLM
10372
+ // This fires AFTER controller modifications (e.g., trailing messages)
10373
+ onLLMCallReady: async (context) => {
10374
+ if (llmLogsBaseDir) {
10375
+ if (!llmSessionDir) {
10376
+ llmSessionDir = await createSessionDir(llmLogsBaseDir);
10377
+ }
10378
+ if (llmSessionDir) {
10379
+ const filename = `${formatCallNumber(llmCallCounter)}.request`;
10380
+ const content = formatLlmRequest(context.options.messages);
10381
+ await writeLogFile(llmSessionDir, filename, content);
10382
+ }
10208
10383
  }
10209
10384
  },
10210
10385
  // onStreamChunk: Real-time updates as LLM generates tokens
@@ -10269,9 +10444,9 @@ async function executeAgent(promptArg, options, env) {
10269
10444
  `);
10270
10445
  }
10271
10446
  }
10272
- if (llmResponsesDir) {
10273
- const filename = `${Date.now()}_call_${llmCallCounter}.response.txt`;
10274
- await writeLogFile(llmResponsesDir, filename, context.rawResponse);
10447
+ if (llmSessionDir) {
10448
+ const filename = `${formatCallNumber(llmCallCounter)}.response`;
10449
+ await writeLogFile(llmSessionDir, filename, context.rawResponse);
10275
10450
  }
10276
10451
  }
10277
10452
  },
@@ -10368,6 +10543,13 @@ Denied: ${result.reason ?? "by user"}`
10368
10543
  parameterMapping: (text) => ({ message: text, done: false, type: "info" }),
10369
10544
  resultMapping: (text) => `\u2139\uFE0F ${text}`
10370
10545
  });
10546
+ builder.withTrailingMessage(
10547
+ (ctx) => [
10548
+ `[Iteration ${ctx.iteration + 1}/${ctx.maxIterations}]`,
10549
+ "Think carefully: what gadget invocations can you make in parallel right now?",
10550
+ "Maximize efficiency by batching independent operations in a single response."
10551
+ ].join(" ")
10552
+ );
10371
10553
  const agent = builder.ask(prompt);
10372
10554
  let textBuffer = "";
10373
10555
  const flushTextBuffer = () => {
@@ -10393,7 +10575,7 @@ Denied: ${result.reason ?? "by user"}`
10393
10575
  }
10394
10576
  } else {
10395
10577
  const tokenCount = await countGadgetOutputTokens(event.result.result);
10396
- env.stderr.write(`${formatGadgetSummary({ ...event.result, tokenCount })}
10578
+ env.stderr.write(`${formatGadgetSummary2({ ...event.result, tokenCount })}
10397
10579
  `);
10398
10580
  }
10399
10581
  }
@@ -10405,7 +10587,10 @@ Denied: ${result.reason ?? "by user"}`
10405
10587
  } finally {
10406
10588
  isStreaming = false;
10407
10589
  keyboard.cleanupEsc?.();
10408
- keyboard.cleanupSigint?.();
10590
+ if (keyboard.cleanupSigint) {
10591
+ keyboard.cleanupSigint();
10592
+ process.once("SIGINT", () => process.exit(130));
10593
+ }
10409
10594
  }
10410
10595
  flushTextBuffer();
10411
10596
  progress.complete();
@@ -10453,13 +10638,15 @@ async function executeComplete(promptArg, options, env) {
10453
10638
  }
10454
10639
  builder.addUser(prompt);
10455
10640
  const messages = builder.build();
10456
- const llmRequestsDir = resolveLogDir(options.logLlmRequests, "requests");
10457
- const llmResponsesDir = resolveLogDir(options.logLlmResponses, "responses");
10458
- const timestamp = Date.now();
10459
- if (llmRequestsDir) {
10460
- const filename = `${timestamp}_complete.request.txt`;
10461
- const content = formatLlmRequest(messages);
10462
- await writeLogFile(llmRequestsDir, filename, content);
10641
+ const llmLogsBaseDir = resolveLogDir(options.logLlmRequests, "requests");
10642
+ let llmSessionDir;
10643
+ if (llmLogsBaseDir) {
10644
+ llmSessionDir = await createSessionDir(llmLogsBaseDir);
10645
+ if (llmSessionDir) {
10646
+ const filename = "0001.request";
10647
+ const content = formatLlmRequest(messages);
10648
+ await writeLogFile(llmSessionDir, filename, content);
10649
+ }
10463
10650
  }
10464
10651
  const stream2 = client.stream({
10465
10652
  model,
@@ -10498,9 +10685,9 @@ async function executeComplete(promptArg, options, env) {
10498
10685
  progress.endCall(usage);
10499
10686
  progress.complete();
10500
10687
  printer.ensureNewline();
10501
- if (llmResponsesDir) {
10502
- const filename = `${timestamp}_complete.response.txt`;
10503
- await writeLogFile(llmResponsesDir, filename, accumulatedResponse);
10688
+ if (llmSessionDir) {
10689
+ const filename = "0001.response";
10690
+ await writeLogFile(llmSessionDir, filename, accumulatedResponse);
10504
10691
  }
10505
10692
  if (stderrTTY && !options.quiet) {
10506
10693
  const summary = renderSummary({ finishReason, usage, cost: progress.getTotalCost() });