@mindstudio-ai/remy 0.1.35 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2059,7 +2059,7 @@ var init_screenshot = __esm({
2059
2059
  init_sidecar();
2060
2060
  init_runCli();
2061
2061
  init_logger();
2062
- SCREENSHOT_ANALYSIS_PROMPT = "Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be thorough and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).";
2062
+ SCREENSHOT_ANALYSIS_PROMPT = "Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components). Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.";
2063
2063
  }
2064
2064
  });
2065
2065
 
@@ -2097,6 +2097,91 @@ var init_screenshot2 = __esm({
2097
2097
  }
2098
2098
  });
2099
2099
 
2100
+ // src/statusWatcher.ts
2101
+ function startStatusWatcher(config) {
2102
+ const { apiConfig, getContext, onStatus, interval = 3e3, signal } = config;
2103
+ let lastLabel = "";
2104
+ let inflight = false;
2105
+ let stopped = false;
2106
+ const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
2107
+ async function tick() {
2108
+ if (stopped || signal?.aborted || inflight) {
2109
+ return;
2110
+ }
2111
+ inflight = true;
2112
+ try {
2113
+ const ctx = getContext();
2114
+ if (!ctx.assistantText && !ctx.lastToolName) {
2115
+ log.debug("Status watcher: no context, skipping");
2116
+ return;
2117
+ }
2118
+ log.debug("Status watcher: requesting label", {
2119
+ textLength: ctx.assistantText.length,
2120
+ lastToolName: ctx.lastToolName
2121
+ });
2122
+ const res = await fetch(url, {
2123
+ method: "POST",
2124
+ headers: {
2125
+ "Content-Type": "application/json",
2126
+ Authorization: `Bearer ${apiConfig.apiKey}`
2127
+ },
2128
+ body: JSON.stringify({
2129
+ assistantText: ctx.assistantText.slice(-500),
2130
+ lastToolName: ctx.lastToolName,
2131
+ lastToolResult: ctx.lastToolResult?.slice(-200),
2132
+ onboardingState: ctx.onboardingState,
2133
+ userMessage: ctx.userMessage?.slice(-200)
2134
+ }),
2135
+ signal
2136
+ });
2137
+ if (!res.ok) {
2138
+ log.debug("Status watcher: endpoint returned non-ok", {
2139
+ status: res.status
2140
+ });
2141
+ return;
2142
+ }
2143
+ const data = await res.json();
2144
+ if (!data.label) {
2145
+ log.debug("Status watcher: no label in response");
2146
+ return;
2147
+ }
2148
+ if (data.label === lastLabel) {
2149
+ log.debug("Status watcher: duplicate label, skipping", {
2150
+ label: data.label
2151
+ });
2152
+ return;
2153
+ }
2154
+ lastLabel = data.label;
2155
+ if (stopped) {
2156
+ return;
2157
+ }
2158
+ log.debug("Status watcher: emitting", { label: data.label });
2159
+ onStatus(data.label);
2160
+ } catch (err) {
2161
+ log.debug("Status watcher: error", { error: err?.message ?? "unknown" });
2162
+ } finally {
2163
+ inflight = false;
2164
+ }
2165
+ }
2166
+ const timer = setInterval(tick, interval);
2167
+ tick().catch(() => {
2168
+ });
2169
+ log.debug("Status watcher started", { interval });
2170
+ return {
2171
+ stop() {
2172
+ stopped = true;
2173
+ clearInterval(timer);
2174
+ log.debug("Status watcher stopped");
2175
+ }
2176
+ };
2177
+ }
2178
+ var init_statusWatcher = __esm({
2179
+ "src/statusWatcher.ts"() {
2180
+ "use strict";
2181
+ init_logger();
2182
+ }
2183
+ });
2184
+
2100
2185
  // src/subagents/common/cleanMessages.ts
2101
2186
  function cleanMessagesForApi(messages) {
2102
2187
  return messages.map((msg) => {
@@ -2145,19 +2230,47 @@ async function runSubAgent(config) {
2145
2230
  signal,
2146
2231
  parentToolId,
2147
2232
  onEvent,
2148
- resolveExternalTool
2233
+ resolveExternalTool,
2234
+ toolRegistry
2149
2235
  } = config;
2150
2236
  const emit2 = (e) => {
2151
2237
  onEvent({ ...e, parentToolId });
2152
2238
  };
2153
2239
  const messages = [{ role: "user", content: task }];
2240
+ function getPartialText(blocks) {
2241
+ return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2242
+ }
2243
+ function abortResult(blocks) {
2244
+ if (signal?.reason === "graceful") {
2245
+ const partial = getPartialText(blocks);
2246
+ return {
2247
+ text: partial ? `[INTERRUPTED]
2248
+
2249
+ ${partial}` : "[INTERRUPTED] Sub-agent was interrupted before producing output.",
2250
+ messages
2251
+ };
2252
+ }
2253
+ return { text: "Error: cancelled", messages };
2254
+ }
2255
+ let lastToolResult = "";
2154
2256
  while (true) {
2155
2257
  if (signal?.aborted) {
2156
- return { text: "Error: cancelled", messages };
2258
+ return abortResult([]);
2157
2259
  }
2158
2260
  const contentBlocks = [];
2159
2261
  let thinkingStartedAt = 0;
2160
2262
  let stopReason = "end_turn";
2263
+ let currentToolNames = "";
2264
+ const statusWatcher = startStatusWatcher({
2265
+ apiConfig,
2266
+ getContext: () => ({
2267
+ assistantText: getPartialText(contentBlocks),
2268
+ lastToolName: currentToolNames || void 0,
2269
+ lastToolResult: lastToolResult || void 0
2270
+ }),
2271
+ onStatus: (label) => emit2({ type: "status", message: label }),
2272
+ signal
2273
+ });
2161
2274
  const fullSystem = `${system}
2162
2275
 
2163
2276
  Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC")}`;
@@ -2233,7 +2346,8 @@ Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " "
2233
2346
  }
2234
2347
  }
2235
2348
  if (signal?.aborted) {
2236
- return { text: "Error: cancelled", messages };
2349
+ statusWatcher.stop();
2350
+ return abortResult(contentBlocks);
2237
2351
  }
2238
2352
  messages.push({
2239
2353
  role: "assistant",
@@ -2243,6 +2357,7 @@ Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " "
2243
2357
  (b) => b.type === "tool"
2244
2358
  );
2245
2359
  if (stopReason !== "tool_use" || toolCalls.length === 0) {
2360
+ statusWatcher.stop();
2246
2361
  const text = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2247
2362
  return { text, messages };
2248
2363
  }
@@ -2251,46 +2366,82 @@ Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " "
2251
2366
  count: toolCalls.length,
2252
2367
  tools: toolCalls.map((tc) => tc.name)
2253
2368
  });
2369
+ currentToolNames = toolCalls.map((tc) => tc.name).join(", ");
2254
2370
  const results = await Promise.all(
2255
2371
  toolCalls.map(async (tc) => {
2256
2372
  if (signal?.aborted) {
2257
2373
  return { id: tc.id, result: "Error: cancelled", isError: true };
2258
2374
  }
2259
- try {
2260
- let result;
2261
- if (externalTools.has(tc.name) && resolveExternalTool) {
2262
- result = await resolveExternalTool(tc.id, tc.name, tc.input);
2263
- } else {
2264
- const onLog = (line) => emit2({
2265
- type: "tool_input_delta",
2266
- id: tc.id,
2267
- name: tc.name,
2268
- result: line
2375
+ let settle;
2376
+ const resultPromise = new Promise((res) => {
2377
+ settle = (result, isError) => res({ id: tc.id, result, isError });
2378
+ });
2379
+ let toolAbort = new AbortController();
2380
+ const cascadeAbort = () => toolAbort.abort();
2381
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
2382
+ let settled = false;
2383
+ const safeSettle = (result, isError) => {
2384
+ if (settled) {
2385
+ return;
2386
+ }
2387
+ settled = true;
2388
+ signal?.removeEventListener("abort", cascadeAbort);
2389
+ settle(result, isError);
2390
+ };
2391
+ const run = async (input) => {
2392
+ try {
2393
+ let result;
2394
+ if (externalTools.has(tc.name) && resolveExternalTool) {
2395
+ result = await resolveExternalTool(tc.id, tc.name, input);
2396
+ } else {
2397
+ const onLog = (line) => emit2({
2398
+ type: "tool_input_delta",
2399
+ id: tc.id,
2400
+ name: tc.name,
2401
+ result: line
2402
+ });
2403
+ result = await executeTool2(tc.name, input, tc.id, onLog);
2404
+ }
2405
+ safeSettle(result, result.startsWith("Error"));
2406
+ } catch (err) {
2407
+ safeSettle(`Error: ${err.message}`, true);
2408
+ }
2409
+ };
2410
+ const entry = {
2411
+ id: tc.id,
2412
+ name: tc.name,
2413
+ input: tc.input,
2414
+ parentToolId,
2415
+ abortController: toolAbort,
2416
+ startedAt: Date.now(),
2417
+ settle: safeSettle,
2418
+ rerun: (newInput) => {
2419
+ settled = false;
2420
+ toolAbort = new AbortController();
2421
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
2422
+ once: true
2269
2423
  });
2270
- result = await executeTool2(tc.name, tc.input, tc.id, onLog);
2424
+ entry.abortController = toolAbort;
2425
+ entry.input = newInput;
2426
+ run(newInput);
2271
2427
  }
2272
- const isError = result.startsWith("Error");
2273
- emit2({
2274
- type: "tool_done",
2275
- id: tc.id,
2276
- name: tc.name,
2277
- result,
2278
- isError
2279
- });
2280
- return { id: tc.id, result, isError };
2281
- } catch (err) {
2282
- const errorMsg = `Error: ${err.message}`;
2283
- emit2({
2284
- type: "tool_done",
2285
- id: tc.id,
2286
- name: tc.name,
2287
- result: errorMsg,
2288
- isError: true
2289
- });
2290
- return { id: tc.id, result: errorMsg, isError: true };
2291
- }
2428
+ };
2429
+ toolRegistry?.register(entry);
2430
+ run(tc.input);
2431
+ const r = await resultPromise;
2432
+ toolRegistry?.unregister(tc.id);
2433
+ emit2({
2434
+ type: "tool_done",
2435
+ id: tc.id,
2436
+ name: tc.name,
2437
+ result: r.result,
2438
+ isError: r.isError
2439
+ });
2440
+ return r;
2292
2441
  })
2293
2442
  );
2443
+ statusWatcher.stop();
2444
+ lastToolResult = results.at(-1)?.result ?? "";
2294
2445
  for (const r of results) {
2295
2446
  const block = contentBlocks.find(
2296
2447
  (b) => b.type === "tool" && b.id === r.id
@@ -2298,6 +2449,7 @@ Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " "
2298
2449
  if (block?.type === "tool") {
2299
2450
  block.result = r.result;
2300
2451
  block.isError = r.isError;
2452
+ block.completedAt = Date.now();
2301
2453
  }
2302
2454
  messages.push({
2303
2455
  role: "user",
@@ -2313,6 +2465,7 @@ var init_runner = __esm({
2313
2465
  "use strict";
2314
2466
  init_api();
2315
2467
  init_logger();
2468
+ init_statusWatcher();
2316
2469
  init_cleanMessages();
2317
2470
  }
2318
2471
  });
@@ -2604,7 +2757,8 @@ var init_browserAutomation = __esm({
2604
2757
  }
2605
2758
  }
2606
2759
  return result2;
2607
- }
2760
+ },
2761
+ toolRegistry: context.toolRegistry
2608
2762
  });
2609
2763
  context.subAgentMessages?.set(context.toolCallId, result.messages);
2610
2764
  return result.text;
@@ -2738,7 +2892,7 @@ Brief description of the types used on the page. If you can identify the actual
2738
2892
  ## Techniques
2739
2893
  Identify the specific design moves that make this page interesting and unique, described in terms of how a designer with a technical background would write them down as notes in their notebook for inspiration. Focus only on the non-obvious, hard-to-think-of techniques \u2014 the things that make this page gallery-worthy. Skip basics like "high contrast CTA" or "generous whitespace" that any competent designer already knows.
2740
2894
 
2741
- Respond only with the analysis and absolutely no other text.
2895
+ Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.
2742
2896
  `;
2743
2897
  definition3 = {
2744
2898
  name: "analyzeDesign",
@@ -2781,7 +2935,7 @@ var init_analyzeImage = __esm({
2781
2935
  "src/subagents/designExpert/tools/analyzeImage.ts"() {
2782
2936
  "use strict";
2783
2937
  init_runCli();
2784
- DEFAULT_PROMPT = "Describe everything visible in this image \u2014 every element, its position, its size relative to the frame, its colors, its content. Be thorough and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).";
2938
+ DEFAULT_PROMPT = "Describe everything visible in this image \u2014 every element, its position, its size relative to the frame, its colors, its content. Be comprhensive, thorough and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components). Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.";
2785
2939
  definition4 = {
2786
2940
  name: "analyzeImage",
2787
2941
  description: "Analyze an image by URL. Returns a detailed description of everything visible. Provide a custom prompt to ask a specific question instead of the default full description.",
@@ -2880,7 +3034,7 @@ async function seedreamGenerate(opts) {
2880
3034
  );
2881
3035
  try {
2882
3036
  const parsed = JSON.parse(batchResult);
2883
- imageUrls = parsed.results.map(
3037
+ imageUrls = parsed.map(
2884
3038
  (r) => r.output?.imageUrl ?? `Error: ${r.error}`
2885
3039
  );
2886
3040
  } catch {
@@ -2920,7 +3074,7 @@ var init_seedream = __esm({
2920
3074
  "src/subagents/designExpert/tools/_seedream.ts"() {
2921
3075
  "use strict";
2922
3076
  init_runCli();
2923
- ANALYZE_PROMPT = "You are reviewing this image for a visual designer sourcing assets for a project. Describe: what the image depicts, the mood and color palette, how the lighting and composition work, whether there are any issues (unwanted text, artifacts, distortions), and how it could be used in a layout (hero background, feature section, card texture, etc). Be concise and practical.";
3077
+ ANALYZE_PROMPT = "You are reviewing this image for a visual designer sourcing assets for a project. Describe: what the image depicts, the mood and color palette, how the lighting and composition work, whether there are any issues (unwanted text, artifacts, distortions), and how it could be used in a layout (hero background, feature section, card texture, etc). Be concise and practical. Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.";
2924
3078
  }
2925
3079
  });
2926
3080
 
@@ -3345,7 +3499,8 @@ Visual design expert. Describe the situation and what you need \u2014 the agent
3345
3499
  signal: context.signal,
3346
3500
  parentToolId: context.toolCallId,
3347
3501
  onEvent: context.onEvent,
3348
- resolveExternalTool: context.resolveExternalTool
3502
+ resolveExternalTool: context.resolveExternalTool,
3503
+ toolRegistry: context.toolRegistry
3349
3504
  });
3350
3505
  context.subAgentMessages?.set(context.toolCallId, result.messages);
3351
3506
  return result.text;
@@ -3660,7 +3815,8 @@ var init_productVision = __esm({
3660
3815
  signal: context.signal,
3661
3816
  parentToolId: context.toolCallId,
3662
3817
  onEvent: context.onEvent,
3663
- resolveExternalTool: context.resolveExternalTool
3818
+ resolveExternalTool: context.resolveExternalTool,
3819
+ toolRegistry: context.toolRegistry
3664
3820
  });
3665
3821
  context.subAgentMessages?.set(context.toolCallId, result.messages);
3666
3822
  return result.text;
@@ -3814,7 +3970,8 @@ var init_codeSanityCheck = __esm({
3814
3970
  signal: context.signal,
3815
3971
  parentToolId: context.toolCallId,
3816
3972
  onEvent: context.onEvent,
3817
- resolveExternalTool: context.resolveExternalTool
3973
+ resolveExternalTool: context.resolveExternalTool,
3974
+ toolRegistry: context.toolRegistry
3818
3975
  });
3819
3976
  context.subAgentMessages?.set(context.toolCallId, result.messages);
3820
3977
  return result.text;
@@ -4186,91 +4343,6 @@ var init_parsePartialJson = __esm({
4186
4343
  }
4187
4344
  });
4188
4345
 
4189
- // src/statusWatcher.ts
4190
- function startStatusWatcher(config) {
4191
- const { apiConfig, getContext, onStatus, interval = 3e3, signal } = config;
4192
- let lastLabel = "";
4193
- let inflight = false;
4194
- let stopped = false;
4195
- const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
4196
- async function tick() {
4197
- if (stopped || signal?.aborted || inflight) {
4198
- return;
4199
- }
4200
- inflight = true;
4201
- try {
4202
- const ctx = getContext();
4203
- if (!ctx.assistantText && !ctx.lastToolName) {
4204
- log.debug("Status watcher: no context, skipping");
4205
- return;
4206
- }
4207
- log.debug("Status watcher: requesting label", {
4208
- textLength: ctx.assistantText.length,
4209
- lastToolName: ctx.lastToolName
4210
- });
4211
- const res = await fetch(url, {
4212
- method: "POST",
4213
- headers: {
4214
- "Content-Type": "application/json",
4215
- Authorization: `Bearer ${apiConfig.apiKey}`
4216
- },
4217
- body: JSON.stringify({
4218
- assistantText: ctx.assistantText.slice(-500),
4219
- lastToolName: ctx.lastToolName,
4220
- lastToolResult: ctx.lastToolResult?.slice(-200),
4221
- onboardingState: ctx.onboardingState,
4222
- userMessage: ctx.userMessage?.slice(-200)
4223
- }),
4224
- signal
4225
- });
4226
- if (!res.ok) {
4227
- log.debug("Status watcher: endpoint returned non-ok", {
4228
- status: res.status
4229
- });
4230
- return;
4231
- }
4232
- const data = await res.json();
4233
- if (!data.label) {
4234
- log.debug("Status watcher: no label in response");
4235
- return;
4236
- }
4237
- if (data.label === lastLabel) {
4238
- log.debug("Status watcher: duplicate label, skipping", {
4239
- label: data.label
4240
- });
4241
- return;
4242
- }
4243
- lastLabel = data.label;
4244
- if (stopped) {
4245
- return;
4246
- }
4247
- log.debug("Status watcher: emitting", { label: data.label });
4248
- onStatus(data.label);
4249
- } catch (err) {
4250
- log.debug("Status watcher: error", { error: err?.message ?? "unknown" });
4251
- } finally {
4252
- inflight = false;
4253
- }
4254
- }
4255
- const timer = setInterval(tick, interval);
4256
- tick().catch(() => {
4257
- });
4258
- log.debug("Status watcher started", { interval });
4259
- return {
4260
- stop() {
4261
- stopped = true;
4262
- clearInterval(timer);
4263
- log.debug("Status watcher stopped");
4264
- }
4265
- };
4266
- }
4267
- var init_statusWatcher = __esm({
4268
- "src/statusWatcher.ts"() {
4269
- "use strict";
4270
- init_logger();
4271
- }
4272
- });
4273
-
4274
4346
  // src/errors.ts
4275
4347
  function friendlyError(raw) {
4276
4348
  for (const [pattern, message] of patterns) {
@@ -4327,7 +4399,8 @@ async function runTurn(params) {
4327
4399
  signal,
4328
4400
  onEvent,
4329
4401
  resolveExternalTool,
4330
- hidden
4402
+ hidden,
4403
+ toolRegistry
4331
4404
  } = params;
4332
4405
  const tools2 = getToolDefinitions(onboardingState);
4333
4406
  log.info("Turn started", {
@@ -4380,6 +4453,20 @@ async function runTurn(params) {
4380
4453
  let thinkingStartedAt = 0;
4381
4454
  const toolInputAccumulators = /* @__PURE__ */ new Map();
4382
4455
  let stopReason = "end_turn";
4456
+ let subAgentText = "";
4457
+ let currentToolNames = "";
4458
+ const statusWatcher = startStatusWatcher({
4459
+ apiConfig,
4460
+ getContext: () => ({
4461
+ assistantText: subAgentText || getTextContent(contentBlocks).slice(-500),
4462
+ lastToolName: currentToolNames || getToolCalls(contentBlocks).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools || void 0,
4463
+ lastToolResult: lastCompletedResult || void 0,
4464
+ onboardingState,
4465
+ userMessage
4466
+ }),
4467
+ onStatus: (label) => onEvent({ type: "status", message: label }),
4468
+ signal
4469
+ });
4383
4470
  async function handlePartialInput(acc, id, name, partial) {
4384
4471
  const tool = getToolByName(name);
4385
4472
  if (!tool?.streaming) {
@@ -4443,18 +4530,6 @@ async function runTurn(params) {
4443
4530
  onEvent({ type: "tool_input_delta", id, name, result: content });
4444
4531
  }
4445
4532
  }
4446
- const statusWatcher = startStatusWatcher({
4447
- apiConfig,
4448
- getContext: () => ({
4449
- assistantText: getTextContent(contentBlocks).slice(-500),
4450
- lastToolName: getToolCalls(contentBlocks).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools || void 0,
4451
- lastToolResult: lastCompletedResult || void 0,
4452
- onboardingState,
4453
- userMessage
4454
- }),
4455
- onStatus: (label) => onEvent({ type: "status", message: label }),
4456
- signal
4457
- });
4458
4533
  try {
4459
4534
  for await (const event of streamChatWithRetry(
4460
4535
  {
@@ -4575,10 +4650,9 @@ async function runTurn(params) {
4575
4650
  } else {
4576
4651
  throw err;
4577
4652
  }
4578
- } finally {
4579
- statusWatcher.stop();
4580
4653
  }
4581
4654
  if (signal?.aborted) {
4655
+ statusWatcher.stop();
4582
4656
  if (contentBlocks.length > 0) {
4583
4657
  contentBlocks.push({
4584
4658
  type: "text",
@@ -4600,6 +4674,7 @@ async function runTurn(params) {
4600
4674
  });
4601
4675
  const toolCalls = getToolCalls(contentBlocks);
4602
4676
  if (stopReason !== "tool_use" || toolCalls.length === 0) {
4677
+ statusWatcher.stop();
4603
4678
  saveSession(state);
4604
4679
  onEvent({ type: "turn_done" });
4605
4680
  return;
@@ -4608,8 +4683,7 @@ async function runTurn(params) {
4608
4683
  count: toolCalls.length,
4609
4684
  tools: toolCalls.map((tc) => tc.name)
4610
4685
  });
4611
- let subAgentText = "";
4612
- const origOnEvent = onEvent;
4686
+ currentToolNames = toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ");
4613
4687
  const wrappedOnEvent = (e) => {
4614
4688
  if ("parentToolId" in e && e.parentToolId) {
4615
4689
  if (e.type === "text") {
@@ -4618,86 +4692,103 @@ async function runTurn(params) {
4618
4692
  subAgentText = `Using ${e.name}`;
4619
4693
  }
4620
4694
  }
4621
- origOnEvent(e);
4695
+ onEvent(e);
4622
4696
  };
4623
- const toolStatusWatcher = startStatusWatcher({
4624
- apiConfig,
4625
- getContext: () => ({
4626
- assistantText: subAgentText || getTextContent(contentBlocks).slice(-500),
4627
- lastToolName: toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ") || void 0,
4628
- lastToolResult: lastCompletedResult || void 0,
4629
- onboardingState,
4630
- userMessage
4631
- }),
4632
- onStatus: (label) => origOnEvent({ type: "status", message: label }),
4633
- signal
4634
- });
4635
4697
  const subAgentMessages = /* @__PURE__ */ new Map();
4636
4698
  const results = await Promise.all(
4637
4699
  toolCalls.map(async (tc) => {
4638
4700
  if (signal?.aborted) {
4639
- return {
4640
- id: tc.id,
4641
- result: "Error: cancelled",
4642
- isError: true
4643
- };
4701
+ return { id: tc.id, result: "Error: cancelled", isError: true };
4644
4702
  }
4645
4703
  const toolStart = Date.now();
4646
- try {
4647
- let result;
4648
- if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
4649
- saveSession(state);
4650
- log.info("Waiting for external tool result", {
4651
- name: tc.name,
4652
- id: tc.id
4653
- });
4654
- result = await resolveExternalTool(tc.id, tc.name, tc.input);
4655
- } else {
4656
- result = await executeTool(tc.name, tc.input, {
4657
- apiConfig,
4658
- model,
4659
- signal,
4660
- onEvent: wrappedOnEvent,
4661
- resolveExternalTool,
4662
- toolCallId: tc.id,
4663
- subAgentMessages,
4664
- onLog: (line) => wrappedOnEvent({
4665
- type: "tool_input_delta",
4666
- id: tc.id,
4704
+ let settle;
4705
+ const resultPromise = new Promise((res) => {
4706
+ settle = (result, isError) => res({ id: tc.id, result, isError });
4707
+ });
4708
+ let toolAbort = new AbortController();
4709
+ const cascadeAbort = () => toolAbort.abort();
4710
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
4711
+ let settled = false;
4712
+ const safeSettle = (result, isError) => {
4713
+ if (settled) {
4714
+ return;
4715
+ }
4716
+ settled = true;
4717
+ signal?.removeEventListener("abort", cascadeAbort);
4718
+ settle(result, isError);
4719
+ };
4720
+ const run = async (input) => {
4721
+ try {
4722
+ let result;
4723
+ if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
4724
+ saveSession(state);
4725
+ log.info("Waiting for external tool result", {
4667
4726
  name: tc.name,
4668
- result: line
4669
- })
4727
+ id: tc.id
4728
+ });
4729
+ result = await resolveExternalTool(tc.id, tc.name, input);
4730
+ } else {
4731
+ result = await executeTool(tc.name, input, {
4732
+ apiConfig,
4733
+ model,
4734
+ signal: toolAbort.signal,
4735
+ onEvent: wrappedOnEvent,
4736
+ resolveExternalTool,
4737
+ toolCallId: tc.id,
4738
+ subAgentMessages,
4739
+ toolRegistry,
4740
+ onLog: (line) => wrappedOnEvent({
4741
+ type: "tool_input_delta",
4742
+ id: tc.id,
4743
+ name: tc.name,
4744
+ result: line
4745
+ })
4746
+ });
4747
+ }
4748
+ safeSettle(result, result.startsWith("Error"));
4749
+ } catch (err) {
4750
+ safeSettle(`Error: ${err.message}`, true);
4751
+ }
4752
+ };
4753
+ const entry = {
4754
+ id: tc.id,
4755
+ name: tc.name,
4756
+ input: tc.input,
4757
+ abortController: toolAbort,
4758
+ startedAt: toolStart,
4759
+ settle: safeSettle,
4760
+ rerun: (newInput) => {
4761
+ settled = false;
4762
+ toolAbort = new AbortController();
4763
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
4764
+ once: true
4670
4765
  });
4766
+ entry.abortController = toolAbort;
4767
+ entry.input = newInput;
4768
+ run(newInput);
4671
4769
  }
4672
- const isError = result.startsWith("Error");
4673
- log.info("Tool completed", {
4674
- name: tc.name,
4675
- elapsed: `${Date.now() - toolStart}ms`,
4676
- isError,
4677
- resultLength: result.length
4678
- });
4679
- onEvent({
4680
- type: "tool_done",
4681
- id: tc.id,
4682
- name: tc.name,
4683
- result,
4684
- isError
4685
- });
4686
- return { id: tc.id, result, isError };
4687
- } catch (err) {
4688
- const errorMsg = `Error: ${err.message}`;
4689
- onEvent({
4690
- type: "tool_done",
4691
- id: tc.id,
4692
- name: tc.name,
4693
- result: errorMsg,
4694
- isError: true
4695
- });
4696
- return { id: tc.id, result: errorMsg, isError: true };
4697
- }
4770
+ };
4771
+ toolRegistry?.register(entry);
4772
+ run(tc.input);
4773
+ const r = await resultPromise;
4774
+ toolRegistry?.unregister(tc.id);
4775
+ log.info("Tool completed", {
4776
+ name: tc.name,
4777
+ elapsed: `${Date.now() - toolStart}ms`,
4778
+ isError: r.isError,
4779
+ resultLength: r.result.length
4780
+ });
4781
+ onEvent({
4782
+ type: "tool_done",
4783
+ id: tc.id,
4784
+ name: tc.name,
4785
+ result: r.result,
4786
+ isError: r.isError
4787
+ });
4788
+ return r;
4698
4789
  })
4699
4790
  );
4700
- toolStatusWatcher.stop();
4791
+ statusWatcher.stop();
4701
4792
  for (const r of results) {
4702
4793
  const block = contentBlocks.find(
4703
4794
  (b) => b.type === "tool" && b.id === r.id
@@ -4705,6 +4796,7 @@ async function runTurn(params) {
4705
4796
  if (block?.type === "tool") {
4706
4797
  block.result = r.result;
4707
4798
  block.isError = r.isError;
4799
+ block.completedAt = Date.now();
4708
4800
  const msgs = subAgentMessages.get(r.id);
4709
4801
  if (msgs) {
4710
4802
  block.subAgentMessages = msgs;
@@ -5063,6 +5155,84 @@ var init_config = __esm({
5063
5155
  }
5064
5156
  });
5065
5157
 
5158
+ // src/toolRegistry.ts
5159
+ var ToolRegistry;
5160
+ var init_toolRegistry = __esm({
5161
+ "src/toolRegistry.ts"() {
5162
+ "use strict";
5163
+ ToolRegistry = class {
5164
+ entries = /* @__PURE__ */ new Map();
5165
+ onEvent;
5166
+ register(entry) {
5167
+ this.entries.set(entry.id, entry);
5168
+ }
5169
+ unregister(id) {
5170
+ this.entries.delete(id);
5171
+ }
5172
+ get(id) {
5173
+ return this.entries.get(id);
5174
+ }
5175
+ /**
5176
+ * Stop a running tool.
5177
+ *
5178
+ * - graceful: abort and settle with [INTERRUPTED] + partial result
5179
+ * - hard: abort and settle with a generic error
5180
+ *
5181
+ * Returns true if the tool was found and stopped.
5182
+ */
5183
+ stop(id, mode) {
5184
+ const entry = this.entries.get(id);
5185
+ if (!entry) {
5186
+ return false;
5187
+ }
5188
+ entry.abortController.abort(mode);
5189
+ if (mode === "graceful") {
5190
+ const partial = entry.getPartialResult?.() ?? "";
5191
+ const result = partial ? `[INTERRUPTED]
5192
+
5193
+ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
5194
+ entry.settle(result, false);
5195
+ } else {
5196
+ entry.settle("Error: tool was cancelled", true);
5197
+ }
5198
+ this.onEvent?.({
5199
+ type: "tool_stopped",
5200
+ id: entry.id,
5201
+ name: entry.name,
5202
+ mode,
5203
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
5204
+ });
5205
+ this.entries.delete(id);
5206
+ return true;
5207
+ }
5208
+ /**
5209
+ * Restart a running tool with the same or patched input.
5210
+ * The original controllable promise stays pending and settles
5211
+ * when the new execution finishes.
5212
+ *
5213
+ * Returns true if the tool was found and restarted.
5214
+ */
5215
+ restart(id, patchedInput) {
5216
+ const entry = this.entries.get(id);
5217
+ if (!entry) {
5218
+ return false;
5219
+ }
5220
+ entry.abortController.abort("restart");
5221
+ const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
5222
+ this.onEvent?.({
5223
+ type: "tool_restarted",
5224
+ id: entry.id,
5225
+ name: entry.name,
5226
+ input: newInput,
5227
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
5228
+ });
5229
+ entry.rerun(newInput);
5230
+ return true;
5231
+ }
5232
+ };
5233
+ }
5234
+ });
5235
+
5066
5236
  // src/headless.ts
5067
5237
  var headless_exports = {};
5068
5238
  __export(headless_exports, {
@@ -5134,6 +5304,7 @@ async function startHeadless(opts = {}) {
5134
5304
  const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
5135
5305
  const pendingTools = /* @__PURE__ */ new Map();
5136
5306
  const earlyResults = /* @__PURE__ */ new Map();
5307
+ const toolRegistry = new ToolRegistry();
5137
5308
  const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
5138
5309
  "promptUser",
5139
5310
  "confirmDestructiveAction",
@@ -5238,14 +5409,46 @@ async function startHeadless(opts = {}) {
5238
5409
  rid
5239
5410
  );
5240
5411
  return;
5412
+ case "tool_stopped":
5413
+ emit(
5414
+ "tool_stopped",
5415
+ {
5416
+ id: e.id,
5417
+ name: e.name,
5418
+ mode: e.mode,
5419
+ ...e.parentToolId && { parentToolId: e.parentToolId }
5420
+ },
5421
+ rid
5422
+ );
5423
+ return;
5424
+ case "tool_restarted":
5425
+ emit(
5426
+ "tool_restarted",
5427
+ {
5428
+ id: e.id,
5429
+ name: e.name,
5430
+ input: e.input,
5431
+ ...e.parentToolId && { parentToolId: e.parentToolId }
5432
+ },
5433
+ rid
5434
+ );
5435
+ return;
5241
5436
  case "status":
5242
- emit("status", { message: e.message }, rid);
5437
+ emit(
5438
+ "status",
5439
+ {
5440
+ message: e.message,
5441
+ ...e.parentToolId && { parentToolId: e.parentToolId }
5442
+ },
5443
+ rid
5444
+ );
5243
5445
  return;
5244
5446
  case "error":
5245
5447
  emit("error", { error: e.error }, rid);
5246
5448
  return;
5247
5449
  }
5248
5450
  }
5451
+ toolRegistry.onEvent = onEvent;
5249
5452
  async function handleMessage(parsed, requestId) {
5250
5453
  if (running) {
5251
5454
  emit(
@@ -5297,7 +5500,8 @@ async function startHeadless(opts = {}) {
5297
5500
  signal: currentAbort.signal,
5298
5501
  onEvent,
5299
5502
  resolveExternalTool,
5300
- hidden: isCommand
5503
+ hidden: isCommand,
5504
+ toolRegistry
5301
5505
  });
5302
5506
  if (!completedEmitted) {
5303
5507
  emit(
@@ -5351,6 +5555,36 @@ async function startHeadless(opts = {}) {
5351
5555
  emit("completed", { success: true }, requestId);
5352
5556
  return;
5353
5557
  }
5558
+ if (action === "stop_tool") {
5559
+ const id = parsed.id;
5560
+ const mode = parsed.mode ?? "hard";
5561
+ const found = toolRegistry.stop(id, mode);
5562
+ if (found) {
5563
+ emit("completed", { success: true }, requestId);
5564
+ } else {
5565
+ emit(
5566
+ "completed",
5567
+ { success: false, error: "Tool not found" },
5568
+ requestId
5569
+ );
5570
+ }
5571
+ return;
5572
+ }
5573
+ if (action === "restart_tool") {
5574
+ const id = parsed.id;
5575
+ const patchedInput = parsed.input;
5576
+ const found = toolRegistry.restart(id, patchedInput);
5577
+ if (found) {
5578
+ emit("completed", { success: true }, requestId);
5579
+ } else {
5580
+ emit(
5581
+ "completed",
5582
+ { success: false, error: "Tool not found" },
5583
+ requestId
5584
+ );
5585
+ }
5586
+ return;
5587
+ }
5354
5588
  if (action === "message") {
5355
5589
  await handleMessage(parsed, requestId);
5356
5590
  return;
@@ -5385,6 +5619,7 @@ var init_headless = __esm({
5385
5619
  init_lsp();
5386
5620
  init_agent();
5387
5621
  init_session();
5622
+ init_toolRegistry();
5388
5623
  }
5389
5624
  });
5390
5625