@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/index.js CHANGED
@@ -250,13 +250,38 @@ async function* streamChatWithRetry(params, options) {
250
250
  return;
251
251
  }
252
252
  }
253
- var MAX_RETRIES, INITIAL_BACKOFF_MS;
253
+ async function generateBackgroundAck(params) {
254
+ const url = `${params.apiConfig.baseUrl}/_internal/v2/agent/remy/generate-ack`;
255
+ try {
256
+ const res = await fetch(url, {
257
+ method: "POST",
258
+ headers: {
259
+ "Content-Type": "application/json",
260
+ Authorization: `Bearer ${params.apiConfig.apiKey}`
261
+ },
262
+ body: JSON.stringify({
263
+ agentName: params.agentName,
264
+ task: params.task
265
+ }),
266
+ signal: AbortSignal.timeout(2e4)
267
+ });
268
+ if (!res.ok) {
269
+ return FALLBACK_ACK;
270
+ }
271
+ const data = await res.json();
272
+ return data.message || FALLBACK_ACK;
273
+ } catch (err) {
274
+ return FALLBACK_ACK;
275
+ }
276
+ }
277
+ var MAX_RETRIES, INITIAL_BACKOFF_MS, FALLBACK_ACK;
254
278
  var init_api = __esm({
255
279
  "src/api.ts"() {
256
280
  "use strict";
257
281
  init_logger();
258
282
  MAX_RETRIES = 3;
259
283
  INITIAL_BACKOFF_MS = 1e3;
284
+ FALLBACK_ACK = "[Message sent to agent. Agent is working in the background and will report back with its results when finished.]";
260
285
  }
261
286
  });
262
287
 
@@ -1020,8 +1045,12 @@ function runCli(cmd, options) {
1020
1045
  const maxBuffer = options?.maxBuffer ?? 1024 * 1024;
1021
1046
  const cmdWithLogs = options?.jsonLogs && !cmd.includes("--json-logs") ? cmd.replace(/^(mindstudio\s+\S+)/, "$1 --json-logs") : cmd;
1022
1047
  const child = spawn("sh", ["-c", cmdWithLogs], {
1023
- stdio: ["ignore", "pipe", "pipe"]
1048
+ stdio: [options?.stdin ? "pipe" : "ignore", "pipe", "pipe"]
1024
1049
  });
1050
+ if (options?.stdin) {
1051
+ child.stdin.write(options.stdin);
1052
+ child.stdin.end();
1053
+ }
1025
1054
  const logs = [];
1026
1055
  let stdout = "";
1027
1056
  let stderr = "";
@@ -1201,24 +1230,31 @@ var init_searchGoogle = __esm({
1201
1230
  }
1202
1231
  });
1203
1232
 
1204
- // src/tools/common/setProjectName.ts
1205
- var setProjectNameTool;
1206
- var init_setProjectName = __esm({
1207
- "src/tools/common/setProjectName.ts"() {
1233
+ // src/tools/common/setProjectMetadata.ts
1234
+ var setProjectMetadataTool;
1235
+ var init_setProjectMetadata = __esm({
1236
+ "src/tools/common/setProjectMetadata.ts"() {
1208
1237
  "use strict";
1209
- setProjectNameTool = {
1238
+ setProjectMetadataTool = {
1210
1239
  definition: {
1211
- name: "setProjectName",
1212
- 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").`,
1240
+ name: "setProjectMetadata",
1241
+ 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.",
1213
1242
  inputSchema: {
1214
1243
  type: "object",
1215
1244
  properties: {
1216
1245
  name: {
1217
1246
  type: "string",
1218
- description: "The project name."
1247
+ 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."
1248
+ },
1249
+ iconUrl: {
1250
+ type: "string",
1251
+ description: "URL for the app icon (square."
1252
+ },
1253
+ openGraphShareImageUrl: {
1254
+ type: "string",
1255
+ description: "URL for the Open Graph share image (1200x630)."
1219
1256
  }
1220
- },
1221
- required: ["name"]
1257
+ }
1222
1258
  }
1223
1259
  },
1224
1260
  async execute() {
@@ -1972,7 +2008,7 @@ var init_runScenario = __esm({
1972
2008
  runScenarioTool = {
1973
2009
  definition: {
1974
2010
  name: "runScenario",
1975
- 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.",
2011
+ 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.",
1976
2012
  inputSchema: {
1977
2013
  type: "object",
1978
2014
  properties: {
@@ -1999,7 +2035,7 @@ var init_runMethod = __esm({
1999
2035
  runMethodTool = {
2000
2036
  definition: {
2001
2037
  name: "runMethod",
2002
- 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.",
2038
+ 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.",
2003
2039
  inputSchema: {
2004
2040
  type: "object",
2005
2041
  properties: {
@@ -2059,7 +2095,7 @@ var init_screenshot = __esm({
2059
2095
  init_sidecar();
2060
2096
  init_runCli();
2061
2097
  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).";
2098
+ 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
2099
  }
2064
2100
  });
2065
2101
 
@@ -2097,9 +2133,100 @@ var init_screenshot2 = __esm({
2097
2133
  }
2098
2134
  });
2099
2135
 
2136
+ // src/statusWatcher.ts
2137
+ function startStatusWatcher(config) {
2138
+ const { apiConfig, getContext, onStatus, interval = 3e3, signal } = config;
2139
+ let lastLabel = "";
2140
+ let inflight = false;
2141
+ let stopped = false;
2142
+ const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
2143
+ async function tick() {
2144
+ if (stopped || signal?.aborted || inflight) {
2145
+ return;
2146
+ }
2147
+ inflight = true;
2148
+ try {
2149
+ const ctx = getContext();
2150
+ if (!ctx.assistantText && !ctx.lastToolName) {
2151
+ log.debug("Status watcher: no context, skipping");
2152
+ return;
2153
+ }
2154
+ log.debug("Status watcher: requesting label", {
2155
+ textLength: ctx.assistantText.length,
2156
+ lastToolName: ctx.lastToolName
2157
+ });
2158
+ const res = await fetch(url, {
2159
+ method: "POST",
2160
+ headers: {
2161
+ "Content-Type": "application/json",
2162
+ Authorization: `Bearer ${apiConfig.apiKey}`
2163
+ },
2164
+ body: JSON.stringify({
2165
+ assistantText: ctx.assistantText.slice(-500),
2166
+ lastToolName: ctx.lastToolName,
2167
+ lastToolResult: ctx.lastToolResult?.slice(-200),
2168
+ onboardingState: ctx.onboardingState,
2169
+ userMessage: ctx.userMessage?.slice(-200)
2170
+ }),
2171
+ signal
2172
+ });
2173
+ if (!res.ok) {
2174
+ log.debug("Status watcher: endpoint returned non-ok", {
2175
+ status: res.status
2176
+ });
2177
+ return;
2178
+ }
2179
+ const data = await res.json();
2180
+ if (!data.label) {
2181
+ log.debug("Status watcher: no label in response");
2182
+ return;
2183
+ }
2184
+ if (data.label === lastLabel) {
2185
+ log.debug("Status watcher: duplicate label, skipping", {
2186
+ label: data.label
2187
+ });
2188
+ return;
2189
+ }
2190
+ lastLabel = data.label;
2191
+ if (stopped) {
2192
+ return;
2193
+ }
2194
+ log.debug("Status watcher: emitting", { label: data.label });
2195
+ onStatus(data.label);
2196
+ } catch (err) {
2197
+ log.debug("Status watcher: error", { error: err?.message ?? "unknown" });
2198
+ } finally {
2199
+ inflight = false;
2200
+ }
2201
+ }
2202
+ const timer = setInterval(tick, interval);
2203
+ tick().catch(() => {
2204
+ });
2205
+ log.debug("Status watcher started", { interval });
2206
+ return {
2207
+ stop() {
2208
+ stopped = true;
2209
+ clearInterval(timer);
2210
+ log.debug("Status watcher stopped");
2211
+ }
2212
+ };
2213
+ }
2214
+ var init_statusWatcher = __esm({
2215
+ "src/statusWatcher.ts"() {
2216
+ "use strict";
2217
+ init_logger();
2218
+ }
2219
+ });
2220
+
2100
2221
  // src/subagents/common/cleanMessages.ts
2101
2222
  function cleanMessagesForApi(messages) {
2102
2223
  return messages.map((msg) => {
2224
+ if (msg.role === "user" && typeof msg.content === "string" && msg.content.startsWith("@@automated::")) {
2225
+ return {
2226
+ ...msg,
2227
+ content: msg.content.replace(/^@@automated::[^@]*@@\n?/, "")
2228
+ };
2229
+ }
2103
2230
  if (!Array.isArray(msg.content)) {
2104
2231
  return msg;
2105
2232
  }
@@ -2142,177 +2269,271 @@ async function runSubAgent(config) {
2142
2269
  apiConfig,
2143
2270
  model,
2144
2271
  subAgentId,
2145
- signal,
2272
+ signal: parentSignal,
2146
2273
  parentToolId,
2147
2274
  onEvent,
2148
- resolveExternalTool
2275
+ resolveExternalTool,
2276
+ toolRegistry,
2277
+ background,
2278
+ onBackgroundComplete
2149
2279
  } = config;
2280
+ const bgAbort = background ? new AbortController() : null;
2281
+ const signal = background ? bgAbort.signal : parentSignal;
2150
2282
  const emit2 = (e) => {
2151
2283
  onEvent({ ...e, parentToolId });
2152
2284
  };
2153
- const messages = [{ role: "user", content: task }];
2154
- while (true) {
2155
- if (signal?.aborted) {
2285
+ const run = async () => {
2286
+ const messages = [{ role: "user", content: task }];
2287
+ function getPartialText(blocks) {
2288
+ return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2289
+ }
2290
+ function abortResult(blocks) {
2291
+ if (signal?.reason === "graceful") {
2292
+ const partial = getPartialText(blocks);
2293
+ return {
2294
+ 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.
2295
+
2296
+ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
2297
+ messages
2298
+ };
2299
+ }
2156
2300
  return { text: "Error: cancelled", messages };
2157
2301
  }
2158
- const contentBlocks = [];
2159
- let thinkingStartedAt = 0;
2160
- let stopReason = "end_turn";
2161
- const fullSystem = `${system}
2302
+ let lastToolResult = "";
2303
+ while (true) {
2304
+ if (signal?.aborted) {
2305
+ return abortResult([]);
2306
+ }
2307
+ const contentBlocks = [];
2308
+ let thinkingStartedAt = 0;
2309
+ let stopReason = "end_turn";
2310
+ let currentToolNames = "";
2311
+ const statusWatcher = startStatusWatcher({
2312
+ apiConfig,
2313
+ getContext: () => ({
2314
+ assistantText: getPartialText(contentBlocks),
2315
+ lastToolName: currentToolNames || void 0,
2316
+ lastToolResult: lastToolResult || void 0
2317
+ }),
2318
+ onStatus: (label) => emit2({ type: "status", message: label }),
2319
+ signal
2320
+ });
2321
+ const fullSystem = `${system}
2162
2322
 
2163
2323
  Current date/time: ${(/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC")}`;
2164
- try {
2165
- for await (const event of streamChatWithRetry({
2166
- ...apiConfig,
2167
- model,
2168
- subAgentId,
2169
- system: fullSystem,
2170
- messages: cleanMessagesForApi(messages),
2171
- tools: tools2,
2172
- signal
2173
- })) {
2174
- if (signal?.aborted) {
2175
- break;
2176
- }
2177
- switch (event.type) {
2178
- case "text": {
2179
- const lastBlock = contentBlocks.at(-1);
2180
- if (lastBlock?.type === "text") {
2181
- lastBlock.text += event.text;
2182
- } else {
2183
- contentBlocks.push({
2184
- type: "text",
2185
- text: event.text,
2186
- startedAt: event.ts
2187
- });
2188
- }
2189
- emit2({ type: "text", text: event.text });
2324
+ try {
2325
+ for await (const event of streamChatWithRetry(
2326
+ {
2327
+ ...apiConfig,
2328
+ model,
2329
+ subAgentId,
2330
+ system: fullSystem,
2331
+ messages: cleanMessagesForApi(messages),
2332
+ tools: tools2,
2333
+ signal
2334
+ },
2335
+ {
2336
+ onRetry: (attempt) => emit2({
2337
+ type: "status",
2338
+ message: `Lost connection, retrying (attempt ${attempt + 2} of 3)...`
2339
+ })
2340
+ }
2341
+ )) {
2342
+ if (signal?.aborted) {
2190
2343
  break;
2191
2344
  }
2192
- case "thinking":
2193
- if (!thinkingStartedAt) {
2194
- thinkingStartedAt = event.ts;
2345
+ switch (event.type) {
2346
+ case "text": {
2347
+ const lastBlock = contentBlocks.at(-1);
2348
+ if (lastBlock?.type === "text") {
2349
+ lastBlock.text += event.text;
2350
+ } else {
2351
+ contentBlocks.push({
2352
+ type: "text",
2353
+ text: event.text,
2354
+ startedAt: event.ts
2355
+ });
2356
+ }
2357
+ emit2({ type: "text", text: event.text });
2358
+ break;
2195
2359
  }
2196
- emit2({ type: "thinking", text: event.text });
2197
- break;
2198
- case "thinking_complete":
2199
- contentBlocks.push({
2200
- type: "thinking",
2201
- thinking: event.thinking,
2202
- signature: event.signature,
2203
- startedAt: thinkingStartedAt,
2204
- completedAt: event.ts
2205
- });
2206
- thinkingStartedAt = 0;
2207
- break;
2208
- case "tool_use":
2209
- contentBlocks.push({
2210
- type: "tool",
2211
- id: event.id,
2212
- name: event.name,
2213
- input: event.input,
2214
- startedAt: Date.now()
2215
- });
2216
- emit2({
2217
- type: "tool_start",
2218
- id: event.id,
2219
- name: event.name,
2220
- input: event.input
2221
- });
2222
- break;
2223
- case "done":
2224
- stopReason = event.stopReason;
2225
- break;
2226
- case "error":
2227
- return { text: `Error: ${event.error}`, messages };
2360
+ case "thinking":
2361
+ if (!thinkingStartedAt) {
2362
+ thinkingStartedAt = event.ts;
2363
+ }
2364
+ emit2({ type: "thinking", text: event.text });
2365
+ break;
2366
+ case "thinking_complete":
2367
+ contentBlocks.push({
2368
+ type: "thinking",
2369
+ thinking: event.thinking,
2370
+ signature: event.signature,
2371
+ startedAt: thinkingStartedAt,
2372
+ completedAt: event.ts
2373
+ });
2374
+ thinkingStartedAt = 0;
2375
+ break;
2376
+ case "tool_use":
2377
+ contentBlocks.push({
2378
+ type: "tool",
2379
+ id: event.id,
2380
+ name: event.name,
2381
+ input: event.input,
2382
+ startedAt: Date.now()
2383
+ });
2384
+ emit2({
2385
+ type: "tool_start",
2386
+ id: event.id,
2387
+ name: event.name,
2388
+ input: event.input
2389
+ });
2390
+ break;
2391
+ case "done":
2392
+ stopReason = event.stopReason;
2393
+ break;
2394
+ case "error":
2395
+ return { text: `Error: ${event.error}`, messages };
2396
+ }
2397
+ }
2398
+ } catch (err) {
2399
+ if (!signal?.aborted) {
2400
+ throw err;
2228
2401
  }
2229
2402
  }
2230
- } catch (err) {
2231
- if (!signal?.aborted) {
2232
- throw err;
2403
+ if (signal?.aborted) {
2404
+ statusWatcher.stop();
2405
+ return abortResult(contentBlocks);
2233
2406
  }
2234
- }
2235
- if (signal?.aborted) {
2236
- return { text: "Error: cancelled", messages };
2237
- }
2238
- messages.push({
2239
- role: "assistant",
2240
- content: contentBlocks
2241
- });
2242
- const toolCalls = contentBlocks.filter(
2243
- (b) => b.type === "tool"
2244
- );
2245
- if (stopReason !== "tool_use" || toolCalls.length === 0) {
2246
- const text = contentBlocks.filter((b) => b.type === "text").map((b) => b.text).join("");
2247
- return { text, messages };
2248
- }
2249
- log.info("Sub-agent executing tools", {
2250
- parentToolId,
2251
- count: toolCalls.length,
2252
- tools: toolCalls.map((tc) => tc.name)
2253
- });
2254
- const results = await Promise.all(
2255
- toolCalls.map(async (tc) => {
2256
- if (signal?.aborted) {
2257
- return { id: tc.id, result: "Error: cancelled", isError: true };
2258
- }
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
2269
- });
2270
- result = await executeTool2(tc.name, tc.input, tc.id, onLog);
2407
+ messages.push({
2408
+ role: "assistant",
2409
+ content: contentBlocks
2410
+ });
2411
+ const toolCalls = contentBlocks.filter(
2412
+ (b) => b.type === "tool"
2413
+ );
2414
+ if (stopReason !== "tool_use" || toolCalls.length === 0) {
2415
+ statusWatcher.stop();
2416
+ const text = getPartialText(contentBlocks);
2417
+ return { text, messages };
2418
+ }
2419
+ log.info("Sub-agent executing tools", {
2420
+ parentToolId,
2421
+ count: toolCalls.length,
2422
+ tools: toolCalls.map((tc) => tc.name)
2423
+ });
2424
+ currentToolNames = toolCalls.map((tc) => tc.name).join(", ");
2425
+ const results = await Promise.all(
2426
+ toolCalls.map(async (tc) => {
2427
+ if (signal?.aborted) {
2428
+ return { id: tc.id, result: "Error: cancelled", isError: true };
2271
2429
  }
2272
- const isError = result.startsWith("Error");
2273
- emit2({
2274
- type: "tool_done",
2430
+ let settle;
2431
+ const resultPromise = new Promise((res) => {
2432
+ settle = (result, isError) => res({ id: tc.id, result, isError });
2433
+ });
2434
+ let toolAbort = new AbortController();
2435
+ const cascadeAbort = () => toolAbort.abort();
2436
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
2437
+ let settled = false;
2438
+ const safeSettle = (result, isError) => {
2439
+ if (settled) {
2440
+ return;
2441
+ }
2442
+ settled = true;
2443
+ signal?.removeEventListener("abort", cascadeAbort);
2444
+ settle(result, isError);
2445
+ };
2446
+ const run2 = async (input) => {
2447
+ try {
2448
+ let result;
2449
+ if (externalTools.has(tc.name) && resolveExternalTool) {
2450
+ result = await resolveExternalTool(tc.id, tc.name, input);
2451
+ } else {
2452
+ const onLog = (line) => emit2({
2453
+ type: "tool_input_delta",
2454
+ id: tc.id,
2455
+ name: tc.name,
2456
+ result: line
2457
+ });
2458
+ result = await executeTool2(tc.name, input, tc.id, onLog);
2459
+ }
2460
+ safeSettle(result, result.startsWith("Error"));
2461
+ } catch (err) {
2462
+ safeSettle(`Error: ${err.message}`, true);
2463
+ }
2464
+ };
2465
+ const entry = {
2275
2466
  id: tc.id,
2276
2467
  name: tc.name,
2277
- result,
2278
- isError
2279
- });
2280
- return { id: tc.id, result, isError };
2281
- } catch (err) {
2282
- const errorMsg = `Error: ${err.message}`;
2468
+ input: tc.input,
2469
+ parentToolId,
2470
+ abortController: toolAbort,
2471
+ startedAt: Date.now(),
2472
+ settle: safeSettle,
2473
+ rerun: (newInput) => {
2474
+ settled = false;
2475
+ toolAbort = new AbortController();
2476
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
2477
+ once: true
2478
+ });
2479
+ entry.abortController = toolAbort;
2480
+ entry.input = newInput;
2481
+ run2(newInput);
2482
+ }
2483
+ };
2484
+ toolRegistry?.register(entry);
2485
+ run2(tc.input);
2486
+ const r = await resultPromise;
2487
+ toolRegistry?.unregister(tc.id);
2283
2488
  emit2({
2284
2489
  type: "tool_done",
2285
2490
  id: tc.id,
2286
2491
  name: tc.name,
2287
- result: errorMsg,
2288
- isError: true
2492
+ result: r.result,
2493
+ isError: r.isError
2289
2494
  });
2290
- return { id: tc.id, result: errorMsg, isError: true };
2291
- }
2292
- })
2293
- );
2294
- for (const r of results) {
2295
- const block = contentBlocks.find(
2296
- (b) => b.type === "tool" && b.id === r.id
2495
+ return r;
2496
+ })
2297
2497
  );
2298
- if (block?.type === "tool") {
2299
- block.result = r.result;
2300
- block.isError = r.isError;
2301
- }
2302
- messages.push({
2303
- role: "user",
2304
- content: r.result,
2305
- toolCallId: r.id,
2306
- isToolError: r.isError
2307
- });
2498
+ statusWatcher.stop();
2499
+ lastToolResult = results.at(-1)?.result ?? "";
2500
+ for (const r of results) {
2501
+ const block = contentBlocks.find(
2502
+ (b) => b.type === "tool" && b.id === r.id
2503
+ );
2504
+ if (block?.type === "tool") {
2505
+ block.result = r.result;
2506
+ block.isError = r.isError;
2507
+ block.completedAt = Date.now();
2508
+ }
2509
+ messages.push({
2510
+ role: "user",
2511
+ content: r.result,
2512
+ toolCallId: r.id,
2513
+ isToolError: r.isError
2514
+ });
2515
+ }
2308
2516
  }
2517
+ };
2518
+ if (!background) {
2519
+ return run();
2309
2520
  }
2521
+ const ack = await generateBackgroundAck({
2522
+ apiConfig,
2523
+ agentName: subAgentId || "agent",
2524
+ task
2525
+ });
2526
+ run().then((finalResult) => onBackgroundComplete?.(finalResult)).catch(
2527
+ (err) => onBackgroundComplete?.({ text: `Error: ${err.message}`, messages: [] })
2528
+ );
2529
+ return { text: ack, messages: [], backgrounded: true };
2310
2530
  }
2311
2531
  var init_runner = __esm({
2312
2532
  "src/subagents/runner.ts"() {
2313
2533
  "use strict";
2314
2534
  init_api();
2315
2535
  init_logger();
2536
+ init_statusWatcher();
2316
2537
  init_cleanMessages();
2317
2538
  }
2318
2539
  });
@@ -2604,7 +2825,8 @@ var init_browserAutomation = __esm({
2604
2825
  }
2605
2826
  }
2606
2827
  return result2;
2607
- }
2828
+ },
2829
+ toolRegistry: context.toolRegistry
2608
2830
  });
2609
2831
  context.subAgentMessages?.set(context.toolCallId, result.messages);
2610
2832
  return result.text;
@@ -2710,9 +2932,7 @@ async function execute3(input, onLog) {
2710
2932
  `mindstudio analyze-image --prompt ${JSON.stringify(analysisPrompt)} --image-url ${JSON.stringify(imageUrl)} --output-key analysis --no-meta`,
2711
2933
  { timeout: 2e5, onLog }
2712
2934
  );
2713
- return isImageUrl ? analysis : `Screenshot: ${imageUrl}
2714
-
2715
- ${analysis}`;
2935
+ return JSON.stringify({ url: imageUrl, analysis });
2716
2936
  }
2717
2937
  var DESIGN_REFERENCE_PROMPT, definition3;
2718
2938
  var init_analyzeDesign = __esm({
@@ -2738,7 +2958,7 @@ Brief description of the types used on the page. If you can identify the actual
2738
2958
  ## Techniques
2739
2959
  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
2960
 
2741
- Respond only with the analysis and absolutely no other text.
2961
+ Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.
2742
2962
  `;
2743
2963
  definition3 = {
2744
2964
  name: "analyzeDesign",
@@ -2781,10 +3001,10 @@ var init_analyzeImage = __esm({
2781
3001
  "src/subagents/designExpert/tools/analyzeImage.ts"() {
2782
3002
  "use strict";
2783
3003
  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).";
3004
+ 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
3005
  definition4 = {
2786
3006
  name: "analyzeImage",
2787
- 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.",
3007
+ 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.",
2788
3008
  inputSchema: {
2789
3009
  type: "object",
2790
3010
  properties: {
@@ -2874,13 +3094,15 @@ async function seedreamGenerate(opts) {
2874
3094
  }
2875
3095
  }
2876
3096
  }));
2877
- const batchResult = await runCli(
2878
- `mindstudio batch ${JSON.stringify(JSON.stringify(steps))} --no-meta`,
2879
- { jsonLogs: true, timeout: 2e5, onLog }
2880
- );
3097
+ const batchResult = await runCli(`mindstudio batch --no-meta`, {
3098
+ jsonLogs: true,
3099
+ timeout: 2e5,
3100
+ onLog,
3101
+ stdin: JSON.stringify(steps)
3102
+ });
2881
3103
  try {
2882
3104
  const parsed = JSON.parse(batchResult);
2883
- imageUrls = parsed.results.map(
3105
+ imageUrls = parsed.map(
2884
3106
  (r) => r.output?.imageUrl ?? `Error: ${r.error}`
2885
3107
  );
2886
3108
  } catch {
@@ -2920,7 +3142,7 @@ var init_seedream = __esm({
2920
3142
  "src/subagents/designExpert/tools/_seedream.ts"() {
2921
3143
  "use strict";
2922
3144
  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.";
3145
+ 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
3146
  }
2925
3147
  });
2926
3148
 
@@ -3324,6 +3546,10 @@ Visual design expert. Describe the situation and what you need \u2014 the agent
3324
3546
  task: {
3325
3547
  type: "string",
3326
3548
  description: "What you need, in natural language. Include context about the project when relevant."
3549
+ },
3550
+ background: {
3551
+ type: "boolean",
3552
+ description: "Run in background \u2014 returns immediately and doesn't block while continuing to do work in the background. Reports results when finished working."
3327
3553
  }
3328
3554
  },
3329
3555
  required: ["task"]
@@ -3345,7 +3571,17 @@ Visual design expert. Describe the situation and what you need \u2014 the agent
3345
3571
  signal: context.signal,
3346
3572
  parentToolId: context.toolCallId,
3347
3573
  onEvent: context.onEvent,
3348
- resolveExternalTool: context.resolveExternalTool
3574
+ resolveExternalTool: context.resolveExternalTool,
3575
+ toolRegistry: context.toolRegistry,
3576
+ background: input.background,
3577
+ onBackgroundComplete: input.background ? (bgResult) => {
3578
+ context.onBackgroundComplete?.(
3579
+ context.toolCallId,
3580
+ "visualDesignExpert",
3581
+ bgResult.text,
3582
+ bgResult.messages
3583
+ );
3584
+ } : void 0
3349
3585
  });
3350
3586
  context.subAgentMessages?.set(context.toolCallId, result.messages);
3351
3587
  return result.text;
@@ -3639,6 +3875,10 @@ var init_productVision = __esm({
3639
3875
  task: {
3640
3876
  type: "string",
3641
3877
  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."
3878
+ },
3879
+ background: {
3880
+ type: "boolean",
3881
+ description: "Run in background \u2014 returns immediately and doesn't block while continuing to do work in the background. Reports results when finished working."
3642
3882
  }
3643
3883
  },
3644
3884
  required: ["task"]
@@ -3660,7 +3900,17 @@ var init_productVision = __esm({
3660
3900
  signal: context.signal,
3661
3901
  parentToolId: context.toolCallId,
3662
3902
  onEvent: context.onEvent,
3663
- resolveExternalTool: context.resolveExternalTool
3903
+ resolveExternalTool: context.resolveExternalTool,
3904
+ toolRegistry: context.toolRegistry,
3905
+ background: input.background,
3906
+ onBackgroundComplete: input.background ? (bgResult) => {
3907
+ context.onBackgroundComplete?.(
3908
+ context.toolCallId,
3909
+ "productVision",
3910
+ bgResult.text,
3911
+ bgResult.messages
3912
+ );
3913
+ } : void 0
3664
3914
  });
3665
3915
  context.subAgentMessages?.set(context.toolCallId, result.messages);
3666
3916
  return result.text;
@@ -3814,7 +4064,8 @@ var init_codeSanityCheck = __esm({
3814
4064
  signal: context.signal,
3815
4065
  parentToolId: context.toolCallId,
3816
4066
  onEvent: context.onEvent,
3817
- resolveExternalTool: context.resolveExternalTool
4067
+ resolveExternalTool: context.resolveExternalTool,
4068
+ toolRegistry: context.toolRegistry
3818
4069
  });
3819
4070
  context.subAgentMessages?.set(context.toolCallId, result.messages);
3820
4071
  return result.text;
@@ -3855,7 +4106,7 @@ function getCommonTools() {
3855
4106
  askMindStudioSdkTool,
3856
4107
  fetchUrlTool,
3857
4108
  searchGoogleTool,
3858
- setProjectNameTool,
4109
+ setProjectMetadataTool,
3859
4110
  designExpertTool,
3860
4111
  productVisionTool,
3861
4112
  codeSanityCheckTool
@@ -3920,7 +4171,7 @@ var init_tools5 = __esm({
3920
4171
  init_sdkConsultant();
3921
4172
  init_fetchUrl();
3922
4173
  init_searchGoogle();
3923
- init_setProjectName();
4174
+ init_setProjectMetadata();
3924
4175
  init_readFile();
3925
4176
  init_writeFile();
3926
4177
  init_editFile();
@@ -4186,91 +4437,6 @@ var init_parsePartialJson = __esm({
4186
4437
  }
4187
4438
  });
4188
4439
 
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
4440
  // src/errors.ts
4275
4441
  function friendlyError(raw) {
4276
4442
  for (const [pattern, message] of patterns) {
@@ -4327,7 +4493,9 @@ async function runTurn(params) {
4327
4493
  signal,
4328
4494
  onEvent,
4329
4495
  resolveExternalTool,
4330
- hidden
4496
+ hidden,
4497
+ toolRegistry,
4498
+ onBackgroundComplete
4331
4499
  } = params;
4332
4500
  const tools2 = getToolDefinitions(onboardingState);
4333
4501
  log.info("Turn started", {
@@ -4340,8 +4508,7 @@ async function runTurn(params) {
4340
4508
  }
4341
4509
  });
4342
4510
  onEvent({ type: "turn_started" });
4343
- const cleanMessage = userMessage.replace(/^@@automated::[^@]*@@/, "");
4344
- const userMsg = { role: "user", content: cleanMessage };
4511
+ const userMsg = { role: "user", content: userMessage };
4345
4512
  if (hidden) {
4346
4513
  userMsg.hidden = true;
4347
4514
  }
@@ -4355,7 +4522,7 @@ async function runTurn(params) {
4355
4522
  state.messages.push(userMsg);
4356
4523
  const STATUS_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
4357
4524
  "setProjectOnboardingState",
4358
- "setProjectName",
4525
+ "setProjectMetadata",
4359
4526
  "clearSyncStatus",
4360
4527
  "editsFinished"
4361
4528
  ]);
@@ -4380,6 +4547,20 @@ async function runTurn(params) {
4380
4547
  let thinkingStartedAt = 0;
4381
4548
  const toolInputAccumulators = /* @__PURE__ */ new Map();
4382
4549
  let stopReason = "end_turn";
4550
+ let subAgentText = "";
4551
+ let currentToolNames = "";
4552
+ const statusWatcher = startStatusWatcher({
4553
+ apiConfig,
4554
+ getContext: () => ({
4555
+ assistantText: subAgentText || getTextContent(contentBlocks).slice(-500),
4556
+ lastToolName: currentToolNames || getToolCalls(contentBlocks).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools || void 0,
4557
+ lastToolResult: lastCompletedResult || void 0,
4558
+ onboardingState,
4559
+ userMessage
4560
+ }),
4561
+ onStatus: (label) => onEvent({ type: "status", message: label }),
4562
+ signal
4563
+ });
4383
4564
  async function handlePartialInput(acc, id, name, partial) {
4384
4565
  const tool = getToolByName(name);
4385
4566
  if (!tool?.streaming) {
@@ -4443,18 +4624,6 @@ async function runTurn(params) {
4443
4624
  onEvent({ type: "tool_input_delta", id, name, result: content });
4444
4625
  }
4445
4626
  }
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
4627
  try {
4459
4628
  for await (const event of streamChatWithRetry(
4460
4629
  {
@@ -4540,7 +4709,8 @@ async function runTurn(params) {
4540
4709
  id: event.id,
4541
4710
  name: event.name,
4542
4711
  input: event.input,
4543
- startedAt: event.ts
4712
+ startedAt: event.ts,
4713
+ ...event.input.background && { background: true }
4544
4714
  });
4545
4715
  const acc = toolInputAccumulators.get(event.id);
4546
4716
  const tool = getToolByName(event.name);
@@ -4575,10 +4745,9 @@ async function runTurn(params) {
4575
4745
  } else {
4576
4746
  throw err;
4577
4747
  }
4578
- } finally {
4579
- statusWatcher.stop();
4580
4748
  }
4581
4749
  if (signal?.aborted) {
4750
+ statusWatcher.stop();
4582
4751
  if (contentBlocks.length > 0) {
4583
4752
  contentBlocks.push({
4584
4753
  type: "text",
@@ -4600,6 +4769,7 @@ async function runTurn(params) {
4600
4769
  });
4601
4770
  const toolCalls = getToolCalls(contentBlocks);
4602
4771
  if (stopReason !== "tool_use" || toolCalls.length === 0) {
4772
+ statusWatcher.stop();
4603
4773
  saveSession(state);
4604
4774
  onEvent({ type: "turn_done" });
4605
4775
  return;
@@ -4608,8 +4778,7 @@ async function runTurn(params) {
4608
4778
  count: toolCalls.length,
4609
4779
  tools: toolCalls.map((tc) => tc.name)
4610
4780
  });
4611
- let subAgentText = "";
4612
- const origOnEvent = onEvent;
4781
+ currentToolNames = toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ");
4613
4782
  const wrappedOnEvent = (e) => {
4614
4783
  if ("parentToolId" in e && e.parentToolId) {
4615
4784
  if (e.type === "text") {
@@ -4618,86 +4787,104 @@ async function runTurn(params) {
4618
4787
  subAgentText = `Using ${e.name}`;
4619
4788
  }
4620
4789
  }
4621
- origOnEvent(e);
4790
+ onEvent(e);
4622
4791
  };
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
4792
  const subAgentMessages = /* @__PURE__ */ new Map();
4636
4793
  const results = await Promise.all(
4637
4794
  toolCalls.map(async (tc) => {
4638
4795
  if (signal?.aborted) {
4639
- return {
4640
- id: tc.id,
4641
- result: "Error: cancelled",
4642
- isError: true
4643
- };
4796
+ return { id: tc.id, result: "Error: cancelled", isError: true };
4644
4797
  }
4645
4798
  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,
4799
+ let settle;
4800
+ const resultPromise = new Promise((res) => {
4801
+ settle = (result, isError) => res({ id: tc.id, result, isError });
4802
+ });
4803
+ let toolAbort = new AbortController();
4804
+ const cascadeAbort = () => toolAbort.abort();
4805
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
4806
+ let settled = false;
4807
+ const safeSettle = (result, isError) => {
4808
+ if (settled) {
4809
+ return;
4810
+ }
4811
+ settled = true;
4812
+ signal?.removeEventListener("abort", cascadeAbort);
4813
+ settle(result, isError);
4814
+ };
4815
+ const run = async (input) => {
4816
+ try {
4817
+ let result;
4818
+ if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
4819
+ saveSession(state);
4820
+ log.info("Waiting for external tool result", {
4667
4821
  name: tc.name,
4668
- result: line
4669
- })
4822
+ id: tc.id
4823
+ });
4824
+ result = await resolveExternalTool(tc.id, tc.name, input);
4825
+ } else {
4826
+ result = await executeTool(tc.name, input, {
4827
+ apiConfig,
4828
+ model,
4829
+ signal: toolAbort.signal,
4830
+ onEvent: wrappedOnEvent,
4831
+ resolveExternalTool,
4832
+ toolCallId: tc.id,
4833
+ subAgentMessages,
4834
+ toolRegistry,
4835
+ onBackgroundComplete,
4836
+ onLog: (line) => wrappedOnEvent({
4837
+ type: "tool_input_delta",
4838
+ id: tc.id,
4839
+ name: tc.name,
4840
+ result: line
4841
+ })
4842
+ });
4843
+ }
4844
+ safeSettle(result, result.startsWith("Error"));
4845
+ } catch (err) {
4846
+ safeSettle(`Error: ${err.message}`, true);
4847
+ }
4848
+ };
4849
+ const entry = {
4850
+ id: tc.id,
4851
+ name: tc.name,
4852
+ input: tc.input,
4853
+ abortController: toolAbort,
4854
+ startedAt: toolStart,
4855
+ settle: safeSettle,
4856
+ rerun: (newInput) => {
4857
+ settled = false;
4858
+ toolAbort = new AbortController();
4859
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
4860
+ once: true
4670
4861
  });
4862
+ entry.abortController = toolAbort;
4863
+ entry.input = newInput;
4864
+ run(newInput);
4671
4865
  }
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
- }
4866
+ };
4867
+ toolRegistry?.register(entry);
4868
+ run(tc.input);
4869
+ const r = await resultPromise;
4870
+ toolRegistry?.unregister(tc.id);
4871
+ log.info("Tool completed", {
4872
+ name: tc.name,
4873
+ elapsed: `${Date.now() - toolStart}ms`,
4874
+ isError: r.isError,
4875
+ resultLength: r.result.length
4876
+ });
4877
+ onEvent({
4878
+ type: "tool_done",
4879
+ id: tc.id,
4880
+ name: tc.name,
4881
+ result: r.result,
4882
+ isError: r.isError
4883
+ });
4884
+ return r;
4698
4885
  })
4699
4886
  );
4700
- toolStatusWatcher.stop();
4887
+ statusWatcher.stop();
4701
4888
  for (const r of results) {
4702
4889
  const block = contentBlocks.find(
4703
4890
  (b) => b.type === "tool" && b.id === r.id
@@ -4705,6 +4892,7 @@ async function runTurn(params) {
4705
4892
  if (block?.type === "tool") {
4706
4893
  block.result = r.result;
4707
4894
  block.isError = r.isError;
4895
+ block.completedAt = Date.now();
4708
4896
  const msgs = subAgentMessages.get(r.id);
4709
4897
  if (msgs) {
4710
4898
  block.subAgentMessages = msgs;
@@ -4751,7 +4939,7 @@ var init_agent = __esm({
4751
4939
  "runScenario",
4752
4940
  "runMethod",
4753
4941
  "browserCommand",
4754
- "setProjectName"
4942
+ "setProjectMetadata"
4755
4943
  ]);
4756
4944
  }
4757
4945
  });
@@ -5063,6 +5251,84 @@ var init_config = __esm({
5063
5251
  }
5064
5252
  });
5065
5253
 
5254
+ // src/toolRegistry.ts
5255
+ var ToolRegistry;
5256
+ var init_toolRegistry = __esm({
5257
+ "src/toolRegistry.ts"() {
5258
+ "use strict";
5259
+ ToolRegistry = class {
5260
+ entries = /* @__PURE__ */ new Map();
5261
+ onEvent;
5262
+ register(entry) {
5263
+ this.entries.set(entry.id, entry);
5264
+ }
5265
+ unregister(id) {
5266
+ this.entries.delete(id);
5267
+ }
5268
+ get(id) {
5269
+ return this.entries.get(id);
5270
+ }
5271
+ /**
5272
+ * Stop a running tool.
5273
+ *
5274
+ * - graceful: abort and settle with [INTERRUPTED] + partial result
5275
+ * - hard: abort and settle with a generic error
5276
+ *
5277
+ * Returns true if the tool was found and stopped.
5278
+ */
5279
+ stop(id, mode) {
5280
+ const entry = this.entries.get(id);
5281
+ if (!entry) {
5282
+ return false;
5283
+ }
5284
+ entry.abortController.abort(mode);
5285
+ if (mode === "graceful") {
5286
+ const partial = entry.getPartialResult?.() ?? "";
5287
+ const result = partial ? `[INTERRUPTED]
5288
+
5289
+ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
5290
+ entry.settle(result, false);
5291
+ } else {
5292
+ entry.settle("Error: tool was cancelled", true);
5293
+ }
5294
+ this.onEvent?.({
5295
+ type: "tool_stopped",
5296
+ id: entry.id,
5297
+ name: entry.name,
5298
+ mode,
5299
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
5300
+ });
5301
+ this.entries.delete(id);
5302
+ return true;
5303
+ }
5304
+ /**
5305
+ * Restart a running tool with the same or patched input.
5306
+ * The original controllable promise stays pending and settles
5307
+ * when the new execution finishes.
5308
+ *
5309
+ * Returns true if the tool was found and restarted.
5310
+ */
5311
+ restart(id, patchedInput) {
5312
+ const entry = this.entries.get(id);
5313
+ if (!entry) {
5314
+ return false;
5315
+ }
5316
+ entry.abortController.abort("restart");
5317
+ const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
5318
+ this.onEvent?.({
5319
+ type: "tool_restarted",
5320
+ id: entry.id,
5321
+ name: entry.name,
5322
+ input: newInput,
5323
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
5324
+ });
5325
+ entry.rerun(newInput);
5326
+ return true;
5327
+ }
5328
+ };
5329
+ }
5330
+ });
5331
+
5066
5332
  // src/headless.ts
5067
5333
  var headless_exports = {};
5068
5334
  __export(headless_exports, {
@@ -5134,6 +5400,55 @@ async function startHeadless(opts = {}) {
5134
5400
  const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
5135
5401
  const pendingTools = /* @__PURE__ */ new Map();
5136
5402
  const earlyResults = /* @__PURE__ */ new Map();
5403
+ const toolRegistry = new ToolRegistry();
5404
+ const backgroundQueue = [];
5405
+ function flushBackgroundQueue() {
5406
+ if (backgroundQueue.length === 0) {
5407
+ return;
5408
+ }
5409
+ const results = backgroundQueue.splice(0);
5410
+ const xmlParts = results.map(
5411
+ (r) => `<tool_result id="${r.toolCallId}" name="${r.name}">
5412
+ ${r.result}
5413
+ </tool_result>`
5414
+ ).join("\n\n");
5415
+ const message = `@@automated::background_results@@
5416
+ <background_results>
5417
+ ${xmlParts}
5418
+ </background_results>`;
5419
+ handleMessage({ action: "message", text: message }, void 0);
5420
+ }
5421
+ function onBackgroundComplete(toolCallId, name, result, subAgentMessages) {
5422
+ for (const msg of state.messages) {
5423
+ if (!Array.isArray(msg.content)) {
5424
+ continue;
5425
+ }
5426
+ for (const block of msg.content) {
5427
+ if (block.type === "tool" && block.id === toolCallId) {
5428
+ block.backgroundResult = result;
5429
+ block.completedAt = Date.now();
5430
+ if (subAgentMessages) {
5431
+ block.subAgentMessages = subAgentMessages;
5432
+ }
5433
+ }
5434
+ }
5435
+ }
5436
+ onEvent({
5437
+ type: "tool_background_complete",
5438
+ id: toolCallId,
5439
+ name,
5440
+ result
5441
+ });
5442
+ backgroundQueue.push({
5443
+ toolCallId,
5444
+ name,
5445
+ result,
5446
+ completedAt: Date.now()
5447
+ });
5448
+ if (!running) {
5449
+ flushBackgroundQueue();
5450
+ }
5451
+ }
5137
5452
  const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
5138
5453
  "promptUser",
5139
5454
  "confirmDestructiveAction",
@@ -5174,6 +5489,7 @@ async function startHeadless(opts = {}) {
5174
5489
  case "turn_done":
5175
5490
  completedEmitted = true;
5176
5491
  emit("completed", { success: true }, rid);
5492
+ setTimeout(() => flushBackgroundQueue(), 0);
5177
5493
  return;
5178
5494
  case "turn_cancelled":
5179
5495
  completedEmitted = true;
@@ -5220,6 +5536,7 @@ async function startHeadless(opts = {}) {
5220
5536
  name: e.name,
5221
5537
  input: e.input,
5222
5538
  ...e.partial && { partial: true },
5539
+ ...e.background && { background: true },
5223
5540
  ...e.parentToolId && { parentToolId: e.parentToolId }
5224
5541
  },
5225
5542
  rid
@@ -5238,14 +5555,58 @@ async function startHeadless(opts = {}) {
5238
5555
  rid
5239
5556
  );
5240
5557
  return;
5558
+ case "tool_background_complete":
5559
+ emit(
5560
+ "tool_background_complete",
5561
+ {
5562
+ id: e.id,
5563
+ name: e.name,
5564
+ result: e.result,
5565
+ ...e.parentToolId && { parentToolId: e.parentToolId }
5566
+ },
5567
+ rid
5568
+ );
5569
+ return;
5570
+ case "tool_stopped":
5571
+ emit(
5572
+ "tool_stopped",
5573
+ {
5574
+ id: e.id,
5575
+ name: e.name,
5576
+ mode: e.mode,
5577
+ ...e.parentToolId && { parentToolId: e.parentToolId }
5578
+ },
5579
+ rid
5580
+ );
5581
+ return;
5582
+ case "tool_restarted":
5583
+ emit(
5584
+ "tool_restarted",
5585
+ {
5586
+ id: e.id,
5587
+ name: e.name,
5588
+ input: e.input,
5589
+ ...e.parentToolId && { parentToolId: e.parentToolId }
5590
+ },
5591
+ rid
5592
+ );
5593
+ return;
5241
5594
  case "status":
5242
- emit("status", { message: e.message }, rid);
5595
+ emit(
5596
+ "status",
5597
+ {
5598
+ message: e.message,
5599
+ ...e.parentToolId && { parentToolId: e.parentToolId }
5600
+ },
5601
+ rid
5602
+ );
5243
5603
  return;
5244
5604
  case "error":
5245
5605
  emit("error", { error: e.error }, rid);
5246
5606
  return;
5247
5607
  }
5248
5608
  }
5609
+ toolRegistry.onEvent = onEvent;
5249
5610
  async function handleMessage(parsed, requestId) {
5250
5611
  if (running) {
5251
5612
  emit(
@@ -5273,6 +5634,7 @@ async function startHeadless(opts = {}) {
5273
5634
  }
5274
5635
  let userMessage = parsed.text ?? "";
5275
5636
  const isCommand = !!parsed.runCommand;
5637
+ const isHidden = isCommand || !!parsed.hidden;
5276
5638
  if (parsed.runCommand === "sync") {
5277
5639
  userMessage = loadActionPrompt("sync");
5278
5640
  } else if (parsed.runCommand === "publish") {
@@ -5297,7 +5659,9 @@ async function startHeadless(opts = {}) {
5297
5659
  signal: currentAbort.signal,
5298
5660
  onEvent,
5299
5661
  resolveExternalTool,
5300
- hidden: isCommand
5662
+ hidden: isHidden,
5663
+ toolRegistry,
5664
+ onBackgroundComplete
5301
5665
  });
5302
5666
  if (!completedEmitted) {
5303
5667
  emit(
@@ -5351,6 +5715,36 @@ async function startHeadless(opts = {}) {
5351
5715
  emit("completed", { success: true }, requestId);
5352
5716
  return;
5353
5717
  }
5718
+ if (action === "stop_tool") {
5719
+ const id = parsed.id;
5720
+ const mode = parsed.mode ?? "hard";
5721
+ const found = toolRegistry.stop(id, mode);
5722
+ if (found) {
5723
+ emit("completed", { success: true }, requestId);
5724
+ } else {
5725
+ emit(
5726
+ "completed",
5727
+ { success: false, error: "Tool not found" },
5728
+ requestId
5729
+ );
5730
+ }
5731
+ return;
5732
+ }
5733
+ if (action === "restart_tool") {
5734
+ const id = parsed.id;
5735
+ const patchedInput = parsed.input;
5736
+ const found = toolRegistry.restart(id, patchedInput);
5737
+ if (found) {
5738
+ emit("completed", { success: true }, requestId);
5739
+ } else {
5740
+ emit(
5741
+ "completed",
5742
+ { success: false, error: "Tool not found" },
5743
+ requestId
5744
+ );
5745
+ }
5746
+ return;
5747
+ }
5354
5748
  if (action === "message") {
5355
5749
  await handleMessage(parsed, requestId);
5356
5750
  return;
@@ -5385,6 +5779,7 @@ var init_headless = __esm({
5385
5779
  init_lsp();
5386
5780
  init_agent();
5387
5781
  init_session();
5782
+ init_toolRegistry();
5388
5783
  }
5389
5784
  });
5390
5785