@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/headless.js CHANGED
@@ -2109,7 +2109,7 @@ var runMethodTool = {
2109
2109
  };
2110
2110
 
2111
2111
  // src/tools/_helpers/screenshot.ts
2112
- var 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).";
2112
+ var 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.";
2113
2113
  async function captureAndAnalyzeScreenshot(promptOrOptions) {
2114
2114
  let prompt;
2115
2115
  let onLog;
@@ -2167,6 +2167,85 @@ var screenshotTool = {
2167
2167
  }
2168
2168
  };
2169
2169
 
2170
+ // src/statusWatcher.ts
2171
+ function startStatusWatcher(config) {
2172
+ const { apiConfig, getContext, onStatus, interval = 3e3, signal } = config;
2173
+ let lastLabel = "";
2174
+ let inflight = false;
2175
+ let stopped = false;
2176
+ const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
2177
+ async function tick() {
2178
+ if (stopped || signal?.aborted || inflight) {
2179
+ return;
2180
+ }
2181
+ inflight = true;
2182
+ try {
2183
+ const ctx = getContext();
2184
+ if (!ctx.assistantText && !ctx.lastToolName) {
2185
+ log.debug("Status watcher: no context, skipping");
2186
+ return;
2187
+ }
2188
+ log.debug("Status watcher: requesting label", {
2189
+ textLength: ctx.assistantText.length,
2190
+ lastToolName: ctx.lastToolName
2191
+ });
2192
+ const res = await fetch(url, {
2193
+ method: "POST",
2194
+ headers: {
2195
+ "Content-Type": "application/json",
2196
+ Authorization: `Bearer ${apiConfig.apiKey}`
2197
+ },
2198
+ body: JSON.stringify({
2199
+ assistantText: ctx.assistantText.slice(-500),
2200
+ lastToolName: ctx.lastToolName,
2201
+ lastToolResult: ctx.lastToolResult?.slice(-200),
2202
+ onboardingState: ctx.onboardingState,
2203
+ userMessage: ctx.userMessage?.slice(-200)
2204
+ }),
2205
+ signal
2206
+ });
2207
+ if (!res.ok) {
2208
+ log.debug("Status watcher: endpoint returned non-ok", {
2209
+ status: res.status
2210
+ });
2211
+ return;
2212
+ }
2213
+ const data = await res.json();
2214
+ if (!data.label) {
2215
+ log.debug("Status watcher: no label in response");
2216
+ return;
2217
+ }
2218
+ if (data.label === lastLabel) {
2219
+ log.debug("Status watcher: duplicate label, skipping", {
2220
+ label: data.label
2221
+ });
2222
+ return;
2223
+ }
2224
+ lastLabel = data.label;
2225
+ if (stopped) {
2226
+ return;
2227
+ }
2228
+ log.debug("Status watcher: emitting", { label: data.label });
2229
+ onStatus(data.label);
2230
+ } catch (err) {
2231
+ log.debug("Status watcher: error", { error: err?.message ?? "unknown" });
2232
+ } finally {
2233
+ inflight = false;
2234
+ }
2235
+ }
2236
+ const timer = setInterval(tick, interval);
2237
+ tick().catch(() => {
2238
+ });
2239
+ log.debug("Status watcher started", { interval });
2240
+ return {
2241
+ stop() {
2242
+ stopped = true;
2243
+ clearInterval(timer);
2244
+ log.debug("Status watcher stopped");
2245
+ }
2246
+ };
2247
+ }
2248
+
2170
2249
  // src/subagents/common/cleanMessages.ts
2171
2250
  function cleanMessagesForApi(messages) {
2172
2251
  return messages.map((msg) => {
@@ -2210,19 +2289,47 @@ async function runSubAgent(config) {
2210
2289
  signal,
2211
2290
  parentToolId,
2212
2291
  onEvent,
2213
- resolveExternalTool
2292
+ resolveExternalTool,
2293
+ toolRegistry
2214
2294
  } = config;
2215
2295
  const emit2 = (e) => {
2216
2296
  onEvent({ ...e, parentToolId });
2217
2297
  };
2218
2298
  const messages = [{ role: "user", content: task }];
2299
+ function getPartialText(blocks) {
2300
+ return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2301
+ }
2302
+ function abortResult(blocks) {
2303
+ if (signal?.reason === "graceful") {
2304
+ const partial = getPartialText(blocks);
2305
+ return {
2306
+ text: partial ? `[INTERRUPTED]
2307
+
2308
+ ${partial}` : "[INTERRUPTED] Sub-agent was interrupted before producing output.",
2309
+ messages
2310
+ };
2311
+ }
2312
+ return { text: "Error: cancelled", messages };
2313
+ }
2314
+ let lastToolResult = "";
2219
2315
  while (true) {
2220
2316
  if (signal?.aborted) {
2221
- return { text: "Error: cancelled", messages };
2317
+ return abortResult([]);
2222
2318
  }
2223
2319
  const contentBlocks = [];
2224
2320
  let thinkingStartedAt = 0;
2225
2321
  let stopReason = "end_turn";
2322
+ let currentToolNames = "";
2323
+ const statusWatcher = startStatusWatcher({
2324
+ apiConfig,
2325
+ getContext: () => ({
2326
+ assistantText: getPartialText(contentBlocks),
2327
+ lastToolName: currentToolNames || void 0,
2328
+ lastToolResult: lastToolResult || void 0
2329
+ }),
2330
+ onStatus: (label) => emit2({ type: "status", message: label }),
2331
+ signal
2332
+ });
2226
2333
  const fullSystem = `${system}
2227
2334
 
2228
2335
  Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC")}`;
@@ -2298,7 +2405,8 @@ Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " "
2298
2405
  }
2299
2406
  }
2300
2407
  if (signal?.aborted) {
2301
- return { text: "Error: cancelled", messages };
2408
+ statusWatcher.stop();
2409
+ return abortResult(contentBlocks);
2302
2410
  }
2303
2411
  messages.push({
2304
2412
  role: "assistant",
@@ -2308,6 +2416,7 @@ Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " "
2308
2416
  (b) => b.type === "tool"
2309
2417
  );
2310
2418
  if (stopReason !== "tool_use" || toolCalls.length === 0) {
2419
+ statusWatcher.stop();
2311
2420
  const text = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2312
2421
  return { text, messages };
2313
2422
  }
@@ -2316,46 +2425,82 @@ Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " "
2316
2425
  count: toolCalls.length,
2317
2426
  tools: toolCalls.map((tc) => tc.name)
2318
2427
  });
2428
+ currentToolNames = toolCalls.map((tc) => tc.name).join(", ");
2319
2429
  const results = await Promise.all(
2320
2430
  toolCalls.map(async (tc) => {
2321
2431
  if (signal?.aborted) {
2322
2432
  return { id: tc.id, result: "Error: cancelled", isError: true };
2323
2433
  }
2324
- try {
2325
- let result;
2326
- if (externalTools.has(tc.name) && resolveExternalTool) {
2327
- result = await resolveExternalTool(tc.id, tc.name, tc.input);
2328
- } else {
2329
- const onLog = (line) => emit2({
2330
- type: "tool_input_delta",
2331
- id: tc.id,
2332
- name: tc.name,
2333
- result: line
2434
+ let settle;
2435
+ const resultPromise = new Promise((res) => {
2436
+ settle = (result, isError) => res({ id: tc.id, result, isError });
2437
+ });
2438
+ let toolAbort = new AbortController();
2439
+ const cascadeAbort = () => toolAbort.abort();
2440
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
2441
+ let settled = false;
2442
+ const safeSettle = (result, isError) => {
2443
+ if (settled) {
2444
+ return;
2445
+ }
2446
+ settled = true;
2447
+ signal?.removeEventListener("abort", cascadeAbort);
2448
+ settle(result, isError);
2449
+ };
2450
+ const run = async (input) => {
2451
+ try {
2452
+ let result;
2453
+ if (externalTools.has(tc.name) && resolveExternalTool) {
2454
+ result = await resolveExternalTool(tc.id, tc.name, input);
2455
+ } else {
2456
+ const onLog = (line) => emit2({
2457
+ type: "tool_input_delta",
2458
+ id: tc.id,
2459
+ name: tc.name,
2460
+ result: line
2461
+ });
2462
+ result = await executeTool2(tc.name, input, tc.id, onLog);
2463
+ }
2464
+ safeSettle(result, result.startsWith("Error"));
2465
+ } catch (err) {
2466
+ safeSettle(`Error: ${err.message}`, true);
2467
+ }
2468
+ };
2469
+ const entry = {
2470
+ id: tc.id,
2471
+ name: tc.name,
2472
+ input: tc.input,
2473
+ parentToolId,
2474
+ abortController: toolAbort,
2475
+ startedAt: Date.now(),
2476
+ settle: safeSettle,
2477
+ rerun: (newInput) => {
2478
+ settled = false;
2479
+ toolAbort = new AbortController();
2480
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
2481
+ once: true
2334
2482
  });
2335
- result = await executeTool2(tc.name, tc.input, tc.id, onLog);
2483
+ entry.abortController = toolAbort;
2484
+ entry.input = newInput;
2485
+ run(newInput);
2336
2486
  }
2337
- const isError = result.startsWith("Error");
2338
- emit2({
2339
- type: "tool_done",
2340
- id: tc.id,
2341
- name: tc.name,
2342
- result,
2343
- isError
2344
- });
2345
- return { id: tc.id, result, isError };
2346
- } catch (err) {
2347
- const errorMsg = `Error: ${err.message}`;
2348
- emit2({
2349
- type: "tool_done",
2350
- id: tc.id,
2351
- name: tc.name,
2352
- result: errorMsg,
2353
- isError: true
2354
- });
2355
- return { id: tc.id, result: errorMsg, isError: true };
2356
- }
2487
+ };
2488
+ toolRegistry?.register(entry);
2489
+ run(tc.input);
2490
+ const r = await resultPromise;
2491
+ toolRegistry?.unregister(tc.id);
2492
+ emit2({
2493
+ type: "tool_done",
2494
+ id: tc.id,
2495
+ name: tc.name,
2496
+ result: r.result,
2497
+ isError: r.isError
2498
+ });
2499
+ return r;
2357
2500
  })
2358
2501
  );
2502
+ statusWatcher.stop();
2503
+ lastToolResult = results.at(-1)?.result ?? "";
2359
2504
  for (const r of results) {
2360
2505
  const block = contentBlocks.find(
2361
2506
  (b) => b.type === "tool" && b.id === r.id
@@ -2363,6 +2508,7 @@ Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " "
2363
2508
  if (block?.type === "tool") {
2364
2509
  block.result = r.result;
2365
2510
  block.isError = r.isError;
2511
+ block.completedAt = Date.now();
2366
2512
  }
2367
2513
  messages.push({
2368
2514
  role: "user",
@@ -2594,7 +2740,8 @@ var browserAutomationTool = {
2594
2740
  }
2595
2741
  }
2596
2742
  return result2;
2597
- }
2743
+ },
2744
+ toolRegistry: context.toolRegistry
2598
2745
  });
2599
2746
  context.subAgentMessages?.set(context.toolCallId, result.messages);
2600
2747
  return result.text;
@@ -2684,7 +2831,7 @@ Brief description of the types used on the page. If you can identify the actual
2684
2831
  ## Techniques
2685
2832
  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.
2686
2833
 
2687
- Respond only with the analysis and absolutely no other text.
2834
+ Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.
2688
2835
  `;
2689
2836
  var definition3 = {
2690
2837
  name: "analyzeDesign",
@@ -2734,7 +2881,7 @@ __export(analyzeImage_exports, {
2734
2881
  definition: () => definition4,
2735
2882
  execute: () => execute4
2736
2883
  });
2737
- var 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).";
2884
+ var 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.";
2738
2885
  var definition4 = {
2739
2886
  name: "analyzeImage",
2740
2887
  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.",
@@ -2801,7 +2948,7 @@ __export(generateImages_exports, {
2801
2948
  });
2802
2949
 
2803
2950
  // src/subagents/designExpert/tools/_seedream.ts
2804
- var 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.";
2951
+ var 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.";
2805
2952
  async function seedreamGenerate(opts) {
2806
2953
  const { prompts, sourceImages, transparentBackground, onLog } = opts;
2807
2954
  const width = opts.width || 2048;
@@ -2841,7 +2988,7 @@ async function seedreamGenerate(opts) {
2841
2988
  );
2842
2989
  try {
2843
2990
  const parsed = JSON.parse(batchResult);
2844
- imageUrls = parsed.results.map(
2991
+ imageUrls = parsed.map(
2845
2992
  (r) => r.output?.imageUrl ?? `Error: ${r.error}`
2846
2993
  );
2847
2994
  } catch {
@@ -3232,7 +3379,8 @@ var designExpertTool = {
3232
3379
  signal: context.signal,
3233
3380
  parentToolId: context.toolCallId,
3234
3381
  onEvent: context.onEvent,
3235
- resolveExternalTool: context.resolveExternalTool
3382
+ resolveExternalTool: context.resolveExternalTool,
3383
+ toolRegistry: context.toolRegistry
3236
3384
  });
3237
3385
  context.subAgentMessages?.set(context.toolCallId, result.messages);
3238
3386
  return result.text;
@@ -3514,7 +3662,8 @@ var productVisionTool = {
3514
3662
  signal: context.signal,
3515
3663
  parentToolId: context.toolCallId,
3516
3664
  onEvent: context.onEvent,
3517
- resolveExternalTool: context.resolveExternalTool
3665
+ resolveExternalTool: context.resolveExternalTool,
3666
+ toolRegistry: context.toolRegistry
3518
3667
  });
3519
3668
  context.subAgentMessages?.set(context.toolCallId, result.messages);
3520
3669
  return result.text;
@@ -3651,7 +3800,8 @@ var codeSanityCheckTool = {
3651
3800
  signal: context.signal,
3652
3801
  parentToolId: context.toolCallId,
3653
3802
  onEvent: context.onEvent,
3654
- resolveExternalTool: context.resolveExternalTool
3803
+ resolveExternalTool: context.resolveExternalTool,
3804
+ toolRegistry: context.toolRegistry
3655
3805
  });
3656
3806
  context.subAgentMessages?.set(context.toolCallId, result.messages);
3657
3807
  return result.text;
@@ -3971,85 +4121,6 @@ function parsePartialJson(jsonString) {
3971
4121
  return parseAny();
3972
4122
  }
3973
4123
 
3974
- // src/statusWatcher.ts
3975
- function startStatusWatcher(config) {
3976
- const { apiConfig, getContext, onStatus, interval = 3e3, signal } = config;
3977
- let lastLabel = "";
3978
- let inflight = false;
3979
- let stopped = false;
3980
- const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
3981
- async function tick() {
3982
- if (stopped || signal?.aborted || inflight) {
3983
- return;
3984
- }
3985
- inflight = true;
3986
- try {
3987
- const ctx = getContext();
3988
- if (!ctx.assistantText && !ctx.lastToolName) {
3989
- log.debug("Status watcher: no context, skipping");
3990
- return;
3991
- }
3992
- log.debug("Status watcher: requesting label", {
3993
- textLength: ctx.assistantText.length,
3994
- lastToolName: ctx.lastToolName
3995
- });
3996
- const res = await fetch(url, {
3997
- method: "POST",
3998
- headers: {
3999
- "Content-Type": "application/json",
4000
- Authorization: `Bearer ${apiConfig.apiKey}`
4001
- },
4002
- body: JSON.stringify({
4003
- assistantText: ctx.assistantText.slice(-500),
4004
- lastToolName: ctx.lastToolName,
4005
- lastToolResult: ctx.lastToolResult?.slice(-200),
4006
- onboardingState: ctx.onboardingState,
4007
- userMessage: ctx.userMessage?.slice(-200)
4008
- }),
4009
- signal
4010
- });
4011
- if (!res.ok) {
4012
- log.debug("Status watcher: endpoint returned non-ok", {
4013
- status: res.status
4014
- });
4015
- return;
4016
- }
4017
- const data = await res.json();
4018
- if (!data.label) {
4019
- log.debug("Status watcher: no label in response");
4020
- return;
4021
- }
4022
- if (data.label === lastLabel) {
4023
- log.debug("Status watcher: duplicate label, skipping", {
4024
- label: data.label
4025
- });
4026
- return;
4027
- }
4028
- lastLabel = data.label;
4029
- if (stopped) {
4030
- return;
4031
- }
4032
- log.debug("Status watcher: emitting", { label: data.label });
4033
- onStatus(data.label);
4034
- } catch (err) {
4035
- log.debug("Status watcher: error", { error: err?.message ?? "unknown" });
4036
- } finally {
4037
- inflight = false;
4038
- }
4039
- }
4040
- const timer = setInterval(tick, interval);
4041
- tick().catch(() => {
4042
- });
4043
- log.debug("Status watcher started", { interval });
4044
- return {
4045
- stop() {
4046
- stopped = true;
4047
- clearInterval(timer);
4048
- log.debug("Status watcher stopped");
4049
- }
4050
- };
4051
- }
4052
-
4053
4124
  // src/errors.ts
4054
4125
  var patterns = [
4055
4126
  [
@@ -4113,7 +4184,8 @@ async function runTurn(params) {
4113
4184
  signal,
4114
4185
  onEvent,
4115
4186
  resolveExternalTool,
4116
- hidden
4187
+ hidden,
4188
+ toolRegistry
4117
4189
  } = params;
4118
4190
  const tools2 = getToolDefinitions(onboardingState);
4119
4191
  log.info("Turn started", {
@@ -4166,6 +4238,20 @@ async function runTurn(params) {
4166
4238
  let thinkingStartedAt = 0;
4167
4239
  const toolInputAccumulators = /* @__PURE__ */ new Map();
4168
4240
  let stopReason = "end_turn";
4241
+ let subAgentText = "";
4242
+ let currentToolNames = "";
4243
+ const statusWatcher = startStatusWatcher({
4244
+ apiConfig,
4245
+ getContext: () => ({
4246
+ assistantText: subAgentText || getTextContent(contentBlocks).slice(-500),
4247
+ lastToolName: currentToolNames || getToolCalls(contentBlocks).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools || void 0,
4248
+ lastToolResult: lastCompletedResult || void 0,
4249
+ onboardingState,
4250
+ userMessage
4251
+ }),
4252
+ onStatus: (label) => onEvent({ type: "status", message: label }),
4253
+ signal
4254
+ });
4169
4255
  async function handlePartialInput(acc, id, name, partial) {
4170
4256
  const tool = getToolByName(name);
4171
4257
  if (!tool?.streaming) {
@@ -4229,18 +4315,6 @@ async function runTurn(params) {
4229
4315
  onEvent({ type: "tool_input_delta", id, name, result: content });
4230
4316
  }
4231
4317
  }
4232
- const statusWatcher = startStatusWatcher({
4233
- apiConfig,
4234
- getContext: () => ({
4235
- assistantText: getTextContent(contentBlocks).slice(-500),
4236
- lastToolName: getToolCalls(contentBlocks).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools || void 0,
4237
- lastToolResult: lastCompletedResult || void 0,
4238
- onboardingState,
4239
- userMessage
4240
- }),
4241
- onStatus: (label) => onEvent({ type: "status", message: label }),
4242
- signal
4243
- });
4244
4318
  try {
4245
4319
  for await (const event of streamChatWithRetry(
4246
4320
  {
@@ -4361,10 +4435,9 @@ async function runTurn(params) {
4361
4435
  } else {
4362
4436
  throw err;
4363
4437
  }
4364
- } finally {
4365
- statusWatcher.stop();
4366
4438
  }
4367
4439
  if (signal?.aborted) {
4440
+ statusWatcher.stop();
4368
4441
  if (contentBlocks.length > 0) {
4369
4442
  contentBlocks.push({
4370
4443
  type: "text",
@@ -4386,6 +4459,7 @@ async function runTurn(params) {
4386
4459
  });
4387
4460
  const toolCalls = getToolCalls(contentBlocks);
4388
4461
  if (stopReason !== "tool_use" || toolCalls.length === 0) {
4462
+ statusWatcher.stop();
4389
4463
  saveSession(state);
4390
4464
  onEvent({ type: "turn_done" });
4391
4465
  return;
@@ -4394,8 +4468,7 @@ async function runTurn(params) {
4394
4468
  count: toolCalls.length,
4395
4469
  tools: toolCalls.map((tc) => tc.name)
4396
4470
  });
4397
- let subAgentText = "";
4398
- const origOnEvent = onEvent;
4471
+ currentToolNames = toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ");
4399
4472
  const wrappedOnEvent = (e) => {
4400
4473
  if ("parentToolId" in e && e.parentToolId) {
4401
4474
  if (e.type === "text") {
@@ -4404,86 +4477,103 @@ async function runTurn(params) {
4404
4477
  subAgentText = `Using ${e.name}`;
4405
4478
  }
4406
4479
  }
4407
- origOnEvent(e);
4480
+ onEvent(e);
4408
4481
  };
4409
- const toolStatusWatcher = startStatusWatcher({
4410
- apiConfig,
4411
- getContext: () => ({
4412
- assistantText: subAgentText || getTextContent(contentBlocks).slice(-500),
4413
- lastToolName: toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ") || void 0,
4414
- lastToolResult: lastCompletedResult || void 0,
4415
- onboardingState,
4416
- userMessage
4417
- }),
4418
- onStatus: (label) => origOnEvent({ type: "status", message: label }),
4419
- signal
4420
- });
4421
4482
  const subAgentMessages = /* @__PURE__ */ new Map();
4422
4483
  const results = await Promise.all(
4423
4484
  toolCalls.map(async (tc) => {
4424
4485
  if (signal?.aborted) {
4425
- return {
4426
- id: tc.id,
4427
- result: "Error: cancelled",
4428
- isError: true
4429
- };
4486
+ return { id: tc.id, result: "Error: cancelled", isError: true };
4430
4487
  }
4431
4488
  const toolStart = Date.now();
4432
- try {
4433
- let result;
4434
- if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
4435
- saveSession(state);
4436
- log.info("Waiting for external tool result", {
4437
- name: tc.name,
4438
- id: tc.id
4439
- });
4440
- result = await resolveExternalTool(tc.id, tc.name, tc.input);
4441
- } else {
4442
- result = await executeTool(tc.name, tc.input, {
4443
- apiConfig,
4444
- model,
4445
- signal,
4446
- onEvent: wrappedOnEvent,
4447
- resolveExternalTool,
4448
- toolCallId: tc.id,
4449
- subAgentMessages,
4450
- onLog: (line) => wrappedOnEvent({
4451
- type: "tool_input_delta",
4452
- id: tc.id,
4489
+ let settle;
4490
+ const resultPromise = new Promise((res) => {
4491
+ settle = (result, isError) => res({ id: tc.id, result, isError });
4492
+ });
4493
+ let toolAbort = new AbortController();
4494
+ const cascadeAbort = () => toolAbort.abort();
4495
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
4496
+ let settled = false;
4497
+ const safeSettle = (result, isError) => {
4498
+ if (settled) {
4499
+ return;
4500
+ }
4501
+ settled = true;
4502
+ signal?.removeEventListener("abort", cascadeAbort);
4503
+ settle(result, isError);
4504
+ };
4505
+ const run = async (input) => {
4506
+ try {
4507
+ let result;
4508
+ if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
4509
+ saveSession(state);
4510
+ log.info("Waiting for external tool result", {
4453
4511
  name: tc.name,
4454
- result: line
4455
- })
4512
+ id: tc.id
4513
+ });
4514
+ result = await resolveExternalTool(tc.id, tc.name, input);
4515
+ } else {
4516
+ result = await executeTool(tc.name, input, {
4517
+ apiConfig,
4518
+ model,
4519
+ signal: toolAbort.signal,
4520
+ onEvent: wrappedOnEvent,
4521
+ resolveExternalTool,
4522
+ toolCallId: tc.id,
4523
+ subAgentMessages,
4524
+ toolRegistry,
4525
+ onLog: (line) => wrappedOnEvent({
4526
+ type: "tool_input_delta",
4527
+ id: tc.id,
4528
+ name: tc.name,
4529
+ result: line
4530
+ })
4531
+ });
4532
+ }
4533
+ safeSettle(result, result.startsWith("Error"));
4534
+ } catch (err) {
4535
+ safeSettle(`Error: ${err.message}`, true);
4536
+ }
4537
+ };
4538
+ const entry = {
4539
+ id: tc.id,
4540
+ name: tc.name,
4541
+ input: tc.input,
4542
+ abortController: toolAbort,
4543
+ startedAt: toolStart,
4544
+ settle: safeSettle,
4545
+ rerun: (newInput) => {
4546
+ settled = false;
4547
+ toolAbort = new AbortController();
4548
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
4549
+ once: true
4456
4550
  });
4551
+ entry.abortController = toolAbort;
4552
+ entry.input = newInput;
4553
+ run(newInput);
4457
4554
  }
4458
- const isError = result.startsWith("Error");
4459
- log.info("Tool completed", {
4460
- name: tc.name,
4461
- elapsed: `${Date.now() - toolStart}ms`,
4462
- isError,
4463
- resultLength: result.length
4464
- });
4465
- onEvent({
4466
- type: "tool_done",
4467
- id: tc.id,
4468
- name: tc.name,
4469
- result,
4470
- isError
4471
- });
4472
- return { id: tc.id, result, isError };
4473
- } catch (err) {
4474
- const errorMsg = `Error: ${err.message}`;
4475
- onEvent({
4476
- type: "tool_done",
4477
- id: tc.id,
4478
- name: tc.name,
4479
- result: errorMsg,
4480
- isError: true
4481
- });
4482
- return { id: tc.id, result: errorMsg, isError: true };
4483
- }
4555
+ };
4556
+ toolRegistry?.register(entry);
4557
+ run(tc.input);
4558
+ const r = await resultPromise;
4559
+ toolRegistry?.unregister(tc.id);
4560
+ log.info("Tool completed", {
4561
+ name: tc.name,
4562
+ elapsed: `${Date.now() - toolStart}ms`,
4563
+ isError: r.isError,
4564
+ resultLength: r.result.length
4565
+ });
4566
+ onEvent({
4567
+ type: "tool_done",
4568
+ id: tc.id,
4569
+ name: tc.name,
4570
+ result: r.result,
4571
+ isError: r.isError
4572
+ });
4573
+ return r;
4484
4574
  })
4485
4575
  );
4486
- toolStatusWatcher.stop();
4576
+ statusWatcher.stop();
4487
4577
  for (const r of results) {
4488
4578
  const block = contentBlocks.find(
4489
4579
  (b) => b.type === "tool" && b.id === r.id
@@ -4491,6 +4581,7 @@ async function runTurn(params) {
4491
4581
  if (block?.type === "tool") {
4492
4582
  block.result = r.result;
4493
4583
  block.isError = r.isError;
4584
+ block.completedAt = Date.now();
4494
4585
  const msgs = subAgentMessages.get(r.id);
4495
4586
  if (msgs) {
4496
4587
  block.subAgentMessages = msgs;
@@ -4515,6 +4606,78 @@ async function runTurn(params) {
4515
4606
  }
4516
4607
  }
4517
4608
 
4609
+ // src/toolRegistry.ts
4610
+ var ToolRegistry = class {
4611
+ entries = /* @__PURE__ */ new Map();
4612
+ onEvent;
4613
+ register(entry) {
4614
+ this.entries.set(entry.id, entry);
4615
+ }
4616
+ unregister(id) {
4617
+ this.entries.delete(id);
4618
+ }
4619
+ get(id) {
4620
+ return this.entries.get(id);
4621
+ }
4622
+ /**
4623
+ * Stop a running tool.
4624
+ *
4625
+ * - graceful: abort and settle with [INTERRUPTED] + partial result
4626
+ * - hard: abort and settle with a generic error
4627
+ *
4628
+ * Returns true if the tool was found and stopped.
4629
+ */
4630
+ stop(id, mode) {
4631
+ const entry = this.entries.get(id);
4632
+ if (!entry) {
4633
+ return false;
4634
+ }
4635
+ entry.abortController.abort(mode);
4636
+ if (mode === "graceful") {
4637
+ const partial = entry.getPartialResult?.() ?? "";
4638
+ const result = partial ? `[INTERRUPTED]
4639
+
4640
+ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
4641
+ entry.settle(result, false);
4642
+ } else {
4643
+ entry.settle("Error: tool was cancelled", true);
4644
+ }
4645
+ this.onEvent?.({
4646
+ type: "tool_stopped",
4647
+ id: entry.id,
4648
+ name: entry.name,
4649
+ mode,
4650
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
4651
+ });
4652
+ this.entries.delete(id);
4653
+ return true;
4654
+ }
4655
+ /**
4656
+ * Restart a running tool with the same or patched input.
4657
+ * The original controllable promise stays pending and settles
4658
+ * when the new execution finishes.
4659
+ *
4660
+ * Returns true if the tool was found and restarted.
4661
+ */
4662
+ restart(id, patchedInput) {
4663
+ const entry = this.entries.get(id);
4664
+ if (!entry) {
4665
+ return false;
4666
+ }
4667
+ entry.abortController.abort("restart");
4668
+ const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
4669
+ this.onEvent?.({
4670
+ type: "tool_restarted",
4671
+ id: entry.id,
4672
+ name: entry.name,
4673
+ input: newInput,
4674
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
4675
+ });
4676
+ entry.rerun(newInput);
4677
+ return true;
4678
+ }
4679
+ };
4680
+
4518
4681
  // src/headless.ts
4519
4682
  function loadActionPrompt(name) {
4520
4683
  return readAsset("prompt", "actions", `${name}.md`);
@@ -4581,6 +4744,7 @@ async function startHeadless(opts = {}) {
4581
4744
  const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
4582
4745
  const pendingTools = /* @__PURE__ */ new Map();
4583
4746
  const earlyResults = /* @__PURE__ */ new Map();
4747
+ const toolRegistry = new ToolRegistry();
4584
4748
  const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
4585
4749
  "promptUser",
4586
4750
  "confirmDestructiveAction",
@@ -4685,14 +4849,46 @@ async function startHeadless(opts = {}) {
4685
4849
  rid
4686
4850
  );
4687
4851
  return;
4852
+ case "tool_stopped":
4853
+ emit(
4854
+ "tool_stopped",
4855
+ {
4856
+ id: e.id,
4857
+ name: e.name,
4858
+ mode: e.mode,
4859
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4860
+ },
4861
+ rid
4862
+ );
4863
+ return;
4864
+ case "tool_restarted":
4865
+ emit(
4866
+ "tool_restarted",
4867
+ {
4868
+ id: e.id,
4869
+ name: e.name,
4870
+ input: e.input,
4871
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4872
+ },
4873
+ rid
4874
+ );
4875
+ return;
4688
4876
  case "status":
4689
- emit("status", { message: e.message }, rid);
4877
+ emit(
4878
+ "status",
4879
+ {
4880
+ message: e.message,
4881
+ ...e.parentToolId && { parentToolId: e.parentToolId }
4882
+ },
4883
+ rid
4884
+ );
4690
4885
  return;
4691
4886
  case "error":
4692
4887
  emit("error", { error: e.error }, rid);
4693
4888
  return;
4694
4889
  }
4695
4890
  }
4891
+ toolRegistry.onEvent = onEvent;
4696
4892
  async function handleMessage(parsed, requestId) {
4697
4893
  if (running) {
4698
4894
  emit(
@@ -4744,7 +4940,8 @@ async function startHeadless(opts = {}) {
4744
4940
  signal: currentAbort.signal,
4745
4941
  onEvent,
4746
4942
  resolveExternalTool,
4747
- hidden: isCommand
4943
+ hidden: isCommand,
4944
+ toolRegistry
4748
4945
  });
4749
4946
  if (!completedEmitted) {
4750
4947
  emit(
@@ -4798,6 +4995,36 @@ async function startHeadless(opts = {}) {
4798
4995
  emit("completed", { success: true }, requestId);
4799
4996
  return;
4800
4997
  }
4998
+ if (action === "stop_tool") {
4999
+ const id = parsed.id;
5000
+ const mode = parsed.mode ?? "hard";
5001
+ const found = toolRegistry.stop(id, mode);
5002
+ if (found) {
5003
+ emit("completed", { success: true }, requestId);
5004
+ } else {
5005
+ emit(
5006
+ "completed",
5007
+ { success: false, error: "Tool not found" },
5008
+ requestId
5009
+ );
5010
+ }
5011
+ return;
5012
+ }
5013
+ if (action === "restart_tool") {
5014
+ const id = parsed.id;
5015
+ const patchedInput = parsed.input;
5016
+ const found = toolRegistry.restart(id, patchedInput);
5017
+ if (found) {
5018
+ emit("completed", { success: true }, requestId);
5019
+ } else {
5020
+ emit(
5021
+ "completed",
5022
+ { success: false, error: "Tool not found" },
5023
+ requestId
5024
+ );
5025
+ }
5026
+ return;
5027
+ }
4801
5028
  if (action === "message") {
4802
5029
  await handleMessage(parsed, requestId);
4803
5030
  return;