@mindstudio-ai/remy 0.1.35 → 0.1.37

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
@@ -593,6 +593,31 @@ async function* streamChatWithRetry(params, options) {
593
593
  return;
594
594
  }
595
595
  }
596
+ var FALLBACK_ACK = "[Message sent to agent. Agent is working in the background and will report back with its results when finished.]";
597
+ async function generateBackgroundAck(params) {
598
+ const url = `${params.apiConfig.baseUrl}/_internal/v2/agent/remy/generate-ack`;
599
+ try {
600
+ const res = await fetch(url, {
601
+ method: "POST",
602
+ headers: {
603
+ "Content-Type": "application/json",
604
+ Authorization: `Bearer ${params.apiConfig.apiKey}`
605
+ },
606
+ body: JSON.stringify({
607
+ agentName: params.agentName,
608
+ task: params.task
609
+ }),
610
+ signal: AbortSignal.timeout(2e4)
611
+ });
612
+ if (!res.ok) {
613
+ return FALLBACK_ACK;
614
+ }
615
+ const data = await res.json();
616
+ return data.message || FALLBACK_ACK;
617
+ } catch (err) {
618
+ return FALLBACK_ACK;
619
+ }
620
+ }
596
621
 
597
622
  // src/tools/spec/readSpec.ts
598
623
  import fs5 from "fs/promises";
@@ -1275,8 +1300,12 @@ function runCli(cmd, options) {
1275
1300
  const maxBuffer = options?.maxBuffer ?? 1024 * 1024;
1276
1301
  const cmdWithLogs = options?.jsonLogs && !cmd.includes("--json-logs") ? cmd.replace(/^(mindstudio\s+\S+)/, "$1 --json-logs") : cmd;
1277
1302
  const child = spawn("sh", ["-c", cmdWithLogs], {
1278
- stdio: ["ignore", "pipe", "pipe"]
1303
+ stdio: [options?.stdin ? "pipe" : "ignore", "pipe", "pipe"]
1279
1304
  });
1305
+ if (options?.stdin) {
1306
+ child.stdin.write(options.stdin);
1307
+ child.stdin.end();
1308
+ }
1280
1309
  const logs = [];
1281
1310
  let stdout = "";
1282
1311
  let stderr = "";
@@ -1430,20 +1459,27 @@ var searchGoogleTool = {
1430
1459
  }
1431
1460
  };
1432
1461
 
1433
- // src/tools/common/setProjectName.ts
1434
- var setProjectNameTool = {
1462
+ // src/tools/common/setProjectMetadata.ts
1463
+ var setProjectMetadataTool = {
1435
1464
  definition: {
1436
- name: "setProjectName",
1437
- description: `Set the project display name. Call this after intake once you have enough context to give the project a clear, descriptive name. Keep it short (2-4 words). Use the app's actual name if the user mentioned one, otherwise pick something descriptive ("Vendor Procurement App", "Recipe Manager").`,
1465
+ name: "setProjectMetadata",
1466
+ description: "Set project metadata. Can update any combination of: display name, app icon, and Open Graph share image. Provide only the fields you want to change.",
1438
1467
  inputSchema: {
1439
1468
  type: "object",
1440
1469
  properties: {
1441
1470
  name: {
1442
1471
  type: "string",
1443
- description: "The project name."
1472
+ description: "Project display name. Keep it short (2-4 words). Use the app's actual name if the user mentioned one, otherwise pick something descriptive."
1473
+ },
1474
+ iconUrl: {
1475
+ type: "string",
1476
+ description: "URL for the app icon (square."
1477
+ },
1478
+ openGraphShareImageUrl: {
1479
+ type: "string",
1480
+ description: "URL for the Open Graph share image (1200x630)."
1444
1481
  }
1445
- },
1446
- required: ["name"]
1482
+ }
1447
1483
  }
1448
1484
  },
1449
1485
  async execute() {
@@ -2066,7 +2102,7 @@ var restartProcessTool = {
2066
2102
  var runScenarioTool = {
2067
2103
  definition: {
2068
2104
  name: "runScenario",
2069
- description: "Run a scenario to seed the dev database with test data. Truncates all tables first, then executes the seed function and impersonates the scenario roles. Blocks until complete. Scenario IDs are defined in mindstudio.json. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for details.",
2105
+ description: "Run a scenario to seed the dev database with test data. Truncates all tables first, then executes the seed function and impersonates the scenario roles. Blocks until complete. Scenario IDs are defined in mindstudio.json. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for details. Return synchronously - no need to sleep before checking results.",
2070
2106
  inputSchema: {
2071
2107
  type: "object",
2072
2108
  properties: {
@@ -2087,7 +2123,7 @@ var runScenarioTool = {
2087
2123
  var runMethodTool = {
2088
2124
  definition: {
2089
2125
  name: "runMethod",
2090
- description: "Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details.",
2126
+ description: "Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details. Return synchronously - no need to sleep before checking results.",
2091
2127
  inputSchema: {
2092
2128
  type: "object",
2093
2129
  properties: {
@@ -2109,7 +2145,7 @@ var runMethodTool = {
2109
2145
  };
2110
2146
 
2111
2147
  // 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).";
2148
+ 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
2149
  async function captureAndAnalyzeScreenshot(promptOrOptions) {
2114
2150
  let prompt;
2115
2151
  let onLog;
@@ -2167,9 +2203,94 @@ var screenshotTool = {
2167
2203
  }
2168
2204
  };
2169
2205
 
2206
+ // src/statusWatcher.ts
2207
+ function startStatusWatcher(config) {
2208
+ const { apiConfig, getContext, onStatus, interval = 3e3, signal } = config;
2209
+ let lastLabel = "";
2210
+ let inflight = false;
2211
+ let stopped = false;
2212
+ const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
2213
+ async function tick() {
2214
+ if (stopped || signal?.aborted || inflight) {
2215
+ return;
2216
+ }
2217
+ inflight = true;
2218
+ try {
2219
+ const ctx = getContext();
2220
+ if (!ctx.assistantText && !ctx.lastToolName) {
2221
+ log.debug("Status watcher: no context, skipping");
2222
+ return;
2223
+ }
2224
+ log.debug("Status watcher: requesting label", {
2225
+ textLength: ctx.assistantText.length,
2226
+ lastToolName: ctx.lastToolName
2227
+ });
2228
+ const res = await fetch(url, {
2229
+ method: "POST",
2230
+ headers: {
2231
+ "Content-Type": "application/json",
2232
+ Authorization: `Bearer ${apiConfig.apiKey}`
2233
+ },
2234
+ body: JSON.stringify({
2235
+ assistantText: ctx.assistantText.slice(-500),
2236
+ lastToolName: ctx.lastToolName,
2237
+ lastToolResult: ctx.lastToolResult?.slice(-200),
2238
+ onboardingState: ctx.onboardingState,
2239
+ userMessage: ctx.userMessage?.slice(-200)
2240
+ }),
2241
+ signal
2242
+ });
2243
+ if (!res.ok) {
2244
+ log.debug("Status watcher: endpoint returned non-ok", {
2245
+ status: res.status
2246
+ });
2247
+ return;
2248
+ }
2249
+ const data = await res.json();
2250
+ if (!data.label) {
2251
+ log.debug("Status watcher: no label in response");
2252
+ return;
2253
+ }
2254
+ if (data.label === lastLabel) {
2255
+ log.debug("Status watcher: duplicate label, skipping", {
2256
+ label: data.label
2257
+ });
2258
+ return;
2259
+ }
2260
+ lastLabel = data.label;
2261
+ if (stopped) {
2262
+ return;
2263
+ }
2264
+ log.debug("Status watcher: emitting", { label: data.label });
2265
+ onStatus(data.label);
2266
+ } catch (err) {
2267
+ log.debug("Status watcher: error", { error: err?.message ?? "unknown" });
2268
+ } finally {
2269
+ inflight = false;
2270
+ }
2271
+ }
2272
+ const timer = setInterval(tick, interval);
2273
+ tick().catch(() => {
2274
+ });
2275
+ log.debug("Status watcher started", { interval });
2276
+ return {
2277
+ stop() {
2278
+ stopped = true;
2279
+ clearInterval(timer);
2280
+ log.debug("Status watcher stopped");
2281
+ }
2282
+ };
2283
+ }
2284
+
2170
2285
  // src/subagents/common/cleanMessages.ts
2171
2286
  function cleanMessagesForApi(messages) {
2172
2287
  return messages.map((msg) => {
2288
+ if (msg.role === "user" && typeof msg.content === "string" && msg.content.startsWith("@@automated::")) {
2289
+ return {
2290
+ ...msg,
2291
+ content: msg.content.replace(/^@@automated::[^@]*@@\n?/, "")
2292
+ };
2293
+ }
2173
2294
  if (!Array.isArray(msg.content)) {
2174
2295
  return msg;
2175
2296
  }
@@ -2207,171 +2328,264 @@ async function runSubAgent(config) {
2207
2328
  apiConfig,
2208
2329
  model,
2209
2330
  subAgentId,
2210
- signal,
2331
+ signal: parentSignal,
2211
2332
  parentToolId,
2212
2333
  onEvent,
2213
- resolveExternalTool
2334
+ resolveExternalTool,
2335
+ toolRegistry,
2336
+ background,
2337
+ onBackgroundComplete
2214
2338
  } = config;
2339
+ const bgAbort = background ? new AbortController() : null;
2340
+ const signal = background ? bgAbort.signal : parentSignal;
2215
2341
  const emit2 = (e) => {
2216
2342
  onEvent({ ...e, parentToolId });
2217
2343
  };
2218
- const messages = [{ role: "user", content: task }];
2219
- while (true) {
2220
- if (signal?.aborted) {
2344
+ const run = async () => {
2345
+ const messages = [{ role: "user", content: task }];
2346
+ function getPartialText(blocks) {
2347
+ return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2348
+ }
2349
+ function abortResult(blocks) {
2350
+ if (signal?.reason === "graceful") {
2351
+ const partial = getPartialText(blocks);
2352
+ return {
2353
+ text: partial ? `[INTERRUPTED - PARTIAL OUTPUT RETRIEVED] Note that partial output may include thinking text or other unfinalized decisions. It is NOT an authoritative response from this agent.
2354
+
2355
+ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
2356
+ messages
2357
+ };
2358
+ }
2221
2359
  return { text: "Error: cancelled", messages };
2222
2360
  }
2223
- const contentBlocks = [];
2224
- let thinkingStartedAt = 0;
2225
- let stopReason = "end_turn";
2226
- const fullSystem = `${system}
2361
+ let lastToolResult = "";
2362
+ while (true) {
2363
+ if (signal?.aborted) {
2364
+ return abortResult([]);
2365
+ }
2366
+ const contentBlocks = [];
2367
+ let thinkingStartedAt = 0;
2368
+ let stopReason = "end_turn";
2369
+ let currentToolNames = "";
2370
+ const statusWatcher = startStatusWatcher({
2371
+ apiConfig,
2372
+ getContext: () => ({
2373
+ assistantText: getPartialText(contentBlocks),
2374
+ lastToolName: currentToolNames || void 0,
2375
+ lastToolResult: lastToolResult || void 0
2376
+ }),
2377
+ onStatus: (label) => emit2({ type: "status", message: label }),
2378
+ signal
2379
+ });
2380
+ const fullSystem = `${system}
2227
2381
 
2228
2382
  Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC")}`;
2229
- try {
2230
- for await (const event of streamChatWithRetry({
2231
- ...apiConfig,
2232
- model,
2233
- subAgentId,
2234
- system: fullSystem,
2235
- messages: cleanMessagesForApi(messages),
2236
- tools: tools2,
2237
- signal
2238
- })) {
2239
- if (signal?.aborted) {
2240
- break;
2241
- }
2242
- switch (event.type) {
2243
- case "text": {
2244
- const lastBlock = contentBlocks.at(-1);
2245
- if (lastBlock?.type === "text") {
2246
- lastBlock.text += event.text;
2247
- } else {
2248
- contentBlocks.push({
2249
- type: "text",
2250
- text: event.text,
2251
- startedAt: event.ts
2252
- });
2253
- }
2254
- emit2({ type: "text", text: event.text });
2383
+ try {
2384
+ for await (const event of streamChatWithRetry(
2385
+ {
2386
+ ...apiConfig,
2387
+ model,
2388
+ subAgentId,
2389
+ system: fullSystem,
2390
+ messages: cleanMessagesForApi(messages),
2391
+ tools: tools2,
2392
+ signal
2393
+ },
2394
+ {
2395
+ onRetry: (attempt) => emit2({
2396
+ type: "status",
2397
+ message: `Lost connection, retrying (attempt ${attempt + 2} of 3)...`
2398
+ })
2399
+ }
2400
+ )) {
2401
+ if (signal?.aborted) {
2255
2402
  break;
2256
2403
  }
2257
- case "thinking":
2258
- if (!thinkingStartedAt) {
2259
- thinkingStartedAt = event.ts;
2404
+ switch (event.type) {
2405
+ case "text": {
2406
+ const lastBlock = contentBlocks.at(-1);
2407
+ if (lastBlock?.type === "text") {
2408
+ lastBlock.text += event.text;
2409
+ } else {
2410
+ contentBlocks.push({
2411
+ type: "text",
2412
+ text: event.text,
2413
+ startedAt: event.ts
2414
+ });
2415
+ }
2416
+ emit2({ type: "text", text: event.text });
2417
+ break;
2260
2418
  }
2261
- emit2({ type: "thinking", text: event.text });
2262
- break;
2263
- case "thinking_complete":
2264
- contentBlocks.push({
2265
- type: "thinking",
2266
- thinking: event.thinking,
2267
- signature: event.signature,
2268
- startedAt: thinkingStartedAt,
2269
- completedAt: event.ts
2270
- });
2271
- thinkingStartedAt = 0;
2272
- break;
2273
- case "tool_use":
2274
- contentBlocks.push({
2275
- type: "tool",
2276
- id: event.id,
2277
- name: event.name,
2278
- input: event.input,
2279
- startedAt: Date.now()
2280
- });
2281
- emit2({
2282
- type: "tool_start",
2283
- id: event.id,
2284
- name: event.name,
2285
- input: event.input
2286
- });
2287
- break;
2288
- case "done":
2289
- stopReason = event.stopReason;
2290
- break;
2291
- case "error":
2292
- return { text: `Error: ${event.error}`, messages };
2419
+ case "thinking":
2420
+ if (!thinkingStartedAt) {
2421
+ thinkingStartedAt = event.ts;
2422
+ }
2423
+ emit2({ type: "thinking", text: event.text });
2424
+ break;
2425
+ case "thinking_complete":
2426
+ contentBlocks.push({
2427
+ type: "thinking",
2428
+ thinking: event.thinking,
2429
+ signature: event.signature,
2430
+ startedAt: thinkingStartedAt,
2431
+ completedAt: event.ts
2432
+ });
2433
+ thinkingStartedAt = 0;
2434
+ break;
2435
+ case "tool_use":
2436
+ contentBlocks.push({
2437
+ type: "tool",
2438
+ id: event.id,
2439
+ name: event.name,
2440
+ input: event.input,
2441
+ startedAt: Date.now()
2442
+ });
2443
+ emit2({
2444
+ type: "tool_start",
2445
+ id: event.id,
2446
+ name: event.name,
2447
+ input: event.input
2448
+ });
2449
+ break;
2450
+ case "done":
2451
+ stopReason = event.stopReason;
2452
+ break;
2453
+ case "error":
2454
+ return { text: `Error: ${event.error}`, messages };
2455
+ }
2456
+ }
2457
+ } catch (err) {
2458
+ if (!signal?.aborted) {
2459
+ throw err;
2293
2460
  }
2294
2461
  }
2295
- } catch (err) {
2296
- if (!signal?.aborted) {
2297
- throw err;
2462
+ if (signal?.aborted) {
2463
+ statusWatcher.stop();
2464
+ return abortResult(contentBlocks);
2298
2465
  }
2299
- }
2300
- if (signal?.aborted) {
2301
- return { text: "Error: cancelled", messages };
2302
- }
2303
- messages.push({
2304
- role: "assistant",
2305
- content: contentBlocks
2306
- });
2307
- const toolCalls = contentBlocks.filter(
2308
- (b) => b.type === "tool"
2309
- );
2310
- if (stopReason !== "tool_use" || toolCalls.length === 0) {
2311
- const text = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2312
- return { text, messages };
2313
- }
2314
- log.info("Sub-agent executing tools", {
2315
- parentToolId,
2316
- count: toolCalls.length,
2317
- tools: toolCalls.map((tc) => tc.name)
2318
- });
2319
- const results = await Promise.all(
2320
- toolCalls.map(async (tc) => {
2321
- if (signal?.aborted) {
2322
- return { id: tc.id, result: "Error: cancelled", isError: true };
2323
- }
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
2334
- });
2335
- result = await executeTool2(tc.name, tc.input, tc.id, onLog);
2466
+ messages.push({
2467
+ role: "assistant",
2468
+ content: contentBlocks
2469
+ });
2470
+ const toolCalls = contentBlocks.filter(
2471
+ (b) => b.type === "tool"
2472
+ );
2473
+ if (stopReason !== "tool_use" || toolCalls.length === 0) {
2474
+ statusWatcher.stop();
2475
+ const text = getPartialText(contentBlocks);
2476
+ return { text, messages };
2477
+ }
2478
+ log.info("Sub-agent executing tools", {
2479
+ parentToolId,
2480
+ count: toolCalls.length,
2481
+ tools: toolCalls.map((tc) => tc.name)
2482
+ });
2483
+ currentToolNames = toolCalls.map((tc) => tc.name).join(", ");
2484
+ const results = await Promise.all(
2485
+ toolCalls.map(async (tc) => {
2486
+ if (signal?.aborted) {
2487
+ return { id: tc.id, result: "Error: cancelled", isError: true };
2336
2488
  }
2337
- const isError = result.startsWith("Error");
2338
- emit2({
2339
- type: "tool_done",
2489
+ let settle;
2490
+ const resultPromise = new Promise((res) => {
2491
+ settle = (result, isError) => res({ id: tc.id, result, isError });
2492
+ });
2493
+ let toolAbort = new AbortController();
2494
+ const cascadeAbort = () => toolAbort.abort();
2495
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
2496
+ let settled = false;
2497
+ const safeSettle = (result, isError) => {
2498
+ if (settled) {
2499
+ return;
2500
+ }
2501
+ settled = true;
2502
+ signal?.removeEventListener("abort", cascadeAbort);
2503
+ settle(result, isError);
2504
+ };
2505
+ const run2 = async (input) => {
2506
+ try {
2507
+ let result;
2508
+ if (externalTools.has(tc.name) && resolveExternalTool) {
2509
+ result = await resolveExternalTool(tc.id, tc.name, input);
2510
+ } else {
2511
+ const onLog = (line) => emit2({
2512
+ type: "tool_input_delta",
2513
+ id: tc.id,
2514
+ name: tc.name,
2515
+ result: line
2516
+ });
2517
+ result = await executeTool2(tc.name, input, tc.id, onLog);
2518
+ }
2519
+ safeSettle(result, result.startsWith("Error"));
2520
+ } catch (err) {
2521
+ safeSettle(`Error: ${err.message}`, true);
2522
+ }
2523
+ };
2524
+ const entry = {
2340
2525
  id: tc.id,
2341
2526
  name: tc.name,
2342
- result,
2343
- isError
2344
- });
2345
- return { id: tc.id, result, isError };
2346
- } catch (err) {
2347
- const errorMsg = `Error: ${err.message}`;
2527
+ input: tc.input,
2528
+ parentToolId,
2529
+ abortController: toolAbort,
2530
+ startedAt: Date.now(),
2531
+ settle: safeSettle,
2532
+ rerun: (newInput) => {
2533
+ settled = false;
2534
+ toolAbort = new AbortController();
2535
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
2536
+ once: true
2537
+ });
2538
+ entry.abortController = toolAbort;
2539
+ entry.input = newInput;
2540
+ run2(newInput);
2541
+ }
2542
+ };
2543
+ toolRegistry?.register(entry);
2544
+ run2(tc.input);
2545
+ const r = await resultPromise;
2546
+ toolRegistry?.unregister(tc.id);
2348
2547
  emit2({
2349
2548
  type: "tool_done",
2350
2549
  id: tc.id,
2351
2550
  name: tc.name,
2352
- result: errorMsg,
2353
- isError: true
2551
+ result: r.result,
2552
+ isError: r.isError
2354
2553
  });
2355
- return { id: tc.id, result: errorMsg, isError: true };
2356
- }
2357
- })
2358
- );
2359
- for (const r of results) {
2360
- const block = contentBlocks.find(
2361
- (b) => b.type === "tool" && b.id === r.id
2554
+ return r;
2555
+ })
2362
2556
  );
2363
- if (block?.type === "tool") {
2364
- block.result = r.result;
2365
- block.isError = r.isError;
2557
+ statusWatcher.stop();
2558
+ lastToolResult = results.at(-1)?.result ?? "";
2559
+ for (const r of results) {
2560
+ const block = contentBlocks.find(
2561
+ (b) => b.type === "tool" && b.id === r.id
2562
+ );
2563
+ if (block?.type === "tool") {
2564
+ block.result = r.result;
2565
+ block.isError = r.isError;
2566
+ block.completedAt = Date.now();
2567
+ }
2568
+ messages.push({
2569
+ role: "user",
2570
+ content: r.result,
2571
+ toolCallId: r.id,
2572
+ isToolError: r.isError
2573
+ });
2366
2574
  }
2367
- messages.push({
2368
- role: "user",
2369
- content: r.result,
2370
- toolCallId: r.id,
2371
- isToolError: r.isError
2372
- });
2373
2575
  }
2576
+ };
2577
+ if (!background) {
2578
+ return run();
2374
2579
  }
2580
+ const ack = await generateBackgroundAck({
2581
+ apiConfig,
2582
+ agentName: subAgentId || "agent",
2583
+ task
2584
+ });
2585
+ run().then((finalResult) => onBackgroundComplete?.(finalResult)).catch(
2586
+ (err) => onBackgroundComplete?.({ text: `Error: ${err.message}`, messages: [] })
2587
+ );
2588
+ return { text: ack, messages: [], backgrounded: true };
2375
2589
  }
2376
2590
 
2377
2591
  // src/subagents/browserAutomation/tools.ts
@@ -2594,7 +2808,8 @@ var browserAutomationTool = {
2594
2808
  }
2595
2809
  }
2596
2810
  return result2;
2597
- }
2811
+ },
2812
+ toolRegistry: context.toolRegistry
2598
2813
  });
2599
2814
  context.subAgentMessages?.set(context.toolCallId, result.messages);
2600
2815
  return result.text;
@@ -2684,7 +2899,7 @@ Brief description of the types used on the page. If you can identify the actual
2684
2899
  ## Techniques
2685
2900
  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
2901
 
2687
- Respond only with the analysis and absolutely no other text.
2902
+ Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.
2688
2903
  `;
2689
2904
  var definition3 = {
2690
2905
  name: "analyzeDesign",
@@ -2723,9 +2938,7 @@ async function execute3(input, onLog) {
2723
2938
  `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(imageUrl)} --output-key analysis --no-meta`,
2724
2939
  { timeout: 2e5, onLog }
2725
2940
  );
2726
- return isImageUrl ? analysis : `Screenshot: ${imageUrl}
2727
-
2728
- ${analysis}`;
2941
+ return JSON.stringify({ url: imageUrl, analysis });
2729
2942
  }
2730
2943
 
2731
2944
  // src/subagents/designExpert/tools/analyzeImage.ts
@@ -2734,10 +2947,10 @@ __export(analyzeImage_exports, {
2734
2947
  definition: () => definition4,
2735
2948
  execute: () => execute4
2736
2949
  });
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).";
2950
+ 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
2951
  var definition4 = {
2739
2952
  name: "analyzeImage",
2740
- 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.",
2953
+ description: "Analyze an image by URL using a vision model. Returns an objective description of what is visible \u2014 shapes, colors, layout, text, artifacts. Use for factual inventory of image contents, not for subjective design judgment - the vision model providing the analysis has no sense of design. You are the design expert - use the analysis tool for factual inventory, then apply your own expertise for quality and suitability assessments.",
2741
2954
  inputSchema: {
2742
2955
  type: "object",
2743
2956
  properties: {
@@ -2801,7 +3014,7 @@ __export(generateImages_exports, {
2801
3014
  });
2802
3015
 
2803
3016
  // 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.";
3017
+ 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
3018
  async function seedreamGenerate(opts) {
2806
3019
  const { prompts, sourceImages, transparentBackground, onLog } = opts;
2807
3020
  const width = opts.width || 2048;
@@ -2835,13 +3048,15 @@ async function seedreamGenerate(opts) {
2835
3048
  }
2836
3049
  }
2837
3050
  }));
2838
- const batchResult = await runCli(
2839
- `mindstudio batch ${JSON.stringify(JSON.stringify(steps))} --no-meta`,
2840
- { jsonLogs: true, timeout: 2e5, onLog }
2841
- );
3051
+ const batchResult = await runCli(`mindstudio batch --no-meta`, {
3052
+ jsonLogs: true,
3053
+ timeout: 2e5,
3054
+ onLog,
3055
+ stdin: JSON.stringify(steps)
3056
+ });
2842
3057
  try {
2843
3058
  const parsed = JSON.parse(batchResult);
2844
- imageUrls = parsed.results.map(
3059
+ imageUrls = parsed.map(
2845
3060
  (r) => r.output?.imageUrl ?? `Error: ${r.error}`
2846
3061
  );
2847
3062
  } catch {
@@ -3211,6 +3426,10 @@ var designExpertTool = {
3211
3426
  task: {
3212
3427
  type: "string",
3213
3428
  description: "What you need, in natural language. Include context about the project when relevant."
3429
+ },
3430
+ background: {
3431
+ type: "boolean",
3432
+ description: "Run in background \u2014 returns immediately and doesn't block while continuing to do work in the background. Reports results when finished working."
3214
3433
  }
3215
3434
  },
3216
3435
  required: ["task"]
@@ -3232,7 +3451,17 @@ var designExpertTool = {
3232
3451
  signal: context.signal,
3233
3452
  parentToolId: context.toolCallId,
3234
3453
  onEvent: context.onEvent,
3235
- resolveExternalTool: context.resolveExternalTool
3454
+ resolveExternalTool: context.resolveExternalTool,
3455
+ toolRegistry: context.toolRegistry,
3456
+ background: input.background,
3457
+ onBackgroundComplete: input.background ? (bgResult) => {
3458
+ context.onBackgroundComplete?.(
3459
+ context.toolCallId,
3460
+ "visualDesignExpert",
3461
+ bgResult.text,
3462
+ bgResult.messages
3463
+ );
3464
+ } : void 0
3236
3465
  });
3237
3466
  context.subAgentMessages?.set(context.toolCallId, result.messages);
3238
3467
  return result.text;
@@ -3493,6 +3722,10 @@ var productVisionTool = {
3493
3722
  task: {
3494
3723
  type: "string",
3495
3724
  description: "Brief description of what happened or what is needed. Do not specify how many items to create, what topics to cover, or how to think. The agent reads the spec files and decides for itself."
3725
+ },
3726
+ background: {
3727
+ type: "boolean",
3728
+ description: "Run in background \u2014 returns immediately and doesn't block while continuing to do work in the background. Reports results when finished working."
3496
3729
  }
3497
3730
  },
3498
3731
  required: ["task"]
@@ -3514,7 +3747,17 @@ var productVisionTool = {
3514
3747
  signal: context.signal,
3515
3748
  parentToolId: context.toolCallId,
3516
3749
  onEvent: context.onEvent,
3517
- resolveExternalTool: context.resolveExternalTool
3750
+ resolveExternalTool: context.resolveExternalTool,
3751
+ toolRegistry: context.toolRegistry,
3752
+ background: input.background,
3753
+ onBackgroundComplete: input.background ? (bgResult) => {
3754
+ context.onBackgroundComplete?.(
3755
+ context.toolCallId,
3756
+ "productVision",
3757
+ bgResult.text,
3758
+ bgResult.messages
3759
+ );
3760
+ } : void 0
3518
3761
  });
3519
3762
  context.subAgentMessages?.set(context.toolCallId, result.messages);
3520
3763
  return result.text;
@@ -3651,7 +3894,8 @@ var codeSanityCheckTool = {
3651
3894
  signal: context.signal,
3652
3895
  parentToolId: context.toolCallId,
3653
3896
  onEvent: context.onEvent,
3654
- resolveExternalTool: context.resolveExternalTool
3897
+ resolveExternalTool: context.resolveExternalTool,
3898
+ toolRegistry: context.toolRegistry
3655
3899
  });
3656
3900
  context.subAgentMessages?.set(context.toolCallId, result.messages);
3657
3901
  return result.text;
@@ -3690,7 +3934,7 @@ function getCommonTools() {
3690
3934
  askMindStudioSdkTool,
3691
3935
  fetchUrlTool,
3692
3936
  searchGoogleTool,
3693
- setProjectNameTool,
3937
+ setProjectMetadataTool,
3694
3938
  designExpertTool,
3695
3939
  productVisionTool,
3696
3940
  codeSanityCheckTool
@@ -3971,85 +4215,6 @@ function parsePartialJson(jsonString) {
3971
4215
  return parseAny();
3972
4216
  }
3973
4217
 
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
4218
  // src/errors.ts
4054
4219
  var patterns = [
4055
4220
  [
@@ -4096,7 +4261,7 @@ var EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
4096
4261
  "runScenario",
4097
4262
  "runMethod",
4098
4263
  "browserCommand",
4099
- "setProjectName"
4264
+ "setProjectMetadata"
4100
4265
  ]);
4101
4266
  function createAgentState() {
4102
4267
  return { messages: [] };
@@ -4113,7 +4278,9 @@ async function runTurn(params) {
4113
4278
  signal,
4114
4279
  onEvent,
4115
4280
  resolveExternalTool,
4116
- hidden
4281
+ hidden,
4282
+ toolRegistry,
4283
+ onBackgroundComplete
4117
4284
  } = params;
4118
4285
  const tools2 = getToolDefinitions(onboardingState);
4119
4286
  log.info("Turn started", {
@@ -4126,8 +4293,7 @@ async function runTurn(params) {
4126
4293
  }
4127
4294
  });
4128
4295
  onEvent({ type: "turn_started" });
4129
- const cleanMessage = userMessage.replace(/^@@automated::[^@]*@@/, "");
4130
- const userMsg = { role: "user", content: cleanMessage };
4296
+ const userMsg = { role: "user", content: userMessage };
4131
4297
  if (hidden) {
4132
4298
  userMsg.hidden = true;
4133
4299
  }
@@ -4141,7 +4307,7 @@ async function runTurn(params) {
4141
4307
  state.messages.push(userMsg);
4142
4308
  const STATUS_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
4143
4309
  "setProjectOnboardingState",
4144
- "setProjectName",
4310
+ "setProjectMetadata",
4145
4311
  "clearSyncStatus",
4146
4312
  "editsFinished"
4147
4313
  ]);
@@ -4166,6 +4332,20 @@ async function runTurn(params) {
4166
4332
  let thinkingStartedAt = 0;
4167
4333
  const toolInputAccumulators = /* @__PURE__ */ new Map();
4168
4334
  let stopReason = "end_turn";
4335
+ let subAgentText = "";
4336
+ let currentToolNames = "";
4337
+ const statusWatcher = startStatusWatcher({
4338
+ apiConfig,
4339
+ getContext: () => ({
4340
+ assistantText: subAgentText || getTextContent(contentBlocks).slice(-500),
4341
+ lastToolName: currentToolNames || getToolCalls(contentBlocks).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools || void 0,
4342
+ lastToolResult: lastCompletedResult || void 0,
4343
+ onboardingState,
4344
+ userMessage
4345
+ }),
4346
+ onStatus: (label) => onEvent({ type: "status", message: label }),
4347
+ signal
4348
+ });
4169
4349
  async function handlePartialInput(acc, id, name, partial) {
4170
4350
  const tool = getToolByName(name);
4171
4351
  if (!tool?.streaming) {
@@ -4229,18 +4409,6 @@ async function runTurn(params) {
4229
4409
  onEvent({ type: "tool_input_delta", id, name, result: content });
4230
4410
  }
4231
4411
  }
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
4412
  try {
4245
4413
  for await (const event of streamChatWithRetry(
4246
4414
  {
@@ -4326,7 +4494,8 @@ async function runTurn(params) {
4326
4494
  id: event.id,
4327
4495
  name: event.name,
4328
4496
  input: event.input,
4329
- startedAt: event.ts
4497
+ startedAt: event.ts,
4498
+ ...event.input.background && { background: true }
4330
4499
  });
4331
4500
  const acc = toolInputAccumulators.get(event.id);
4332
4501
  const tool = getToolByName(event.name);
@@ -4361,10 +4530,9 @@ async function runTurn(params) {
4361
4530
  } else {
4362
4531
  throw err;
4363
4532
  }
4364
- } finally {
4365
- statusWatcher.stop();
4366
4533
  }
4367
4534
  if (signal?.aborted) {
4535
+ statusWatcher.stop();
4368
4536
  if (contentBlocks.length > 0) {
4369
4537
  contentBlocks.push({
4370
4538
  type: "text",
@@ -4386,6 +4554,7 @@ async function runTurn(params) {
4386
4554
  });
4387
4555
  const toolCalls = getToolCalls(contentBlocks);
4388
4556
  if (stopReason !== "tool_use" || toolCalls.length === 0) {
4557
+ statusWatcher.stop();
4389
4558
  saveSession(state);
4390
4559
  onEvent({ type: "turn_done" });
4391
4560
  return;
@@ -4394,8 +4563,7 @@ async function runTurn(params) {
4394
4563
  count: toolCalls.length,
4395
4564
  tools: toolCalls.map((tc) => tc.name)
4396
4565
  });
4397
- let subAgentText = "";
4398
- const origOnEvent = onEvent;
4566
+ currentToolNames = toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ");
4399
4567
  const wrappedOnEvent = (e) => {
4400
4568
  if ("parentToolId" in e && e.parentToolId) {
4401
4569
  if (e.type === "text") {
@@ -4404,86 +4572,104 @@ async function runTurn(params) {
4404
4572
  subAgentText = `Using ${e.name}`;
4405
4573
  }
4406
4574
  }
4407
- origOnEvent(e);
4575
+ onEvent(e);
4408
4576
  };
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
4577
  const subAgentMessages = /* @__PURE__ */ new Map();
4422
4578
  const results = await Promise.all(
4423
4579
  toolCalls.map(async (tc) => {
4424
4580
  if (signal?.aborted) {
4425
- return {
4426
- id: tc.id,
4427
- result: "Error: cancelled",
4428
- isError: true
4429
- };
4581
+ return { id: tc.id, result: "Error: cancelled", isError: true };
4430
4582
  }
4431
4583
  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,
4584
+ let settle;
4585
+ const resultPromise = new Promise((res) => {
4586
+ settle = (result, isError) => res({ id: tc.id, result, isError });
4587
+ });
4588
+ let toolAbort = new AbortController();
4589
+ const cascadeAbort = () => toolAbort.abort();
4590
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
4591
+ let settled = false;
4592
+ const safeSettle = (result, isError) => {
4593
+ if (settled) {
4594
+ return;
4595
+ }
4596
+ settled = true;
4597
+ signal?.removeEventListener("abort", cascadeAbort);
4598
+ settle(result, isError);
4599
+ };
4600
+ const run = async (input) => {
4601
+ try {
4602
+ let result;
4603
+ if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
4604
+ saveSession(state);
4605
+ log.info("Waiting for external tool result", {
4453
4606
  name: tc.name,
4454
- result: line
4455
- })
4607
+ id: tc.id
4608
+ });
4609
+ result = await resolveExternalTool(tc.id, tc.name, input);
4610
+ } else {
4611
+ result = await executeTool(tc.name, input, {
4612
+ apiConfig,
4613
+ model,
4614
+ signal: toolAbort.signal,
4615
+ onEvent: wrappedOnEvent,
4616
+ resolveExternalTool,
4617
+ toolCallId: tc.id,
4618
+ subAgentMessages,
4619
+ toolRegistry,
4620
+ onBackgroundComplete,
4621
+ onLog: (line) => wrappedOnEvent({
4622
+ type: "tool_input_delta",
4623
+ id: tc.id,
4624
+ name: tc.name,
4625
+ result: line
4626
+ })
4627
+ });
4628
+ }
4629
+ safeSettle(result, result.startsWith("Error"));
4630
+ } catch (err) {
4631
+ safeSettle(`Error: ${err.message}`, true);
4632
+ }
4633
+ };
4634
+ const entry = {
4635
+ id: tc.id,
4636
+ name: tc.name,
4637
+ input: tc.input,
4638
+ abortController: toolAbort,
4639
+ startedAt: toolStart,
4640
+ settle: safeSettle,
4641
+ rerun: (newInput) => {
4642
+ settled = false;
4643
+ toolAbort = new AbortController();
4644
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
4645
+ once: true
4456
4646
  });
4647
+ entry.abortController = toolAbort;
4648
+ entry.input = newInput;
4649
+ run(newInput);
4457
4650
  }
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
- }
4651
+ };
4652
+ toolRegistry?.register(entry);
4653
+ run(tc.input);
4654
+ const r = await resultPromise;
4655
+ toolRegistry?.unregister(tc.id);
4656
+ log.info("Tool completed", {
4657
+ name: tc.name,
4658
+ elapsed: `${Date.now() - toolStart}ms`,
4659
+ isError: r.isError,
4660
+ resultLength: r.result.length
4661
+ });
4662
+ onEvent({
4663
+ type: "tool_done",
4664
+ id: tc.id,
4665
+ name: tc.name,
4666
+ result: r.result,
4667
+ isError: r.isError
4668
+ });
4669
+ return r;
4484
4670
  })
4485
4671
  );
4486
- toolStatusWatcher.stop();
4672
+ statusWatcher.stop();
4487
4673
  for (const r of results) {
4488
4674
  const block = contentBlocks.find(
4489
4675
  (b) => b.type === "tool" && b.id === r.id
@@ -4491,6 +4677,7 @@ async function runTurn(params) {
4491
4677
  if (block?.type === "tool") {
4492
4678
  block.result = r.result;
4493
4679
  block.isError = r.isError;
4680
+ block.completedAt = Date.now();
4494
4681
  const msgs = subAgentMessages.get(r.id);
4495
4682
  if (msgs) {
4496
4683
  block.subAgentMessages = msgs;
@@ -4515,6 +4702,78 @@ async function runTurn(params) {
4515
4702
  }
4516
4703
  }
4517
4704
 
4705
+ // src/toolRegistry.ts
4706
+ var ToolRegistry = class {
4707
+ entries = /* @__PURE__ */ new Map();
4708
+ onEvent;
4709
+ register(entry) {
4710
+ this.entries.set(entry.id, entry);
4711
+ }
4712
+ unregister(id) {
4713
+ this.entries.delete(id);
4714
+ }
4715
+ get(id) {
4716
+ return this.entries.get(id);
4717
+ }
4718
+ /**
4719
+ * Stop a running tool.
4720
+ *
4721
+ * - graceful: abort and settle with [INTERRUPTED] + partial result
4722
+ * - hard: abort and settle with a generic error
4723
+ *
4724
+ * Returns true if the tool was found and stopped.
4725
+ */
4726
+ stop(id, mode) {
4727
+ const entry = this.entries.get(id);
4728
+ if (!entry) {
4729
+ return false;
4730
+ }
4731
+ entry.abortController.abort(mode);
4732
+ if (mode === "graceful") {
4733
+ const partial = entry.getPartialResult?.() ?? "";
4734
+ const result = partial ? `[INTERRUPTED]
4735
+
4736
+ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
4737
+ entry.settle(result, false);
4738
+ } else {
4739
+ entry.settle("Error: tool was cancelled", true);
4740
+ }
4741
+ this.onEvent?.({
4742
+ type: "tool_stopped",
4743
+ id: entry.id,
4744
+ name: entry.name,
4745
+ mode,
4746
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
4747
+ });
4748
+ this.entries.delete(id);
4749
+ return true;
4750
+ }
4751
+ /**
4752
+ * Restart a running tool with the same or patched input.
4753
+ * The original controllable promise stays pending and settles
4754
+ * when the new execution finishes.
4755
+ *
4756
+ * Returns true if the tool was found and restarted.
4757
+ */
4758
+ restart(id, patchedInput) {
4759
+ const entry = this.entries.get(id);
4760
+ if (!entry) {
4761
+ return false;
4762
+ }
4763
+ entry.abortController.abort("restart");
4764
+ const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
4765
+ this.onEvent?.({
4766
+ type: "tool_restarted",
4767
+ id: entry.id,
4768
+ name: entry.name,
4769
+ input: newInput,
4770
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
4771
+ });
4772
+ entry.rerun(newInput);
4773
+ return true;
4774
+ }
4775
+ };
4776
+
4518
4777
  // src/headless.ts
4519
4778
  function loadActionPrompt(name) {
4520
4779
  return readAsset("prompt", "actions", `${name}.md`);
@@ -4581,6 +4840,55 @@ async function startHeadless(opts = {}) {
4581
4840
  const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
4582
4841
  const pendingTools = /* @__PURE__ */ new Map();
4583
4842
  const earlyResults = /* @__PURE__ */ new Map();
4843
+ const toolRegistry = new ToolRegistry();
4844
+ const backgroundQueue = [];
4845
+ function flushBackgroundQueue() {
4846
+ if (backgroundQueue.length === 0) {
4847
+ return;
4848
+ }
4849
+ const results = backgroundQueue.splice(0);
4850
+ const xmlParts = results.map(
4851
+ (r) => `<tool_result id="${r.toolCallId}" name="${r.name}">
4852
+ ${r.result}
4853
+ </tool_result>`
4854
+ ).join("\n\n");
4855
+ const message = `@@automated::background_results@@
4856
+ <background_results>
4857
+ ${xmlParts}
4858
+ </background_results>`;
4859
+ handleMessage({ action: "message", text: message }, void 0);
4860
+ }
4861
+ function onBackgroundComplete(toolCallId, name, result, subAgentMessages) {
4862
+ for (const msg of state.messages) {
4863
+ if (!Array.isArray(msg.content)) {
4864
+ continue;
4865
+ }
4866
+ for (const block of msg.content) {
4867
+ if (block.type === "tool" && block.id === toolCallId) {
4868
+ block.backgroundResult = result;
4869
+ block.completedAt = Date.now();
4870
+ if (subAgentMessages) {
4871
+ block.subAgentMessages = subAgentMessages;
4872
+ }
4873
+ }
4874
+ }
4875
+ }
4876
+ onEvent({
4877
+ type: "tool_background_complete",
4878
+ id: toolCallId,
4879
+ name,
4880
+ result
4881
+ });
4882
+ backgroundQueue.push({
4883
+ toolCallId,
4884
+ name,
4885
+ result,
4886
+ completedAt: Date.now()
4887
+ });
4888
+ if (!running) {
4889
+ flushBackgroundQueue();
4890
+ }
4891
+ }
4584
4892
  const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
4585
4893
  "promptUser",
4586
4894
  "confirmDestructiveAction",
@@ -4621,6 +4929,7 @@ async function startHeadless(opts = {}) {
4621
4929
  case "turn_done":
4622
4930
  completedEmitted = true;
4623
4931
  emit("completed", { success: true }, rid);
4932
+ setTimeout(() => flushBackgroundQueue(), 0);
4624
4933
  return;
4625
4934
  case "turn_cancelled":
4626
4935
  completedEmitted = true;
@@ -4667,6 +4976,7 @@ async function startHeadless(opts = {}) {
4667
4976
  name: e.name,
4668
4977
  input: e.input,
4669
4978
  ...e.partial && { partial: true },
4979
+ ...e.background && { background: true },
4670
4980
  ...e.parentToolId && { parentToolId: e.parentToolId }
4671
4981
  },
4672
4982
  rid
@@ -4685,14 +4995,58 @@ async function startHeadless(opts = {}) {
4685
4995
  rid
4686
4996
  );
4687
4997
  return;
4998
+ case "tool_background_complete":
4999
+ emit(
5000
+ "tool_background_complete",
5001
+ {
5002
+ id: e.id,
5003
+ name: e.name,
5004
+ result: e.result,
5005
+ ...e.parentToolId && { parentToolId: e.parentToolId }
5006
+ },
5007
+ rid
5008
+ );
5009
+ return;
5010
+ case "tool_stopped":
5011
+ emit(
5012
+ "tool_stopped",
5013
+ {
5014
+ id: e.id,
5015
+ name: e.name,
5016
+ mode: e.mode,
5017
+ ...e.parentToolId && { parentToolId: e.parentToolId }
5018
+ },
5019
+ rid
5020
+ );
5021
+ return;
5022
+ case "tool_restarted":
5023
+ emit(
5024
+ "tool_restarted",
5025
+ {
5026
+ id: e.id,
5027
+ name: e.name,
5028
+ input: e.input,
5029
+ ...e.parentToolId && { parentToolId: e.parentToolId }
5030
+ },
5031
+ rid
5032
+ );
5033
+ return;
4688
5034
  case "status":
4689
- emit("status", { message: e.message }, rid);
5035
+ emit(
5036
+ "status",
5037
+ {
5038
+ message: e.message,
5039
+ ...e.parentToolId && { parentToolId: e.parentToolId }
5040
+ },
5041
+ rid
5042
+ );
4690
5043
  return;
4691
5044
  case "error":
4692
5045
  emit("error", { error: e.error }, rid);
4693
5046
  return;
4694
5047
  }
4695
5048
  }
5049
+ toolRegistry.onEvent = onEvent;
4696
5050
  async function handleMessage(parsed, requestId) {
4697
5051
  if (running) {
4698
5052
  emit(
@@ -4720,6 +5074,7 @@ async function startHeadless(opts = {}) {
4720
5074
  }
4721
5075
  let userMessage = parsed.text ?? "";
4722
5076
  const isCommand = !!parsed.runCommand;
5077
+ const isHidden = isCommand || !!parsed.hidden;
4723
5078
  if (parsed.runCommand === "sync") {
4724
5079
  userMessage = loadActionPrompt("sync");
4725
5080
  } else if (parsed.runCommand === "publish") {
@@ -4744,7 +5099,9 @@ async function startHeadless(opts = {}) {
4744
5099
  signal: currentAbort.signal,
4745
5100
  onEvent,
4746
5101
  resolveExternalTool,
4747
- hidden: isCommand
5102
+ hidden: isHidden,
5103
+ toolRegistry,
5104
+ onBackgroundComplete
4748
5105
  });
4749
5106
  if (!completedEmitted) {
4750
5107
  emit(
@@ -4798,6 +5155,36 @@ async function startHeadless(opts = {}) {
4798
5155
  emit("completed", { success: true }, requestId);
4799
5156
  return;
4800
5157
  }
5158
+ if (action === "stop_tool") {
5159
+ const id = parsed.id;
5160
+ const mode = parsed.mode ?? "hard";
5161
+ const found = toolRegistry.stop(id, mode);
5162
+ if (found) {
5163
+ emit("completed", { success: true }, requestId);
5164
+ } else {
5165
+ emit(
5166
+ "completed",
5167
+ { success: false, error: "Tool not found" },
5168
+ requestId
5169
+ );
5170
+ }
5171
+ return;
5172
+ }
5173
+ if (action === "restart_tool") {
5174
+ const id = parsed.id;
5175
+ const patchedInput = parsed.input;
5176
+ const found = toolRegistry.restart(id, patchedInput);
5177
+ if (found) {
5178
+ emit("completed", { success: true }, requestId);
5179
+ } else {
5180
+ emit(
5181
+ "completed",
5182
+ { success: false, error: "Tool not found" },
5183
+ requestId
5184
+ );
5185
+ }
5186
+ return;
5187
+ }
4801
5188
  if (action === "message") {
4802
5189
  await handleMessage(parsed, requestId);
4803
5190
  return;