@mindstudio-ai/remy 0.1.266 → 0.1.268

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
@@ -1043,7 +1043,6 @@ function extractFrontmatter(content) {
1043
1043
  var DEFAULT_MAX_LINES = 500;
1044
1044
  var readSpecTool = {
1045
1045
  definition: {
1046
- clearable: true,
1047
1046
  name: "readSpec",
1048
1047
  description: "Read a spec file from src/ with line numbers. Always read a spec file before editing it. Paths are relative to the project root and must start with src/ (e.g., src/app.md, src/interfaces/web.md).",
1049
1048
  inputSchema: {
@@ -1157,7 +1156,6 @@ function acquireFileLock(filePath) {
1157
1156
  // src/tools/spec/writeSpec.ts
1158
1157
  var writeSpecTool = {
1159
1158
  definition: {
1160
- clearable: true,
1161
1159
  name: "writeSpec",
1162
1160
  description: "Create a new spec file or completely overwrite an existing one in src/. Parent directories are created automatically. Use this for new spec files or full rewrites. For targeted changes to existing specs, use editSpec instead.",
1163
1161
  inputSchema: {
@@ -1295,7 +1293,6 @@ function formatOccurrenceError(count, lines, filePath) {
1295
1293
  // src/tools/spec/editSpec.ts
1296
1294
  var editSpecTool = {
1297
1295
  definition: {
1298
- clearable: true,
1299
1296
  name: "editSpec",
1300
1297
  description: "Make a targeted find/replace edit to a spec file (src/*.md). old_string must appear exactly once (minor indentation differences are handled automatically); set replace_all to true to replace every occurrence. Read the file with readSpec first so you match the exact text, and include the full enclosing structure rather than an inner fragment. The file's YAML frontmatter (the leading --- \u2026 --- block, which holds required fields like name) is protected \u2014 an edit that would remove or malform it is refused. For a full rewrite, use writeSpec.",
1301
1298
  inputSchema: {
@@ -1400,7 +1397,6 @@ import fs9 from "fs/promises";
1400
1397
  import path6 from "path";
1401
1398
  var listSpecFilesTool = {
1402
1399
  definition: {
1403
- clearable: false,
1404
1400
  name: "listSpecFiles",
1405
1401
  description: "List all files in the src/ directory (spec files, brand guidelines, interface specs, references). Use this to understand what spec files exist before reading or editing them.",
1406
1402
  inputSchema: {
@@ -1451,7 +1447,6 @@ async function listRecursive(dir) {
1451
1447
  // src/tools/spec/presentPublishPlan.ts
1452
1448
  var presentPublishPlanTool = {
1453
1449
  definition: {
1454
- clearable: false,
1455
1450
  name: "presentPublishPlan",
1456
1451
  description: "Present a publish changelog to the user for approval. Write a clear markdown summary of what changed since the last deploy. The user will see this in a full-screen view and can approve or dismiss. Call this BEFORE committing or pushing.",
1457
1452
  inputSchema: {
@@ -1476,7 +1471,6 @@ import fs10 from "fs/promises";
1476
1471
  var PLAN_FILE = ".remy-plan.md";
1477
1472
  var writePlanTool = {
1478
1473
  definition: {
1479
- clearable: false,
1480
1474
  name: "writePlan",
1481
1475
  description: "Write an implementation plan for user approval before making changes. Use this only for large, multi-step changes like new features, new interface types, or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. The plan is displayed standalone in the UI with approve/reject buttons attached. The plan body ends with its last substantive section \u2014 do not write any closing line addressed to the reader. No 'If this lands, I'll start building,' no 'Let me know what to adjust,' no 'Approve when ready,' no trailing horizontal rule with a sign-off. Wrap-up text belongs in your chat message alongside the plan, not in the plan file. If the user asks for revisions, call this tool again with updated content to overwrite the plan.",
1482
1476
  inputSchema: {
@@ -1507,7 +1501,6 @@ import fs11 from "fs/promises";
1507
1501
  var PLAN_FILE2 = ".remy-plan.md";
1508
1502
  var updatePlanStatusTool = {
1509
1503
  definition: {
1510
- clearable: false,
1511
1504
  name: "updatePlanStatus",
1512
1505
  description: 'Update the status of the current implementation plan. Use when the user approves or rejects the plan via chat (e.g. "looks good, go ahead" or "scrap it"). Approving sets the plan to active so you can begin implementation. Rejecting deletes the plan.',
1513
1506
  inputSchema: {
@@ -1549,7 +1542,6 @@ var updatePlanStatusTool = {
1549
1542
  // src/tools/common/setProjectOnboardingState.ts
1550
1543
  var setProjectOnboardingStateTool = {
1551
1544
  definition: {
1552
- clearable: false,
1553
1545
  name: "setProjectOnboardingState",
1554
1546
  description: "Advance the project onboarding state. Forward-only: building \u2192 buildComplete \u2192 onboardingFinished. Normally driven by automated actions \u2014 don't call this out of order during a normal build, or you'll skip stages the user hasn't experienced. Exception: if the project has been in `building` for a while, the build is clearly done (the user is iterating on a working app, deploying, etc.), and the user reports the editor seems stuck \u2014 disabled Preview/Spec/Code tabs, no reveal, etc. \u2014 call `setProjectOnboardingState({ state: 'buildComplete' })` to unstick them. `onboardingFinished` is always set by the frontend after the user dismisses the reveal; never call it yourself.",
1555
1547
  inputSchema: {
@@ -1572,7 +1564,6 @@ var setProjectOnboardingStateTool = {
1572
1564
  // src/tools/common/promptUser.ts
1573
1565
  var promptUserTool = {
1574
1566
  definition: {
1575
- clearable: false,
1576
1567
  name: "promptUser",
1577
1568
  description: 'Ask the user structured questions. Choose type first: "form" for structured intake (5+ questions, takes over screen), "inline" for quick clarifications or confirmations. Blocks until the user responds. Result contains `_dismissed: true` if the user dismisses without answering.',
1578
1569
  inputSchema: {
@@ -1704,7 +1695,6 @@ ${lines.join("\n")}`;
1704
1695
  // src/tools/common/confirmDestructiveAction.ts
1705
1696
  var confirmDestructiveActionTool = {
1706
1697
  definition: {
1707
- clearable: false,
1708
1698
  name: "confirmDestructiveAction",
1709
1699
  description: "Confirm a destructive or irreversible action with the user. Use for things like deleting data, resetting the database, or discarding draft work. Do not use after presentPublishPlan or writePlan (those already include approval). Do not use before onboarding state transitions.",
1710
1700
  inputSchema: {
@@ -1878,7 +1868,6 @@ function runCli(command, args, options) {
1878
1868
  // src/subagents/sdkConsultant/index.ts
1879
1869
  var askMindStudioSdkTool = {
1880
1870
  definition: {
1881
- clearable: false,
1882
1871
  name: "askMindStudioSdk",
1883
1872
  description: "@mindstudio-ai/agent backend SDK expert. Knows every backend action, AI model, connector, and configuration option. Returns architectural guidance and working code. Only covers the backend SDK (@mindstudio-ai/agent) \u2014 do NOT use for frontend/interface SDK questions (@mindstudio-ai/interface) like file uploads, auth, or client-side APIs. Describe what you want to build, not just what API method you need. Batch related questions into a single query.",
1884
1873
  inputSchema: {
@@ -2019,7 +2008,6 @@ function stripDollarKeys(envelope) {
2019
2008
  // src/tools/common/searchGoogle.ts
2020
2009
  var searchGoogleTool = {
2021
2010
  definition: {
2022
- clearable: false,
2023
2011
  name: "searchGoogle",
2024
2012
  description: "Search Google and return results. Use for research, finding documentation, looking up APIs, or any task where web search would help.",
2025
2013
  inputSchema: {
@@ -2050,7 +2038,6 @@ var searchGoogleTool = {
2050
2038
  // src/tools/common/setProjectMetadata.ts
2051
2039
  var setProjectMetadataTool = {
2052
2040
  definition: {
2053
- clearable: false,
2054
2041
  name: "setProjectMetadata",
2055
2042
  description: "Set project metadata. Can update any combination of: display name, short description, app icon, and Open Graph share image. Provide only the fields you want to change.",
2056
2043
  inputSchema: {
@@ -2082,8 +2069,13 @@ var setProjectMetadataTool = {
2082
2069
 
2083
2070
  // src/tools/common/compactConversation.ts
2084
2071
  var compactConversationTool = {
2072
+ backgroundOnly: true,
2073
+ // Silent: the completed summary is written onto the tool block for the UI
2074
+ // detail view only. The model must never receive it as a message — it gets
2075
+ // the summary the correct way, as the checkpoint prefix in every later API
2076
+ // call (cleanMessagesForApi).
2077
+ backgroundNotify: "silent",
2085
2078
  definition: {
2086
- clearable: false,
2087
2079
  name: "compactConversation",
2088
2080
  description: "Compact the conversation history by summarizing older messages into a checkpoint. The summary preserves key decisions, what was built, and the current state of the project, but drops the verbose tool results, diffs, and intermediate steps that are no longer useful. Runs in the background.",
2089
2081
  inputSchema: {
@@ -2095,11 +2087,32 @@ var compactConversationTool = {
2095
2087
  if (!context?.conversationMessages || !context.apiConfig) {
2096
2088
  return "Error: compaction requires execution context.";
2097
2089
  }
2090
+ const { toolCallId, onBackgroundComplete, toolRegistry } = context;
2098
2091
  triggerCompaction(
2099
2092
  { messages: context.conversationMessages },
2100
2093
  context.apiConfig,
2101
- { blocking: false, requestId: context.requestId }
2102
- ).catch(() => {
2094
+ // origin/toolCallId tell the headless listener a real block exists —
2095
+ // it must not synthesize a UI-only one for this compaction.
2096
+ {
2097
+ blocking: false,
2098
+ requestId: context.requestId,
2099
+ origin: "tool",
2100
+ toolCallId
2101
+ }
2102
+ ).then((summaries) => {
2103
+ onBackgroundComplete?.(
2104
+ toolCallId,
2105
+ "compactConversation",
2106
+ formatSummariesResult(summaries)
2107
+ );
2108
+ }).catch((err) => {
2109
+ onBackgroundComplete?.(
2110
+ toolCallId,
2111
+ "compactConversation",
2112
+ `Error: ${err.message || "Compaction failed"}`
2113
+ );
2114
+ }).finally(() => {
2115
+ toolRegistry?.unregister(toolCallId);
2103
2116
  });
2104
2117
  return "Compaction started in the background.";
2105
2118
  }
@@ -2108,7 +2121,6 @@ var compactConversationTool = {
2108
2121
  // src/tools/common/loadSkill.ts
2109
2122
  var loadSkillTool = {
2110
2123
  definition: {
2111
- clearable: true,
2112
2124
  name: "loadSkill",
2113
2125
  description: "Load the full reference for a platform capability that isn't in your system prompt \u2014 task agents, agent interfaces, voice interfaces, MCP interfaces, data sources. The available skills and the trigger for each are listed in <available_skills>. Load one before writing code in its area, not after: these are APIs where a plausible-looking guess is usually wrong. Calling this is cheap and expected \u2014 if you're unsure whether you need it, load it. Only covers the capabilities listed in the catalog; for backend SDK actions and model IDs use askMindStudioSdk.",
2114
2126
  inputSchema: {
@@ -2159,7 +2171,6 @@ function isBinary(buffer) {
2159
2171
  }
2160
2172
  var readFileTool = {
2161
2173
  definition: {
2162
- clearable: true,
2163
2174
  name: "readFile",
2164
2175
  description: "Read a file's contents with line numbers. Always read a file before editing it \u2014 never guess at contents. By default returns the first 500 lines, and at most 64KB \u2014 a file with very wide lines (a CSV, a minified bundle) comes back short of 500 lines, so read a narrower range or grep rather than paging through it. To read a specific range, pass startLine and endLine (1-indexed, inclusive) \u2014 e.g. to read lines 253\u2013343, pass startLine: 253, endLine: 343. To read the end of a file or log, pass tail (the number of lines from the end). Line numbers in the output correspond to what editFile expects. For a large file, locate the relevant section first (symbols or grep), then read just that range.",
2165
2176
  inputSchema: {
@@ -2263,7 +2274,6 @@ import fs14 from "fs/promises";
2263
2274
  import path7 from "path";
2264
2275
  var writeFileTool = {
2265
2276
  definition: {
2266
- clearable: true,
2267
2277
  name: "writeFile",
2268
2278
  description: "Create a new file or completely overwrite an existing one. Parent directories are created automatically. Use this for new files or full rewrites. For targeted changes to existing files, use editFile instead \u2014 it preserves the parts you don't want to change and avoids errors from forgetting to include unchanged code.",
2269
2279
  inputSchema: {
@@ -2329,7 +2339,6 @@ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
2329
2339
  import fs15 from "fs/promises";
2330
2340
  var editFileTool = {
2331
2341
  definition: {
2332
- clearable: true,
2333
2342
  name: "editFile",
2334
2343
  description: "Replace a string in a file. old_string must appear exactly once (minor indentation differences are handled automatically). Set replace_all to true to replace every occurrence at once. For bulk mechanical substitutions (renaming a variable, swapping colors), prefer replace_all. Always read the file first so you know the exact text to match. When editing nested structures (objects, function bodies, arrays, template literals), always include the full enclosing structure in old_string rather than just an inner fragment. Replacing a partial slice from the middle of nested code is the most common source of syntax errors.",
2335
2344
  inputSchema: {
@@ -2422,7 +2431,6 @@ var DEFAULT_MAX_LINES2 = 500;
2422
2431
  var MAX_OUTPUT_BYTES = 3e4;
2423
2432
  var bashTool = {
2424
2433
  definition: {
2425
- clearable: true,
2426
2434
  name: "bash",
2427
2435
  description: "Run a shell command and return stdout + stderr. 120-second timeout by default (configurable). Use for: npm install/build/test, git operations, tsc --noEmit, or any CLI tool. Prefer dedicated tools over bash when available (use grep instead of bash + rg, readFile instead of bash + cat). Output is truncated to 500 lines or 30KB, whichever comes first. If a command would emit a lot of data, narrow it down (grep, head/tail, --short flags) rather than reading everything.",
2428
2436
  inputSchema: {
@@ -2544,7 +2552,6 @@ function formatResults(stdout, max, mode) {
2544
2552
  }
2545
2553
  var grepTool = {
2546
2554
  definition: {
2547
- clearable: true,
2548
2555
  name: "grep",
2549
2556
  description: "Search file contents for a regex pattern. Returns matching lines with file paths and line numbers (default 50 results). Use this to find where something is used, locate function definitions, or search for patterns across the codebase. Set outputMode to 'count' for per-file match counts (like grep -c) or 'filesWithMatches' for just the file paths (like grep -l). Add context (like grep -C), or contextBefore/contextAfter (like grep -B/-A), to include surrounding lines. Set caseInsensitive (like grep -i) for a case-insensitive search. For finding a symbol's definition precisely, prefer the definition tool if LSP is available. Automatically excludes node_modules and .git.",
2550
2557
  inputSchema: {
@@ -2656,7 +2663,6 @@ import fg from "fast-glob";
2656
2663
  var DEFAULT_MAX2 = 200;
2657
2664
  var globTool = {
2658
2665
  definition: {
2659
- clearable: true,
2660
2666
  name: "glob",
2661
2667
  description: 'Find files matching a glob pattern. Returns matching file paths sorted alphabetically (default 200 results). Use this to discover project structure, find files by name or extension, or check if a file exists. Common patterns: "**/*.ts" (all TypeScript files), "src/**/*.tsx" (React components in src), "*.json" (root-level JSON files). Automatically excludes node_modules and .git.',
2662
2668
  inputSchema: {
@@ -2754,7 +2760,6 @@ async function formatFile(dirPath, name, indent) {
2754
2760
  }
2755
2761
  var listDirTool = {
2756
2762
  definition: {
2757
- clearable: true,
2758
2763
  name: "listDir",
2759
2764
  description: "List the contents of a directory with one level of subdirectory expansion. Shows file sizes and collapses single-child directory chains (a/b/c/ shown as one entry). Use this for a quick overview of a directory's structure. For finding files across the whole project, use glob instead.",
2760
2765
  inputSchema: {
@@ -2831,7 +2836,6 @@ var listDirTool = {
2831
2836
  // src/tools/code/editsFinished.ts
2832
2837
  var editsFinishedTool = {
2833
2838
  definition: {
2834
- clearable: false,
2835
2839
  name: "editsFinished",
2836
2840
  description: "Signal that file edits are complete. Call this after you finish writing/editing files so the live preview updates cleanly. The preview is paused while you edit to avoid showing broken intermediate states \u2014 this unpauses it. If you forget to call this, the preview updates when your turn ends.",
2837
2841
  inputSchema: {
@@ -2898,7 +2902,6 @@ async function lspRequest(endpoint, body) {
2898
2902
  // src/tools/code/lspDiagnostics.ts
2899
2903
  var lspDiagnosticsTool = {
2900
2904
  definition: {
2901
- clearable: true,
2902
2905
  name: "lspDiagnostics",
2903
2906
  description: "Get TypeScript diagnostics (type errors, warnings) for a file, with suggested fixes when available. Use this after editing a file to check for errors.",
2904
2907
  inputSchema: {
@@ -2947,7 +2950,6 @@ var lspDiagnosticsTool = {
2947
2950
  // src/tools/code/restartProcess.ts
2948
2951
  var restartProcessTool = {
2949
2952
  definition: {
2950
- clearable: false,
2951
2953
  name: "restartProcess",
2952
2954
  description: "Restart a managed sandbox process. Use this after running npm install or changing package.json to restart the dev server so it picks up new dependencies.",
2953
2955
  inputSchema: {
@@ -2974,7 +2976,6 @@ var restartProcessTool = {
2974
2976
  // src/tools/code/runScenario.ts
2975
2977
  var runScenarioTool = {
2976
2978
  definition: {
2977
- clearable: true,
2978
2979
  name: "runScenario",
2979
2980
  description: "Run a scenario to seed the dev database with test data. By default truncates all tables first, then executes the seed function and impersonates the scenario roles. Use skipTruncate to run the seed function against existing data without resetting. 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.",
2980
2981
  inputSchema: {
@@ -3000,7 +3001,6 @@ var runScenarioTool = {
3000
3001
  // src/tools/code/runMethod.ts
3001
3002
  var runMethodTool = {
3002
3003
  definition: {
3003
- clearable: true,
3004
3004
  name: "runMethod",
3005
3005
  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. Returns synchronously \u2014 no need to sleep before checking results.\n\nBy default methods run unauthenticated. If the method is auth-gated (calls `auth.requireRole()`, filters on `auth.userId`, etc.), pass `userId: "testUser"` to run as the default test user \u2014 no scenario setup required, no userId lookup.',
3006
3006
  inputSchema: {
@@ -3035,7 +3035,6 @@ var runMethodTool = {
3035
3035
  // src/tools/code/queryDatabase.ts
3036
3036
  var queryDatabaseTool = {
3037
3037
  definition: {
3038
- clearable: true,
3039
3038
  name: "queryDatabase",
3040
3039
  description: "Execute a raw SQL query against the dev database and return the results. Use for inspecting data and debugging issues.",
3041
3040
  inputSchema: {
@@ -3385,16 +3384,55 @@ function capToolResult(result) {
3385
3384
  }
3386
3385
 
3387
3386
  // src/statusWatcher.ts
3387
+ var INTERNAL_PAYLOAD_MARKERS = [
3388
+ "[USER CANCELLED]",
3389
+ "[INTERRUPTED]",
3390
+ "[INTERRUPTED - PARTIAL OUTPUT RETRIEVED]",
3391
+ "<background_results>",
3392
+ "<tool_result"
3393
+ ];
3394
+ function sanitizeStatusText(text) {
3395
+ if (!text) {
3396
+ return "";
3397
+ }
3398
+ for (const marker of INTERNAL_PAYLOAD_MARKERS) {
3399
+ if (text.includes(marker)) {
3400
+ return "";
3401
+ }
3402
+ }
3403
+ const truncIdx = text.indexOf("(tool result truncated at");
3404
+ return truncIdx === -1 ? text : text.slice(0, truncIdx);
3405
+ }
3388
3406
  function startStatusWatcher(config) {
3389
3407
  const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
3390
3408
  let inflight2 = false;
3391
3409
  let stopped = false;
3392
3410
  let pauseCount = 0;
3393
3411
  const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
3412
+ let consecutiveFailures = 0;
3413
+ let backoffMs = 0;
3414
+ let nextAllowedAt = 0;
3415
+ const MAX_CONSECUTIVE_FAILURES = 10;
3416
+ const MAX_BACKOFF_MS = 6e4;
3417
+ function recordFailure() {
3418
+ consecutiveFailures++;
3419
+ backoffMs = Math.min(
3420
+ backoffMs === 0 ? interval : backoffMs * 2,
3421
+ MAX_BACKOFF_MS
3422
+ );
3423
+ nextAllowedAt = Date.now() + backoffMs;
3424
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES && !stopped) {
3425
+ stopped = true;
3426
+ clearInterval(timer);
3427
+ }
3428
+ }
3394
3429
  async function tick() {
3395
3430
  if (stopped || signal?.aborted || inflight2 || pauseCount > 0) {
3396
3431
  return;
3397
3432
  }
3433
+ if (Date.now() < nextAllowedAt) {
3434
+ return;
3435
+ }
3398
3436
  inflight2 = true;
3399
3437
  try {
3400
3438
  const context = getContext();
@@ -3410,18 +3448,29 @@ function startStatusWatcher(config) {
3410
3448
  body: JSON.stringify({ appId: apiConfig.appId, context }),
3411
3449
  signal
3412
3450
  });
3413
- if (!res.ok || stopped) {
3451
+ if (stopped) {
3452
+ return;
3453
+ }
3454
+ if (!res.ok) {
3455
+ recordFailure();
3414
3456
  return;
3415
3457
  }
3416
3458
  const data = await res.json();
3417
3459
  if (!data.label) {
3460
+ recordFailure();
3418
3461
  return;
3419
3462
  }
3463
+ consecutiveFailures = 0;
3464
+ backoffMs = 0;
3465
+ nextAllowedAt = 0;
3420
3466
  if (pauseCount > 0 || stopped || signal?.aborted) {
3421
3467
  return;
3422
3468
  }
3423
3469
  onStatus(data.label);
3424
3470
  } catch {
3471
+ if (!stopped && !signal?.aborted) {
3472
+ recordFailure();
3473
+ }
3425
3474
  } finally {
3426
3475
  inflight2 = false;
3427
3476
  }
@@ -3518,7 +3567,12 @@ ${summaryBlock.text}
3518
3567
  }
3519
3568
  startIdx = checkpointIdx + 1;
3520
3569
  }
3521
- const messagesToProcess = fixOrphanedToolCalls(messages.slice(startIdx));
3570
+ const apiMessages = messages.slice(startIdx).filter(
3571
+ (msg) => !(msg.role === "assistant" && Array.isArray(msg.content) && msg.content.some(
3572
+ (b) => b.type === "tool" && b.uiOnly
3573
+ ))
3574
+ );
3575
+ const messagesToProcess = fixOrphanedToolCalls(apiMessages);
3522
3576
  const toolUseIds = /* @__PURE__ */ new Set();
3523
3577
  for (const msg of messagesToProcess) {
3524
3578
  if (msg.role === "assistant" && Array.isArray(msg.content)) {
@@ -3660,329 +3714,341 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3660
3714
  return { text: USER_CANCELLED_RESULT, messages: thisInvocation() };
3661
3715
  }
3662
3716
  let lastToolResult = "";
3663
- while (true) {
3664
- turns++;
3665
- if (signal?.aborted) {
3666
- return abortResult([]);
3667
- }
3668
- const iterStart = Date.now();
3669
- const contentBlocks = [];
3670
- let thinkingStartedAt = 0;
3671
- let lastThinkingRelatedStartedAt;
3672
- let stopReason = "end_turn";
3673
- let currentToolNames = "";
3674
- let lastUsage;
3675
- let lastProviderMetadata;
3676
- let lastModelId;
3677
- const statusWatcher = startStatusWatcher({
3678
- apiConfig,
3679
- getContext: () => {
3680
- const parts = [];
3681
- if (task) {
3682
- parts.push(`Task: ${task.slice(-200)}`);
3683
- }
3684
- const text = getPartialText(contentBlocks);
3685
- if (text) {
3686
- parts.push(`Assistant text: ${text.slice(-500)}`);
3687
- }
3688
- if (currentToolNames) {
3689
- parts.push(`Tool: ${currentToolNames}`);
3690
- }
3691
- if (lastToolResult) {
3692
- parts.push(`Tool result: ${lastToolResult.slice(-200)}`);
3693
- }
3694
- return parts.join("\n");
3695
- },
3696
- onStatus: (label) => emit({ type: "status", message: label }),
3697
- signal
3698
- });
3699
- try {
3700
- for await (const event of streamChatWithRetry(
3701
- {
3702
- ...apiConfig,
3703
- model,
3704
- requestId,
3705
- subAgentId,
3706
- system: fullSystem,
3707
- messages: cleanMessagesForApi(messages),
3708
- tools: tools2,
3709
- signal
3710
- },
3711
- {
3712
- onRetry: (attempt) => emit({
3713
- type: "status",
3714
- message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
3715
- })
3717
+ let watchedBlocks = [];
3718
+ let watchedToolNames = "";
3719
+ const statusWatcher = startStatusWatcher({
3720
+ apiConfig,
3721
+ getContext: () => {
3722
+ const parts = [];
3723
+ if (task) {
3724
+ parts.push(`Task: ${sanitizeStatusText(task).slice(-200)}`);
3725
+ }
3726
+ const text = getPartialText(watchedBlocks);
3727
+ if (text) {
3728
+ parts.push(`Assistant text: ${text.slice(-500)}`);
3729
+ }
3730
+ if (watchedToolNames) {
3731
+ parts.push(`Tool: ${watchedToolNames}`);
3732
+ }
3733
+ const toolResult = sanitizeStatusText(lastToolResult);
3734
+ if (toolResult) {
3735
+ parts.push(`Tool result: ${toolResult.slice(-200)}`);
3736
+ }
3737
+ return parts.join("\n");
3738
+ },
3739
+ onStatus: (label) => emit({ type: "status", message: label }),
3740
+ signal
3741
+ });
3742
+ try {
3743
+ while (true) {
3744
+ turns++;
3745
+ if (signal?.aborted) {
3746
+ return abortResult([]);
3747
+ }
3748
+ const iterStart = Date.now();
3749
+ const contentBlocks = [];
3750
+ let thinkingStartedAt = 0;
3751
+ let lastThinkingRelatedStartedAt;
3752
+ let stopReason = "end_turn";
3753
+ let lastUsage;
3754
+ let lastProviderMetadata;
3755
+ let lastModelId;
3756
+ watchedBlocks = contentBlocks;
3757
+ watchedToolNames = "";
3758
+ try {
3759
+ for await (const event of streamChatWithRetry(
3760
+ {
3761
+ ...apiConfig,
3762
+ model,
3763
+ requestId,
3764
+ subAgentId,
3765
+ system: fullSystem,
3766
+ messages: cleanMessagesForApi(messages),
3767
+ tools: tools2,
3768
+ signal
3769
+ },
3770
+ {
3771
+ onRetry: (attempt) => emit({
3772
+ type: "status",
3773
+ message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
3774
+ })
3775
+ }
3776
+ )) {
3777
+ if (signal?.aborted) {
3778
+ break;
3779
+ }
3780
+ switch (event.type) {
3781
+ case "text": {
3782
+ const lastBlock = contentBlocks.at(-1);
3783
+ if (lastBlock?.type === "text") {
3784
+ lastBlock.text += event.text;
3785
+ } else {
3786
+ contentBlocks.push({
3787
+ type: "text",
3788
+ text: event.text,
3789
+ startedAt: event.ts
3790
+ });
3791
+ }
3792
+ emit({ type: "text", text: event.text });
3793
+ break;
3794
+ }
3795
+ case "thinking":
3796
+ if (!thinkingStartedAt) {
3797
+ thinkingStartedAt = event.ts;
3798
+ }
3799
+ emit({ type: "thinking", text: event.text });
3800
+ break;
3801
+ case "thinking_complete":
3802
+ contentBlocks.push({
3803
+ type: "thinking",
3804
+ thinking: event.thinking,
3805
+ signature: event.signature,
3806
+ startedAt: thinkingStartedAt,
3807
+ completedAt: event.ts
3808
+ });
3809
+ lastThinkingRelatedStartedAt = thinkingStartedAt;
3810
+ thinkingStartedAt = 0;
3811
+ break;
3812
+ case "redacted_thinking_complete": {
3813
+ const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
3814
+ contentBlocks.push({
3815
+ type: "redacted_thinking",
3816
+ data: event.data,
3817
+ startedAt,
3818
+ completedAt: event.ts
3819
+ });
3820
+ lastThinkingRelatedStartedAt = startedAt;
3821
+ break;
3822
+ }
3823
+ case "tool_use":
3824
+ contentBlocks.push({
3825
+ type: "tool",
3826
+ id: event.id,
3827
+ name: event.name,
3828
+ input: event.input,
3829
+ startedAt: Date.now()
3830
+ });
3831
+ emit({
3832
+ type: "tool_start",
3833
+ id: event.id,
3834
+ name: event.name,
3835
+ input: event.input
3836
+ });
3837
+ break;
3838
+ case "done":
3839
+ stopReason = event.stopReason;
3840
+ lastUsage = {
3841
+ inputTokens: event.usage.inputTokens,
3842
+ outputTokens: event.usage.outputTokens,
3843
+ cacheCreationTokens: event.usage.cacheCreationTokens,
3844
+ cacheReadTokens: event.usage.cacheReadTokens,
3845
+ llmCalls: 1
3846
+ };
3847
+ lastProviderMetadata = event.providerMetadata;
3848
+ lastModelId = event.modelId;
3849
+ recordUsage({
3850
+ ts: Date.now(),
3851
+ requestId,
3852
+ agentName: subAgentId || "sub-agent",
3853
+ parentToolId,
3854
+ modelId: event.modelId,
3855
+ inputTokens: event.usage.inputTokens,
3856
+ outputTokens: event.usage.outputTokens,
3857
+ cacheCreationTokens: event.usage.cacheCreationTokens,
3858
+ cacheReadTokens: event.usage.cacheReadTokens,
3859
+ cost: nanoToDollars(event.cost),
3860
+ billingEvents: event.billingEvents,
3861
+ durationMs: Date.now() - iterStart,
3862
+ toolNames: contentBlocks.filter(
3863
+ (b) => b.type === "tool"
3864
+ ).map((b) => b.name)
3865
+ });
3866
+ break;
3867
+ case "error":
3868
+ statusWatcher.stop();
3869
+ return {
3870
+ text: `Error: ${event.error}`,
3871
+ messages: thisInvocation()
3872
+ };
3873
+ }
3716
3874
  }
3717
- )) {
3718
- if (signal?.aborted) {
3719
- break;
3875
+ } catch (err) {
3876
+ if (!signal?.aborted) {
3877
+ throw err;
3720
3878
  }
3721
- switch (event.type) {
3722
- case "text": {
3723
- const lastBlock = contentBlocks.at(-1);
3724
- if (lastBlock?.type === "text") {
3725
- lastBlock.text += event.text;
3726
- } else {
3727
- contentBlocks.push({
3728
- type: "text",
3729
- text: event.text,
3730
- startedAt: event.ts
3879
+ }
3880
+ if (signal?.aborted) {
3881
+ statusWatcher.stop();
3882
+ return abortResult(contentBlocks);
3883
+ }
3884
+ messages.push({
3885
+ role: "assistant",
3886
+ content: contentBlocks,
3887
+ ...lastUsage ? { usage: lastUsage } : {},
3888
+ ...lastProviderMetadata ? { providerMetadata: lastProviderMetadata } : {},
3889
+ model: lastModelId ?? model
3890
+ });
3891
+ const toolCalls = contentBlocks.filter(
3892
+ (b) => b.type === "tool"
3893
+ );
3894
+ if (stopReason !== "tool_use" || toolCalls.length === 0) {
3895
+ statusWatcher.stop();
3896
+ const text = getPartialText(contentBlocks);
3897
+ const hasArtifacts = Object.keys(artifacts).length > 0;
3898
+ return {
3899
+ text,
3900
+ messages: thisInvocation(),
3901
+ ...hasArtifacts ? { artifacts } : {}
3902
+ };
3903
+ }
3904
+ log7.info("Tools executing", {
3905
+ requestId,
3906
+ parentToolId,
3907
+ count: toolCalls.length,
3908
+ tools: toolCalls.map((tc) => tc.name)
3909
+ });
3910
+ watchedToolNames = toolCalls.map((tc) => tc.name).join(", ");
3911
+ const results = await Promise.all(
3912
+ toolCalls.map(async (tc) => {
3913
+ if (signal?.aborted) {
3914
+ return {
3915
+ id: tc.id,
3916
+ result: USER_CANCELLED_RESULT,
3917
+ isError: true
3918
+ };
3919
+ }
3920
+ let settle;
3921
+ const resultPromise = new Promise((res) => {
3922
+ settle = (result, isError) => res({ id: tc.id, result, isError });
3923
+ });
3924
+ let toolAbort = new AbortController();
3925
+ const cascadeAbort = () => toolAbort.abort();
3926
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
3927
+ let settled = false;
3928
+ const safeSettle = (result, isError) => {
3929
+ if (settled) {
3930
+ return;
3931
+ }
3932
+ settled = true;
3933
+ signal?.removeEventListener("abort", cascadeAbort);
3934
+ settle(result, isError);
3935
+ };
3936
+ const run2 = async (input) => {
3937
+ try {
3938
+ let result;
3939
+ if (externalTools.has(tc.name) && resolveExternalTool) {
3940
+ result = await resolveExternalTool(tc.id, tc.name, input);
3941
+ } else {
3942
+ const onLog = (line) => emit({
3943
+ type: "tool_input_delta",
3944
+ id: tc.id,
3945
+ name: tc.name,
3946
+ result: line
3947
+ });
3948
+ result = await executeTool2(
3949
+ tc.name,
3950
+ input,
3951
+ tc.id,
3952
+ onLog,
3953
+ subAgentMessages
3954
+ );
3955
+ }
3956
+ safeSettle(capToolResult(result), result.startsWith("Error"));
3957
+ } catch (err) {
3958
+ safeSettle(`Error: ${err.message}`, true);
3959
+ }
3960
+ };
3961
+ const entry = {
3962
+ id: tc.id,
3963
+ name: tc.name,
3964
+ input: tc.input,
3965
+ parentToolId,
3966
+ abortController: toolAbort,
3967
+ startedAt: Date.now(),
3968
+ settle: safeSettle,
3969
+ rerun: (newInput) => {
3970
+ settled = false;
3971
+ toolAbort = new AbortController();
3972
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
3973
+ once: true
3731
3974
  });
3975
+ entry.abortController = toolAbort;
3976
+ entry.input = newInput;
3977
+ run2(newInput);
3732
3978
  }
3733
- emit({ type: "text", text: event.text });
3734
- break;
3979
+ };
3980
+ toolRegistry?.register(entry);
3981
+ const toolStart = Date.now();
3982
+ run2(tc.input);
3983
+ const r = await resultPromise;
3984
+ toolRegistry?.unregister(tc.id);
3985
+ log7.info("Tool completed", {
3986
+ requestId,
3987
+ parentToolId,
3988
+ toolCallId: tc.id,
3989
+ name: tc.name,
3990
+ durationMs: Date.now() - toolStart,
3991
+ isError: r.isError
3992
+ });
3993
+ emit({
3994
+ type: "tool_done",
3995
+ id: tc.id,
3996
+ name: tc.name,
3997
+ result: r.result,
3998
+ isError: r.isError
3999
+ });
4000
+ return r;
4001
+ })
4002
+ );
4003
+ lastToolResult = results.at(-1)?.result ?? "";
4004
+ for (const r of results) {
4005
+ const block = contentBlocks.find(
4006
+ (b) => b.type === "tool" && b.id === r.id
4007
+ );
4008
+ if (block?.type === "tool") {
4009
+ block.result = r.result;
4010
+ block.isError = r.isError;
4011
+ block.completedAt = Date.now();
4012
+ const innerMsgs = subAgentMessages.get(r.id);
4013
+ if (innerMsgs) {
4014
+ block.subAgentMessages = innerMsgs;
3735
4015
  }
3736
- case "thinking":
3737
- if (!thinkingStartedAt) {
3738
- thinkingStartedAt = event.ts;
4016
+ if (captureArtifacts?.includes(block.name) && !r.isError) {
4017
+ try {
4018
+ artifacts[block.name] = JSON.parse(r.result);
4019
+ } catch {
3739
4020
  }
3740
- emit({ type: "thinking", text: event.text });
3741
- break;
3742
- case "thinking_complete":
3743
- contentBlocks.push({
3744
- type: "thinking",
3745
- thinking: event.thinking,
3746
- signature: event.signature,
3747
- startedAt: thinkingStartedAt,
3748
- completedAt: event.ts
3749
- });
3750
- lastThinkingRelatedStartedAt = thinkingStartedAt;
3751
- thinkingStartedAt = 0;
3752
- break;
3753
- case "redacted_thinking_complete": {
3754
- const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
3755
- contentBlocks.push({
3756
- type: "redacted_thinking",
3757
- data: event.data,
3758
- startedAt,
3759
- completedAt: event.ts
3760
- });
3761
- lastThinkingRelatedStartedAt = startedAt;
3762
- break;
3763
4021
  }
3764
- case "tool_use":
3765
- contentBlocks.push({
3766
- type: "tool",
3767
- id: event.id,
3768
- name: event.name,
3769
- input: event.input,
3770
- startedAt: Date.now()
3771
- });
3772
- emit({
3773
- type: "tool_start",
3774
- id: event.id,
3775
- name: event.name,
3776
- input: event.input
3777
- });
3778
- break;
3779
- case "done":
3780
- stopReason = event.stopReason;
3781
- lastUsage = {
3782
- inputTokens: event.usage.inputTokens,
3783
- outputTokens: event.usage.outputTokens,
3784
- cacheCreationTokens: event.usage.cacheCreationTokens,
3785
- cacheReadTokens: event.usage.cacheReadTokens,
3786
- llmCalls: 1
3787
- };
3788
- lastProviderMetadata = event.providerMetadata;
3789
- lastModelId = event.modelId;
3790
- recordUsage({
3791
- ts: Date.now(),
3792
- requestId,
3793
- agentName: subAgentId || "sub-agent",
3794
- parentToolId,
3795
- modelId: event.modelId,
3796
- inputTokens: event.usage.inputTokens,
3797
- outputTokens: event.usage.outputTokens,
3798
- cacheCreationTokens: event.usage.cacheCreationTokens,
3799
- cacheReadTokens: event.usage.cacheReadTokens,
3800
- cost: nanoToDollars(event.cost),
3801
- billingEvents: event.billingEvents,
3802
- durationMs: Date.now() - iterStart,
3803
- toolNames: contentBlocks.filter(
3804
- (b) => b.type === "tool"
3805
- ).map((b) => b.name)
3806
- });
3807
- break;
3808
- case "error":
3809
- return {
3810
- text: `Error: ${event.error}`,
3811
- messages: thisInvocation()
3812
- };
3813
4022
  }
3814
- }
3815
- } catch (err) {
3816
- if (!signal?.aborted) {
3817
- throw err;
4023
+ messages.push({
4024
+ role: "user",
4025
+ content: r.result,
4026
+ toolCallId: r.id,
4027
+ isToolError: r.isError
4028
+ });
3818
4029
  }
3819
4030
  }
3820
- if (signal?.aborted) {
3821
- statusWatcher.stop();
3822
- return abortResult(contentBlocks);
3823
- }
3824
- messages.push({
3825
- role: "assistant",
3826
- content: contentBlocks,
3827
- ...lastUsage ? { usage: lastUsage } : {},
3828
- ...lastProviderMetadata ? { providerMetadata: lastProviderMetadata } : {},
3829
- model: lastModelId ?? model
4031
+ } finally {
4032
+ statusWatcher.stop();
4033
+ }
4034
+ };
4035
+ const wrapRun = async () => {
4036
+ try {
4037
+ const result = await run();
4038
+ log7.info("Sub-agent complete", {
4039
+ requestId,
4040
+ parentToolId,
4041
+ agentName,
4042
+ durationMs: Date.now() - runStart,
4043
+ turns
3830
4044
  });
3831
- const toolCalls = contentBlocks.filter(
3832
- (b) => b.type === "tool"
3833
- );
3834
- if (stopReason !== "tool_use" || toolCalls.length === 0) {
3835
- statusWatcher.stop();
3836
- const text = getPartialText(contentBlocks);
3837
- const hasArtifacts = Object.keys(artifacts).length > 0;
3838
- return {
3839
- text,
3840
- messages: thisInvocation(),
3841
- ...hasArtifacts ? { artifacts } : {}
3842
- };
3843
- }
3844
- log7.info("Tools executing", {
4045
+ return result;
4046
+ } catch (err) {
4047
+ log7.warn("Sub-agent error", {
3845
4048
  requestId,
3846
4049
  parentToolId,
3847
- count: toolCalls.length,
3848
- tools: toolCalls.map((tc) => tc.name)
3849
- });
3850
- currentToolNames = toolCalls.map((tc) => tc.name).join(", ");
3851
- const results = await Promise.all(
3852
- toolCalls.map(async (tc) => {
3853
- if (signal?.aborted) {
3854
- return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
3855
- }
3856
- let settle;
3857
- const resultPromise = new Promise((res) => {
3858
- settle = (result, isError) => res({ id: tc.id, result, isError });
3859
- });
3860
- let toolAbort = new AbortController();
3861
- const cascadeAbort = () => toolAbort.abort();
3862
- signal?.addEventListener("abort", cascadeAbort, { once: true });
3863
- let settled = false;
3864
- const safeSettle = (result, isError) => {
3865
- if (settled) {
3866
- return;
3867
- }
3868
- settled = true;
3869
- signal?.removeEventListener("abort", cascadeAbort);
3870
- settle(result, isError);
3871
- };
3872
- const run2 = async (input) => {
3873
- try {
3874
- let result;
3875
- if (externalTools.has(tc.name) && resolveExternalTool) {
3876
- result = await resolveExternalTool(tc.id, tc.name, input);
3877
- } else {
3878
- const onLog = (line) => emit({
3879
- type: "tool_input_delta",
3880
- id: tc.id,
3881
- name: tc.name,
3882
- result: line
3883
- });
3884
- result = await executeTool2(
3885
- tc.name,
3886
- input,
3887
- tc.id,
3888
- onLog,
3889
- subAgentMessages
3890
- );
3891
- }
3892
- safeSettle(capToolResult(result), result.startsWith("Error"));
3893
- } catch (err) {
3894
- safeSettle(`Error: ${err.message}`, true);
3895
- }
3896
- };
3897
- const entry = {
3898
- id: tc.id,
3899
- name: tc.name,
3900
- input: tc.input,
3901
- parentToolId,
3902
- abortController: toolAbort,
3903
- startedAt: Date.now(),
3904
- settle: safeSettle,
3905
- rerun: (newInput) => {
3906
- settled = false;
3907
- toolAbort = new AbortController();
3908
- signal?.addEventListener("abort", () => toolAbort.abort(), {
3909
- once: true
3910
- });
3911
- entry.abortController = toolAbort;
3912
- entry.input = newInput;
3913
- run2(newInput);
3914
- }
3915
- };
3916
- toolRegistry?.register(entry);
3917
- const toolStart = Date.now();
3918
- run2(tc.input);
3919
- const r = await resultPromise;
3920
- toolRegistry?.unregister(tc.id);
3921
- log7.info("Tool completed", {
3922
- requestId,
3923
- parentToolId,
3924
- toolCallId: tc.id,
3925
- name: tc.name,
3926
- durationMs: Date.now() - toolStart,
3927
- isError: r.isError
3928
- });
3929
- emit({
3930
- type: "tool_done",
3931
- id: tc.id,
3932
- name: tc.name,
3933
- result: r.result,
3934
- isError: r.isError
3935
- });
3936
- return r;
3937
- })
3938
- );
3939
- statusWatcher.stop();
3940
- lastToolResult = results.at(-1)?.result ?? "";
3941
- for (const r of results) {
3942
- const block = contentBlocks.find(
3943
- (b) => b.type === "tool" && b.id === r.id
3944
- );
3945
- if (block?.type === "tool") {
3946
- block.result = r.result;
3947
- block.isError = r.isError;
3948
- block.completedAt = Date.now();
3949
- const innerMsgs = subAgentMessages.get(r.id);
3950
- if (innerMsgs) {
3951
- block.subAgentMessages = innerMsgs;
3952
- }
3953
- if (captureArtifacts?.includes(block.name) && !r.isError) {
3954
- try {
3955
- artifacts[block.name] = JSON.parse(r.result);
3956
- } catch {
3957
- }
3958
- }
3959
- }
3960
- messages.push({
3961
- role: "user",
3962
- content: r.result,
3963
- toolCallId: r.id,
3964
- isToolError: r.isError
3965
- });
3966
- }
3967
- }
3968
- };
3969
- const wrapRun = async () => {
3970
- try {
3971
- const result = await run();
3972
- log7.info("Sub-agent complete", {
3973
- requestId,
3974
- parentToolId,
3975
- agentName,
3976
- durationMs: Date.now() - runStart,
3977
- turns
3978
- });
3979
- return result;
3980
- } catch (err) {
3981
- log7.warn("Sub-agent error", {
3982
- requestId,
3983
- parentToolId,
3984
- agentName,
3985
- error: err.message
4050
+ agentName,
4051
+ error: err.message
3986
4052
  });
3987
4053
  throw err;
3988
4054
  }
@@ -4040,7 +4106,6 @@ var COMMON_READ_TOOL_NAMES = new Set(
4040
4106
  // src/subagents/browserAutomation/tools.ts
4041
4107
  var BROWSER_TOOLS = [
4042
4108
  {
4043
- clearable: false,
4044
4109
  name: "setupBrowser",
4045
4110
  description: "Pre-authenticate the browser and optionally navigate to a starting page. Call this before interacting with authenticated content instead of manually logging in. Auth is optional \u2014 omit to just navigate without authenticating.",
4046
4111
  inputSchema: {
@@ -4073,7 +4138,6 @@ var BROWSER_TOOLS = [
4073
4138
  }
4074
4139
  },
4075
4140
  {
4076
- clearable: true,
4077
4141
  name: "browserCommand",
4078
4142
  description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Batches that contain an interactive step (click, type, select) also return a `recording` object \u2014 one chunk of a continuous per-session rrweb recording that the viewer stitches into a single replay (not a standalone per-call clip). Timeout: 120s.",
4079
4143
  inputSchema: {
@@ -4490,7 +4554,6 @@ async function runBrowserAutomation(task, context, opts) {
4490
4554
  }
4491
4555
  var browserAutomationTool = {
4492
4556
  definition: {
4493
- clearable: true,
4494
4557
  name: "runAutomatedBrowserTest",
4495
4558
  description: "Run an automated browser test against the live preview. Describe what to test \u2014 the agent figures out how. Use after meaningful changes to frontend code, to reproduce user-reported issues, or to test end-to-end flows. Never give it explicit values to use when filling out forms or creating accounts \u2014 it will use its own judgement (often it needs specific values to trigger dev-mode bypasses of things like login verification codes).",
4496
4559
  inputSchema: {
@@ -4520,7 +4583,6 @@ var browserAutomationTool = {
4520
4583
 
4521
4584
  // src/tools/code/screenshot.ts
4522
4585
  var screenshotDefinition = {
4523
- clearable: true,
4524
4586
  name: "screenshot",
4525
4587
  description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. The analysis is not precise about every detail \u2014 for example it cannot reliably identify specific fonts by name, only describe what the letterforms look like. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as `imageUrl` to skip recapture; `imageUrl` also accepts the disk path of an image file (a user upload, a saved asset) to analyze that instead of the preview. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To render a fixed-size image such as an Open Graph share card, set `width` and `height` (e.g. 1200 \xD7 630) and `format: 'png'`: the tool navigates to `path`, clips to exactly those pixel dimensions, and returns the image URL.",
4526
4588
  inputSchema: {
@@ -4626,7 +4688,6 @@ __export(searchGoogle_exports, {
4626
4688
  execute: () => execute
4627
4689
  });
4628
4690
  var definition = {
4629
- clearable: false,
4630
4691
  name: "searchGoogle",
4631
4692
  description: 'Search Google for web results. Reserch modern design trends in industries or verticals, "best [domain] apps 2026", ui patterns, or find something specific if the the user has an explicit reference. Searching for and reading case studies is a great way to get information and context about a project\'s domain. Prioritize authoritative sources like Figma and other design leaders, avoid random blog spam. Pick one or more URLs from the results and then use `scrapeWebUrl` to get their text content.',
4632
4693
  inputSchema: {
@@ -4659,7 +4720,6 @@ __export(scrapeWebUrl_exports, {
4659
4720
  execute: () => execute2
4660
4721
  });
4661
4722
  var definition2 = {
4662
- clearable: false,
4663
4723
  name: "scrapeWebUrl",
4664
4724
  description: "Fetch the content of a web page as markdown. Use when reading sites from search results or specific things the user wants to incorporate.",
4665
4725
  inputSchema: {
@@ -4717,7 +4777,6 @@ Identify the specific design moves that make this page interesting and unique, d
4717
4777
  Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.
4718
4778
  `;
4719
4779
  var definition3 = {
4720
- clearable: false,
4721
4780
  name: "analyzeDesign",
4722
4781
  description: "Analyze the visual design of a website, an image URL, or an image file on disk. Websites are automatically screenshotted first. Provides static image analysis only, will not capture animations or video. If no prompt is provided, performs a full design reference analysis (mood, color, typography, layout, distinctiveness). Provide a custom prompt to ask a specific design question instead. Use a bulleted list to ask many questions at once.",
4723
4782
  inputSchema: {
@@ -4782,7 +4841,6 @@ __export(analyzeImage_exports, {
4782
4841
  execute: () => execute4
4783
4842
  });
4784
4843
  var definition4 = {
4785
- clearable: true,
4786
4844
  name: "analyzeImage",
4787
4845
  description: "Analyze an image using a vision model. Provides static image analysis only, will not capture animations or video. 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. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. If you are analyzing a screenshot of the app preview, you can reuse the same screenshot URL multiple times to ask multiple questions.",
4788
4846
  inputSchema: {
@@ -5016,7 +5074,6 @@ async function generateImageAssets(opts) {
5016
5074
 
5017
5075
  // src/subagents/designExpert/tools/images/generateImages.ts
5018
5076
  var definition5 = {
5019
- clearable: false,
5020
5077
  name: "generateImages",
5021
5078
  description: "Generate images. Returns CDN URLs with a quality analysis for each image. Produces high-quality results for everything from photorealistic images and abstract/creative visuals. Pass multiple prompts to generate in parallel. No need to analyze images separately after generating \u2014 the analysis is included.",
5022
5079
  inputSchema: {
@@ -5084,7 +5141,6 @@ __export(editImages_exports, {
5084
5141
  execute: () => execute6
5085
5142
  });
5086
5143
  var definition6 = {
5087
- clearable: false,
5088
5144
  name: "editImages",
5089
5145
  description: "Edit or transform existing images. Provide one or more source image URLs as reference and a prompt describing the desired edit. Use for compositing, style transfer, subject transformation, blending multiple references, or incorporating one or more references into something new. Returns CDN URLs with analysis.",
5090
5146
  inputSchema: {
@@ -5162,7 +5218,6 @@ var COPY_EDITOR_TOOLS = [...COMMON_READ_TOOLS];
5162
5218
  var BASE_PROMPT2 = readAsset("subagents/copyEditor", "prompt.md");
5163
5219
  var copyEditorTool = {
5164
5220
  definition: {
5165
- clearable: false,
5166
5221
  name: "copyEditor",
5167
5222
  description: "Hand it user-facing copy and it hands back a sharper version \u2014 better structured for its audience and free of the overused words, telltale constructions, and rhythms that make text read as AI-generated. Think of it as a design expert for words: it elevates how the copy communicates and strips the AI fingerprints, but it never invents facts or claims you didn't give it. Use it on anything users will read: in-app strings, empty states, errors, the Build Overview, deck copy, launch posts, Slack announcements. Readonly.",
5168
5223
  inputSchema: {
@@ -5210,7 +5265,6 @@ var copyEditorTool = {
5210
5265
 
5211
5266
  // src/subagents/designExpert/tools/polishCopy.ts
5212
5267
  var definition7 = {
5213
- clearable: false,
5214
5268
  name: "polishCopy",
5215
5269
  description: "Hand off any user-facing copy you've written \u2014 headlines, captions, labels, body text \u2014 and get back a sharper version: better built for its audience and free of the fingerprints that make writing read as AI. It elevates how the copy communicates without inventing facts or claims you didn't give it. Give it the text plus what it's for (where it appears, the audience).",
5216
5270
  inputSchema: {
@@ -5562,7 +5616,6 @@ async function runDesignExpert(opts, context) {
5562
5616
  }
5563
5617
  var designExpertTool = {
5564
5618
  definition: {
5565
- clearable: false,
5566
5619
  name: "visualDesignExpert",
5567
5620
  description: DESCRIPTION,
5568
5621
  inputSchema: {
@@ -5770,7 +5823,6 @@ function getProductVisionPrompt() {
5770
5823
  // src/subagents/productVision/index.ts
5771
5824
  var productVisionTool = {
5772
5825
  definition: {
5773
- clearable: false,
5774
5826
  name: "productVision",
5775
5827
  description: "Owns the product roadmap. Reads spec and roadmap files automatically. Creates, updates, and deletes roadmap items in src/roadmap/. Describe the situation and what needs to happen.",
5776
5828
  inputSchema: {
@@ -5883,7 +5935,6 @@ var SANITY_CHECK_TOOLS = [
5883
5935
  var BASE_PROMPT4 = readAsset("subagents/codeSanityCheck", "prompt.md");
5884
5936
  var codeSanityCheckTool = {
5885
5937
  definition: {
5886
- clearable: false,
5887
5938
  name: "codeSanityCheck",
5888
5939
  description: 'Quick sanity check on an approach before building. Reviews architecture, package choices, and flags potential issues. Usually responds with "looks good." Occasionally catches something important. Readonly \u2014 can search the web and read code but cannot modify anything.',
5889
5940
  inputSchema: {
@@ -5929,6 +5980,121 @@ var codeSanityCheckTool = {
5929
5980
  }
5930
5981
  };
5931
5982
 
5983
+ // src/tools/spec/writeBuildOverview.ts
5984
+ import fs20 from "fs";
5985
+ var OVERVIEW_FILE = "src/overview.html";
5986
+ var DESIGN_BRIEF = `We are building the Build Overview for this app \u2014 the home page of its Spec tab. It is a calm, dense, one-page reference of everything the app actually contains, including the parts the user can't see. It renders flush inside the Spec tab's content panel (the IDE supplies the surrounding nav).
5987
+
5988
+ Take the plain-language copy in <overview_copy> and lay it out and skin it into a single, beautiful, self-contained HTML document in the app's own brand.
5989
+
5990
+ ### The single hard rule
5991
+ The copy in <overview_copy> is final \u2014 it was authored and edited before it reached you. Treat it as locked content to typeset, not a draft to improve. Reproduce the words exactly: do not rewrite, rephrase, shorten, expand, reorder, or "polish" them, and do not run them through any copy tool. This is the opposite of your usual role \u2014 here you own layout, typography, and visual design only, and the words (every number, name, label, claim, and sentence) are fixed. A single changed word or wrong number breaks this document's purpose.
5992
+
5993
+ ### What it is (and is not)
5994
+ - A typeset reference dossier: composed, dense, a little cool \u2014 substantial at a glance, then readable. Density communicates substance; sparse and airy reads as "not much here."
5995
+ - Brand-skinned: pull the palette, type system, and a single accent from the app's spec. The accent leads labels and figures; it is a mark, not a fill.
5996
+ - NOT a slide deck \u2014 no theatrics, no building to a climax, no persuasion.
5997
+ - NOT a docs site \u2014 no raised or shadowed cards, no hover-lift, no clickable-feeling surfaces, no sticky table of contents.
5998
+ - NOT a sparse memo \u2014 a restrained single-column page under-sells the work.
5999
+ - No header chrome and no footer: open straight on the app's logo and name so it sits flush with the IDE.
6000
+
6001
+ ### Constraints
6002
+ - A single self-contained HTML file. Fonts may load from a CDN; everything else (CSS, the logo SVG) is inline.
6003
+ - Responsive: fills the embedded panel width and collapses gracefully at narrow widths.`;
6004
+ var OVERVIEW_SHELL = `<!DOCTYPE html>
6005
+ <html lang="en">
6006
+ <head>
6007
+ <meta charset="utf-8">
6008
+ <!-- This renders inside an iframe in the IDE. Keep it a simple, self-contained
6009
+ static document \u2014 no scroll/zoom/accessibility scaffolding beyond this head.
6010
+ The user-scalable=no viewport is intentional; leave it. -->
6011
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
6012
+ <title>Build Overview</title>
6013
+ <!-- SKIN: link the app's fonts here (CDN \u2014 e.g. Fontshare / Google Fonts). -->
6014
+ <style>
6015
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
6016
+ </style>
6017
+ </head>
6018
+ <body>
6019
+ <!-- Compose the Build Overview here. Layout, sections, type, and styling are
6020
+ yours \u2014 keep everything inline and self-contained, brand-skinned from the
6021
+ app's spec. -->
6022
+ </body>
6023
+ </html>`;
6024
+ function initialDelivery() {
6025
+ return `### Your deliverable
6026
+ Write the complete Build Overview to \`${OVERVIEW_FILE}\`. The file does not exist yet \u2014 start from this scaffold, which carries the technical hygiene for the iframed render context (keep the viewport meta and the reset). Layout, sections, type, and styling are yours to compose.
6027
+
6028
+ <overview_shell>
6029
+ ${OVERVIEW_SHELL}
6030
+ </overview_shell>
6031
+
6032
+ Then reply with a one-line summary of what you wrote.`;
6033
+ }
6034
+ function refreshDelivery() {
6035
+ return `### Your deliverable
6036
+ The Build Overview already exists at \`${OVERVIEW_FILE}\`. Read it, then update it to reflect <overview_copy>, preserving its established skin \u2014 change only what the copy changed.
6037
+
6038
+ Then reply with a one-line summary of what you changed.`;
6039
+ }
6040
+ async function renderBuildOverview(content, context, opts) {
6041
+ if (!content) {
6042
+ return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
6043
+ }
6044
+ const exists = fs20.existsSync(OVERVIEW_FILE);
6045
+ const task = `<overview_copy>${content}</overview_copy>
6046
+
6047
+ ${DESIGN_BRIEF}
6048
+
6049
+ ${exists ? refreshDelivery() : initialDelivery()}`;
6050
+ try {
6051
+ if (opts.background) {
6052
+ const result2 = await runDesignExpertRender(
6053
+ { task, background: true, reportingName: "writeBuildOverview" },
6054
+ context
6055
+ );
6056
+ context.subAgentMessages?.set(context.toolCallId, result2.messages);
6057
+ return result2.text;
6058
+ }
6059
+ const result = await runDesignExpertRender({ task }, context);
6060
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
6061
+ if (!fs20.existsSync(OVERVIEW_FILE)) {
6062
+ return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
6063
+ ${result.text}`;
6064
+ }
6065
+ return `Build overview written to ${OVERVIEW_FILE}. ${result.text}`;
6066
+ } catch (err) {
6067
+ return `Error generating build overview: ${err.message}`;
6068
+ }
6069
+ }
6070
+ var buildOverviewTool = {
6071
+ definition: {
6072
+ name: "writeBuildOverview",
6073
+ description: "Generate or refresh the Build Overview \u2014 the project's home page in the Spec tab: a single-page, plain-language reference of everything the app actually contains, including the parts the user can't see (data stores, backend operations, access and roles, background jobs, seeded scenarios, the design system). You author the full copy: read the manifest and spec and state, plainly and exactly, what genuinely exists \u2014 real names and accurate counts \u2014 in calm, declarative, present-tense outcome language, with no persuasion or hype. Describe only what exists. Pass the complete copy as `content`; the design expert lays it out and skins it to the app's brand using your copy verbatim \u2014 it typesets your words, it does not rewrite them, so polish the copy before you pass it. Generate it at the end of a build; routine refreshes ride specSync's `refreshBuildOverview` flag instead, so call this directly only for the initial generation or when the user explicitly asks.",
6074
+ inputSchema: {
6075
+ type: "object",
6076
+ properties: {
6077
+ content: {
6078
+ type: "string",
6079
+ description: "The full Build Overview copy you authored: everything the app contains, in plain present-tense outcome language, with real names and exact counts. The design expert lays this out and skins it to the brand verbatim \u2014 it styles your words, it does not rewrite them \u2014 so this should be the final copy."
6080
+ }
6081
+ },
6082
+ required: ["content"]
6083
+ }
6084
+ },
6085
+ async execute(input, context) {
6086
+ if (!context) {
6087
+ return "Error: writeBuildOverview requires execution context for design expert delegation";
6088
+ }
6089
+ const content = (input.content ?? "").trim();
6090
+ const background = Boolean(content) && fs20.existsSync(OVERVIEW_FILE);
6091
+ if (background) {
6092
+ input.background = true;
6093
+ }
6094
+ return renderBuildOverview(content, context, { background });
6095
+ }
6096
+ };
6097
+
5932
6098
  // src/subagents/specSync/tools.ts
5933
6099
  var SPEC_SYNC_TOOLS = [
5934
6100
  ...COMMON_READ_TOOLS,
@@ -5963,15 +6129,18 @@ var specSyncTool = {
5963
6129
  // next real turn as a hidden background_results note instead.
5964
6130
  backgroundNotify: "passive",
5965
6131
  definition: {
5966
- clearable: false,
5967
6132
  name: "specSync",
5968
- description: "Reconcile the spec to bring it in line with code changes you have made. Provide a brief, bulleted list of what changed and why; it finds the affected spec sections and updates them to match. Always runs in the background and completes silently \u2014 do not wait for it; its outcome appears as an automated note at the start of a later turn.",
6133
+ description: "Reconcile the spec to bring it in line with code changes you have made. Provide a brief, bulleted list of what changed and why; it finds the affected spec sections and updates them to match. Set `refreshBuildOverview` after a deploy or a large milestone to also re-author the Build Overview from the updated spec. Always runs in the background and completes silently \u2014 do not wait for it; its outcome appears as an automated note at the start of a later turn.",
5969
6134
  inputSchema: {
5970
6135
  type: "object",
5971
6136
  properties: {
5972
6137
  task: {
5973
6138
  type: "string",
5974
6139
  description: "What you changed in the code and why, in plain language \u2014 the way you would explain it to a teammate. The specialist reads the spec itself and decides which sections to update; you do not need to name files or spec locations."
6140
+ },
6141
+ refreshBuildOverview: {
6142
+ type: "boolean",
6143
+ description: "Also refresh the Build Overview (src/overview.html) after reconciling \u2014 the specialist authors fresh copy from the updated spec and re-renders the page. Set this after a deploy or a large milestone; leave it off for routine syncs."
5975
6144
  }
5976
6145
  },
5977
6146
  required: ["task"]
@@ -5995,12 +6164,27 @@ var specSyncTool = {
5995
6164
  parts.push(specIndex);
5996
6165
  }
5997
6166
  const system = parts.join("\n\n");
6167
+ const refreshOverview = input.refreshBuildOverview === true;
6168
+ const tools2 = refreshOverview ? [...SPEC_SYNC_TOOLS, buildOverviewTool.definition] : SPEC_SYNC_TOOLS;
6169
+ const task = refreshOverview ? `${input.task}
6170
+
6171
+ After reconciling the spec, refresh the Build Overview: author the complete updated copy from the freshly-reconciled spec (follow the writeBuildOverview tool description) and call \`writeBuildOverview\` once with the final copy.` : input.task;
5998
6172
  const result = await runSubAgent({
5999
6173
  system,
6000
- task: input.task,
6001
- tools: SPEC_SYNC_TOOLS,
6174
+ task,
6175
+ tools: tools2,
6002
6176
  externalTools: /* @__PURE__ */ new Set(),
6003
- executeTool: (name, toolInput) => executeTool(name, toolInput, context),
6177
+ executeTool: (name, toolInput, toolCallId, _onLog, sams) => {
6178
+ if (name === "writeBuildOverview") {
6179
+ const childCtx = toolCallId ? { ...deriveContext(context, toolCallId), subAgentMessages: sams } : { ...context, subAgentMessages: sams };
6180
+ return renderBuildOverview(
6181
+ String(toolInput.content ?? "").trim(),
6182
+ childCtx,
6183
+ { background: false }
6184
+ );
6185
+ }
6186
+ return executeTool(name, toolInput, context);
6187
+ },
6004
6188
  apiConfig: context.apiConfig,
6005
6189
  model: resolveModel("specSync", context.models, context.model),
6006
6190
  subAgentId: "specSync",
@@ -6031,7 +6215,6 @@ var specSyncTool = {
6031
6215
  // src/tools/common/scrapeWebUrl.ts
6032
6216
  var scrapeWebUrlTool = {
6033
6217
  definition: {
6034
- clearable: false,
6035
6218
  name: "scrapeWebUrl",
6036
6219
  description: "Scrape the content of a web page. Returns the HTML of the page as markdown text. Optionally capture a screenshot if you need see the visual design. Use this when you need to fetch or analyze content from a website",
6037
6220
  inputSchema: {
@@ -6073,119 +6256,9 @@ var scrapeWebUrlTool = {
6073
6256
  }
6074
6257
  };
6075
6258
 
6076
- // src/tools/spec/writeBuildOverview.ts
6077
- import fs20 from "fs";
6078
- var OVERVIEW_FILE = "src/overview.html";
6079
- var DESIGN_BRIEF = `We are building the Build Overview for this app \u2014 the home page of its Spec tab. It is a calm, dense, one-page reference of everything the app actually contains, including the parts the user can't see. It renders flush inside the Spec tab's content panel (the IDE supplies the surrounding nav).
6080
-
6081
- Take the plain-language copy in <overview_copy> and lay it out and skin it into a single, beautiful, self-contained HTML document in the app's own brand.
6082
-
6083
- ### The single hard rule
6084
- The copy in <overview_copy> is final \u2014 it was authored and edited before it reached you. Treat it as locked content to typeset, not a draft to improve. Reproduce the words exactly: do not rewrite, rephrase, shorten, expand, reorder, or "polish" them, and do not run them through any copy tool. This is the opposite of your usual role \u2014 here you own layout, typography, and visual design only, and the words (every number, name, label, claim, and sentence) are fixed. A single changed word or wrong number breaks this document's purpose.
6085
-
6086
- ### What it is (and is not)
6087
- - A typeset reference dossier: composed, dense, a little cool \u2014 substantial at a glance, then readable. Density communicates substance; sparse and airy reads as "not much here."
6088
- - Brand-skinned: pull the palette, type system, and a single accent from the app's spec. The accent leads labels and figures; it is a mark, not a fill.
6089
- - NOT a slide deck \u2014 no theatrics, no building to a climax, no persuasion.
6090
- - NOT a docs site \u2014 no raised or shadowed cards, no hover-lift, no clickable-feeling surfaces, no sticky table of contents.
6091
- - NOT a sparse memo \u2014 a restrained single-column page under-sells the work.
6092
- - No header chrome and no footer: open straight on the app's logo and name so it sits flush with the IDE.
6093
-
6094
- ### Constraints
6095
- - A single self-contained HTML file. Fonts may load from a CDN; everything else (CSS, the logo SVG) is inline.
6096
- - Responsive: fills the embedded panel width and collapses gracefully at narrow widths.`;
6097
- var OVERVIEW_SHELL = `<!DOCTYPE html>
6098
- <html lang="en">
6099
- <head>
6100
- <meta charset="utf-8">
6101
- <!-- This renders inside an iframe in the IDE. Keep it a simple, self-contained
6102
- static document \u2014 no scroll/zoom/accessibility scaffolding beyond this head.
6103
- The user-scalable=no viewport is intentional; leave it. -->
6104
- <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
6105
- <title>Build Overview</title>
6106
- <!-- SKIN: link the app's fonts here (CDN \u2014 e.g. Fontshare / Google Fonts). -->
6107
- <style>
6108
- *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
6109
- </style>
6110
- </head>
6111
- <body>
6112
- <!-- Compose the Build Overview here. Layout, sections, type, and styling are
6113
- yours \u2014 keep everything inline and self-contained, brand-skinned from the
6114
- app's spec. -->
6115
- </body>
6116
- </html>`;
6117
- function initialDelivery() {
6118
- return `### Your deliverable
6119
- Write the complete Build Overview to \`${OVERVIEW_FILE}\`. The file does not exist yet \u2014 start from this scaffold, which carries the technical hygiene for the iframed render context (keep the viewport meta and the reset). Layout, sections, type, and styling are yours to compose.
6120
-
6121
- <overview_shell>
6122
- ${OVERVIEW_SHELL}
6123
- </overview_shell>
6124
-
6125
- Then reply with a one-line summary of what you wrote.`;
6126
- }
6127
- function refreshDelivery() {
6128
- return `### Your deliverable
6129
- The Build Overview already exists at \`${OVERVIEW_FILE}\`. Read it, then update it to reflect <overview_copy>, preserving its established skin \u2014 change only what the copy changed.
6130
-
6131
- Then reply with a one-line summary of what you changed.`;
6132
- }
6133
- var buildOverviewTool = {
6134
- definition: {
6135
- clearable: false,
6136
- name: "writeBuildOverview",
6137
- description: "Generate or refresh the Build Overview \u2014 the project's home page in the Spec tab: a single-page, plain-language reference of everything the app actually contains, including the parts the user can't see (data stores, backend operations, access and roles, background jobs, seeded scenarios, the design system). You author the full copy: read the manifest and spec and state, plainly and exactly, what genuinely exists \u2014 real names and accurate counts \u2014 in calm, declarative, present-tense outcome language, with no persuasion or hype. Describe only what exists. Pass the complete copy as `content`; the design expert lays it out and skins it to the app's brand using your copy verbatim \u2014 it typesets your words, it does not rewrite them, so polish the copy before you pass it. Generate it at the end of a build and refresh it after meaningful work.",
6138
- inputSchema: {
6139
- type: "object",
6140
- properties: {
6141
- content: {
6142
- type: "string",
6143
- description: "The full Build Overview copy you authored: everything the app contains, in plain present-tense outcome language, with real names and exact counts. The design expert lays this out and skins it to the brand verbatim \u2014 it styles your words, it does not rewrite them \u2014 so this should be the final copy."
6144
- }
6145
- },
6146
- required: ["content"]
6147
- }
6148
- },
6149
- async execute(input, context) {
6150
- if (!context) {
6151
- return "Error: writeBuildOverview requires execution context for design expert delegation";
6152
- }
6153
- const content = (input.content ?? "").trim();
6154
- if (!content) {
6155
- return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
6156
- }
6157
- const exists = fs20.existsSync(OVERVIEW_FILE);
6158
- const task = `<overview_copy>${content}</overview_copy>
6159
-
6160
- ${DESIGN_BRIEF}
6161
-
6162
- ${exists ? refreshDelivery() : initialDelivery()}`;
6163
- try {
6164
- if (exists) {
6165
- input.background = true;
6166
- const result2 = await runDesignExpertRender(
6167
- { task, background: true, reportingName: "writeBuildOverview" },
6168
- context
6169
- );
6170
- context.subAgentMessages?.set(context.toolCallId, result2.messages);
6171
- return result2.text;
6172
- }
6173
- const result = await runDesignExpertRender({ task }, context);
6174
- context.subAgentMessages?.set(context.toolCallId, result.messages);
6175
- if (!fs20.existsSync(OVERVIEW_FILE)) {
6176
- return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
6177
- ${result.text}`;
6178
- }
6179
- return `Build overview written to ${OVERVIEW_FILE}. ${result.text}`;
6180
- } catch (err) {
6181
- return `Error generating build overview: ${err.message}`;
6182
- }
6183
- }
6184
- };
6185
-
6186
- // src/tools/index.ts
6187
- function deriveContext(parent, toolCallId) {
6188
- return { ...parent, toolCallId };
6259
+ // src/tools/index.ts
6260
+ function deriveContext(parent, toolCallId) {
6261
+ return { ...parent, toolCallId };
6189
6262
  }
6190
6263
  var ALL_TOOLS = [
6191
6264
  // Common
@@ -6381,7 +6454,11 @@ function getConversationMessagesForSummary(messages, endIndex) {
6381
6454
  break;
6382
6455
  }
6383
6456
  }
6384
- return messages.slice(startIdx, endIndex);
6457
+ return messages.slice(startIdx, endIndex).filter(
6458
+ (msg) => !(Array.isArray(msg.content) && msg.content.some(
6459
+ (b) => b.type === "tool" && b.uiOnly
6460
+ ))
6461
+ );
6385
6462
  }
6386
6463
  function getSubAgentMessagesForSummary(messages, subAgentName, endIndex) {
6387
6464
  let checkpointIdx = -1;
@@ -6659,6 +6736,11 @@ import path12 from "path";
6659
6736
  var log10 = createLogger("session");
6660
6737
  var SESSION_FILE = ".remy-session.json";
6661
6738
  var ARCHIVE_DIR = ".logs/sessions";
6739
+ function writeFileAtomicSync(file, data) {
6740
+ const tmp = `${file}.tmp`;
6741
+ fs21.writeFileSync(tmp, data, "utf-8");
6742
+ fs21.renameSync(tmp, file);
6743
+ }
6662
6744
  var ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
6663
6745
  var RETAIN_TAIL_BYTES = 16 * 1024 * 1024;
6664
6746
  var ARCHIVE_RETENTION_BYTES = 64 * 1024 * 1024;
@@ -6758,7 +6840,7 @@ function archiveMessages(messages, label, models) {
6758
6840
  if (models && Object.keys(models).length > 0) {
6759
6841
  payload.models = models;
6760
6842
  }
6761
- fs21.writeFileSync(dest, JSON.stringify(payload), "utf-8");
6843
+ writeFileAtomicSync(dest, JSON.stringify(payload));
6762
6844
  archiveCountCache.set(path12.basename(dest), count);
6763
6845
  log10.info("Session archived", { label, dest, messageCount: count });
6764
6846
  pruneArchives();
@@ -6963,7 +7045,7 @@ function saveSession(state) {
6963
7045
  if (Buffer.byteLength(serialized, "utf-8") > ROTATE_THRESHOLD_BYTES && rotate(state)) {
6964
7046
  serialized = JSON.stringify(buildPayload(state));
6965
7047
  }
6966
- fs21.writeFileSync(SESSION_FILE, serialized, "utf-8");
7048
+ writeFileAtomicSync(SESSION_FILE, serialized);
6967
7049
  log10.info("Session saved", { messageCount: state.messages.length });
6968
7050
  } catch (err) {
6969
7051
  log10.warn("Session save failed", { error: err.message });
@@ -6993,6 +7075,28 @@ function clearSession(state) {
6993
7075
  var log11 = createLogger("compaction:trigger");
6994
7076
  var pending = null;
6995
7077
  var inflightCompaction = null;
7078
+ function getInflightCompaction() {
7079
+ return inflightCompaction;
7080
+ }
7081
+ function summariesOf(result) {
7082
+ const out = [];
7083
+ for (const msg of result.checkpoints) {
7084
+ if (!Array.isArray(msg.content)) {
7085
+ continue;
7086
+ }
7087
+ for (const block of msg.content) {
7088
+ if (block.type === "summary") {
7089
+ out.push({
7090
+ name: block.name,
7091
+ text: block.text,
7092
+ ...block.recent && { recent: block.recent },
7093
+ startedAt: block.startedAt
7094
+ });
7095
+ }
7096
+ }
7097
+ }
7098
+ return out;
7099
+ }
6996
7100
  function applyPendingSummaries(state) {
6997
7101
  const drained = pending;
6998
7102
  pending = null;
@@ -7013,6 +7117,25 @@ function applyPendingSummaries(state) {
7013
7117
  });
7014
7118
  saveSession(state);
7015
7119
  }
7120
+ var SUMMARY_SECTION_LABELS = {
7121
+ conversation: "Conversation",
7122
+ visualDesignExpert: "Design Agent Thread",
7123
+ productVision: "Roadmap Agent Thread"
7124
+ };
7125
+ function formatSummariesResult(summaries) {
7126
+ if (summaries === null) {
7127
+ return "A compaction checkpoint was already pending \u2014 no new compaction was needed.";
7128
+ }
7129
+ if (summaries.length === 0) {
7130
+ return "Nothing to compact \u2014 the conversation is already fully summarized.";
7131
+ }
7132
+ if (summaries.length === 1 && summaries[0].name === "conversation") {
7133
+ return summaries[0].text;
7134
+ }
7135
+ return summaries.map((s) => `## ${SUMMARY_SECTION_LABELS[s.name] ?? s.name}
7136
+
7137
+ ${s.text}`).join("\n\n");
7138
+ }
7016
7139
  var listener = null;
7017
7140
  function setCompactionListener(l) {
7018
7141
  listener = l;
@@ -7023,18 +7146,24 @@ function triggerCompaction(state, apiConfig, opts = {}) {
7023
7146
  }
7024
7147
  if (pending) {
7025
7148
  log11.info("Compaction skipped \u2014 a checkpoint is already waiting to apply");
7026
- return Promise.resolve();
7149
+ return Promise.resolve(null);
7027
7150
  }
7028
- const { blocking = false, requestId, model } = opts;
7029
- listener?.({ type: "started", blocking, requestId });
7151
+ const { blocking = false, requestId, model, origin, toolCallId } = opts;
7152
+ listener?.({ type: "started", blocking, requestId, origin, toolCallId });
7030
7153
  inflightCompaction = compactConversation(
7031
7154
  state.messages,
7032
7155
  apiConfig,
7033
7156
  resolveModel("conversationSummarizer", state.models, model)
7034
7157
  ).then((result) => {
7035
7158
  pending = result;
7036
- listener?.({ type: "complete", requestId });
7159
+ const summaries = summariesOf(result);
7160
+ listener?.({
7161
+ type: "complete",
7162
+ requestId,
7163
+ ...summaries.length > 0 && { summaries }
7164
+ });
7037
7165
  log11.info("Compaction complete");
7166
+ return summaries;
7038
7167
  }).catch((err) => {
7039
7168
  const message = err.message || "Compaction failed";
7040
7169
  listener?.({ type: "complete", error: message, requestId });
@@ -7554,6 +7683,62 @@ function parsePartialJson(jsonString) {
7554
7683
  return parseAny();
7555
7684
  }
7556
7685
 
7686
+ // src/automatedActions/resolve.ts
7687
+ var NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
7688
+ function resolveAction(text) {
7689
+ const parsed = parseSentinel(text);
7690
+ if (!parsed) {
7691
+ return null;
7692
+ }
7693
+ const { name: triggerName, remainder } = parsed;
7694
+ if (NON_ACTION_SENTINELS.has(triggerName)) {
7695
+ return null;
7696
+ }
7697
+ let params = {};
7698
+ if (remainder) {
7699
+ try {
7700
+ params = JSON.parse(remainder.split("\n")[0]);
7701
+ } catch {
7702
+ }
7703
+ }
7704
+ let body = readAsset("automatedActions", `${triggerName}.md`);
7705
+ let next;
7706
+ const fmMatch = body.match(/^---\s*\n([\s\S]*?)\n---/);
7707
+ if (fmMatch) {
7708
+ const nextMatch = fmMatch[1].match(/^\s*next:\s*(\w+)\s*$/m);
7709
+ if (nextMatch) {
7710
+ next = nextMatch[1];
7711
+ }
7712
+ }
7713
+ body = body.replace(/^---[\s\S]*?---\s*/, "");
7714
+ for (const [key, value] of Object.entries(params)) {
7715
+ const str = typeof value === "string" ? value : JSON.stringify(value);
7716
+ body = body.replaceAll(`{{${key}}}`, str);
7717
+ }
7718
+ return {
7719
+ message: automatedMessage(triggerName, body),
7720
+ next
7721
+ };
7722
+ }
7723
+ function getActionChain(startName) {
7724
+ const chain = [];
7725
+ const seen = /* @__PURE__ */ new Set();
7726
+ let name = startName;
7727
+ while (name && !seen.has(name)) {
7728
+ seen.add(name);
7729
+ chain.push(name);
7730
+ let body;
7731
+ try {
7732
+ body = readAsset("automatedActions", `${name}.md`);
7733
+ } catch {
7734
+ break;
7735
+ }
7736
+ const fm = body.match(/^---\s*\n([\s\S]*?)\n---/);
7737
+ name = fm?.[1].match(/^\s*next:\s*(\w+)\s*$/m)?.[1];
7738
+ }
7739
+ return chain;
7740
+ }
7741
+
7557
7742
  // src/errors.ts
7558
7743
  var patterns = [
7559
7744
  [
@@ -7705,6 +7890,9 @@ async function runTurn(params) {
7705
7890
  let lastCompletedTools = "";
7706
7891
  let lastCompletedInput = "";
7707
7892
  let lastCompletedResult = "";
7893
+ let subAgentText = "";
7894
+ let currentToolNames = "";
7895
+ let contentBlocksRef = [];
7708
7896
  let turnInputTokens = 0;
7709
7897
  let turnOutputTokens = 0;
7710
7898
  let turnCacheCreation = 0;
@@ -7713,281 +7901,328 @@ async function runTurn(params) {
7713
7901
  let lastCallInputTokens = 0;
7714
7902
  let lastCallCacheCreation = 0;
7715
7903
  let lastCallCacheRead = 0;
7716
- while (true) {
7717
- let getOrCreateAccumulator2 = function(id, name) {
7718
- let acc = toolInputAccumulators.get(id);
7719
- if (!acc) {
7720
- acc = { name, json: "", started: false, lastEmittedCount: 0 };
7721
- toolInputAccumulators.set(id, acc);
7722
- }
7723
- return acc;
7724
- };
7725
- var getOrCreateAccumulator = getOrCreateAccumulator2;
7726
- if (signal?.aborted) {
7727
- onEvent({ type: "turn_cancelled" });
7728
- saveSession(state);
7729
- return;
7730
- }
7731
- const iterStart = Date.now();
7732
- const contentBlocks = [];
7733
- const thinkingBlockStartTimes = [];
7734
- let thinkingCompleteCount = 0;
7735
- let lastThinkingRelatedStartedAt;
7736
- let textBlockOpen = false;
7737
- const toolInputAccumulators = /* @__PURE__ */ new Map();
7738
- let stopReason = "end_turn";
7739
- let turnProviderMetadata;
7740
- let turnModelId;
7741
- let subAgentText = "";
7742
- let currentToolNames = "";
7743
- const statusWatcher = isFirstMessage ? { stop() {
7744
- }, pause() {
7745
- }, resume() {
7746
- } } : startStatusWatcher({
7747
- apiConfig,
7748
- getContext: () => {
7749
- const parts = [];
7750
- const toolName = currentToolNames || getToolCalls(contentBlocks).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools;
7751
- if (toolName) {
7752
- parts.push(`Tool: ${toolName}`);
7753
- }
7754
- if (lastCompletedInput) {
7755
- parts.push(`Tool input: ${lastCompletedInput.slice(-1500)}`);
7756
- }
7757
- if (lastCompletedResult) {
7758
- parts.push(`Tool result: ${lastCompletedResult.slice(-1500)}`);
7759
- }
7760
- const text = subAgentText || getTextContent(contentBlocks).slice(-2e3);
7761
- if (text) {
7762
- parts.push(`Assistant text: ${text}`);
7763
- }
7764
- if (onboardingState && onboardingState !== "onboardingFinished") {
7765
- parts.push(`Build phase: ${onboardingState}`);
7904
+ const statusWatcher = isFirstMessage ? { stop() {
7905
+ }, pause() {
7906
+ }, resume() {
7907
+ } } : startStatusWatcher({
7908
+ apiConfig,
7909
+ getContext: () => {
7910
+ const parts = [];
7911
+ const toolName = currentToolNames || getToolCalls(contentBlocksRef).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools;
7912
+ if (toolName) {
7913
+ parts.push(`Tool: ${toolName}`);
7914
+ }
7915
+ const toolInput = sanitizeStatusText(lastCompletedInput);
7916
+ if (toolInput) {
7917
+ parts.push(`Tool input: ${toolInput.slice(-1500)}`);
7918
+ }
7919
+ const toolResult = sanitizeStatusText(lastCompletedResult);
7920
+ if (toolResult) {
7921
+ parts.push(`Tool result: ${toolResult.slice(-1500)}`);
7922
+ }
7923
+ const text = subAgentText || getTextContent(contentBlocksRef).slice(-2e3);
7924
+ if (text) {
7925
+ parts.push(`Assistant text: ${text}`);
7926
+ }
7927
+ if (onboardingState && onboardingState !== "onboardingFinished") {
7928
+ parts.push(`Build phase: ${onboardingState}`);
7929
+ }
7930
+ let hasUserSignal = false;
7931
+ for (const entry of keptEntries) {
7932
+ if (entry.hidden) {
7933
+ continue;
7766
7934
  }
7767
- for (const entry of keptEntries) {
7768
- const automated = parseSentinel(entry.text);
7769
- if (automated) {
7935
+ const automated = parseSentinel(entry.text);
7936
+ if (automated) {
7937
+ if (!NON_ACTION_SENTINELS.has(automated.name)) {
7770
7938
  parts.push(`Automated action: ${automated.name}`);
7771
- } else if (entry.text) {
7772
- parts.push(`User request: ${entry.text.slice(-500)}`);
7939
+ hasUserSignal = true;
7773
7940
  }
7941
+ } else if (entry.text) {
7942
+ parts.push(`User request: ${entry.text.slice(-500)}`);
7943
+ hasUserSignal = true;
7774
7944
  }
7775
- return parts.join("\n");
7776
- },
7777
- onStatus: (label) => onEvent({ type: "status", message: label }),
7778
- signal
7779
- });
7780
- async function handlePartialInput(acc, id, name, partial) {
7781
- const tool = getToolByName(name);
7782
- if (!tool?.streaming) {
7783
- return;
7784
7945
  }
7785
- const {
7786
- contentField = "content",
7787
- transform,
7788
- partialInput
7789
- } = tool.streaming;
7790
- if (partialInput) {
7791
- const result = partialInput(partial, acc.lastEmittedCount);
7792
- if (!result) {
7793
- return;
7794
- }
7795
- acc.lastEmittedCount = result.emittedCount;
7796
- acc.started = true;
7797
- onEvent({
7798
- type: "tool_start",
7799
- id,
7800
- name,
7801
- input: result.input,
7802
- partial: true
7803
- });
7804
- return;
7946
+ if (!hasUserSignal && !toolName && !text) {
7947
+ return "";
7805
7948
  }
7806
- const content = partial[contentField];
7807
- if (typeof content !== "string") {
7949
+ return parts.join("\n");
7950
+ },
7951
+ onStatus: (label) => onEvent({ type: "status", message: label }),
7952
+ signal
7953
+ });
7954
+ try {
7955
+ while (true) {
7956
+ let getOrCreateAccumulator2 = function(id, name) {
7957
+ let acc = toolInputAccumulators.get(id);
7958
+ if (!acc) {
7959
+ acc = { name, json: "", started: false, lastEmittedCount: 0 };
7960
+ toolInputAccumulators.set(id, acc);
7961
+ }
7962
+ return acc;
7963
+ };
7964
+ var getOrCreateAccumulator = getOrCreateAccumulator2;
7965
+ if (signal?.aborted) {
7966
+ onEvent({ type: "turn_cancelled" });
7967
+ saveSession(state);
7808
7968
  return;
7809
7969
  }
7810
- if (!acc.started) {
7811
- acc.started = true;
7812
- onEvent({ type: "tool_start", id, name, input: partial });
7813
- }
7814
- if (transform) {
7815
- const result = await transform(partial);
7816
- if (result === null) {
7970
+ const iterStart = Date.now();
7971
+ const contentBlocks = [];
7972
+ const thinkingBlockStartTimes = [];
7973
+ let thinkingCompleteCount = 0;
7974
+ let lastThinkingRelatedStartedAt;
7975
+ let textBlockOpen = false;
7976
+ const toolInputAccumulators = /* @__PURE__ */ new Map();
7977
+ let stopReason = "end_turn";
7978
+ let turnProviderMetadata;
7979
+ let turnModelId;
7980
+ contentBlocksRef = contentBlocks;
7981
+ subAgentText = "";
7982
+ currentToolNames = "";
7983
+ async function handlePartialInput(acc, id, name, partial) {
7984
+ const tool = getToolByName(name);
7985
+ if (!tool?.streaming) {
7817
7986
  return;
7818
7987
  }
7819
- onEvent({ type: "tool_input_delta", id, name, result });
7820
- } else {
7821
- onEvent({ type: "tool_input_delta", id, name, result: content });
7822
- }
7823
- }
7824
- try {
7825
- for await (const event of streamChatWithRetry(
7826
- {
7827
- ...apiConfig,
7828
- model: parentModel,
7829
- requestId,
7830
- system,
7831
- messages: cleanMessagesForApi(state.messages),
7832
- tools: tools2,
7833
- signal
7834
- },
7835
- {
7836
- onRetry: (attempt) => {
7837
- onEvent({
7838
- type: "status",
7839
- message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
7840
- });
7988
+ const {
7989
+ contentField = "content",
7990
+ transform,
7991
+ partialInput
7992
+ } = tool.streaming;
7993
+ if (partialInput) {
7994
+ const result = partialInput(partial, acc.lastEmittedCount);
7995
+ if (!result) {
7996
+ return;
7841
7997
  }
7998
+ acc.lastEmittedCount = result.emittedCount;
7999
+ acc.started = true;
8000
+ onEvent({
8001
+ type: "tool_start",
8002
+ id,
8003
+ name,
8004
+ input: result.input,
8005
+ partial: true
8006
+ });
8007
+ return;
7842
8008
  }
7843
- )) {
7844
- if (signal?.aborted) {
7845
- break;
8009
+ const content = partial[contentField];
8010
+ if (typeof content !== "string") {
8011
+ return;
7846
8012
  }
7847
- switch (event.type) {
7848
- case "text": {
7849
- const lastBlock = contentBlocks.at(-1);
7850
- if (lastBlock?.type === "text" && textBlockOpen) {
7851
- lastBlock.text += event.text;
7852
- } else {
7853
- contentBlocks.push({
7854
- type: "text",
7855
- text: event.text,
7856
- startedAt: event.ts
7857
- });
7858
- }
7859
- textBlockOpen = true;
7860
- onEvent({ type: "text", text: event.text });
7861
- break;
8013
+ if (!acc.started) {
8014
+ acc.started = true;
8015
+ onEvent({ type: "tool_start", id, name, input: partial });
8016
+ }
8017
+ if (transform) {
8018
+ const result = await transform(partial);
8019
+ if (result === null) {
8020
+ return;
7862
8021
  }
7863
- case "thinking":
7864
- if (event.text === "") {
7865
- thinkingBlockStartTimes.push(event.ts);
7866
- textBlockOpen = false;
8022
+ onEvent({ type: "tool_input_delta", id, name, result });
8023
+ } else {
8024
+ onEvent({ type: "tool_input_delta", id, name, result: content });
8025
+ }
8026
+ }
8027
+ try {
8028
+ for await (const event of streamChatWithRetry(
8029
+ {
8030
+ ...apiConfig,
8031
+ model: parentModel,
8032
+ requestId,
8033
+ system,
8034
+ messages: cleanMessagesForApi(state.messages),
8035
+ tools: tools2,
8036
+ signal
8037
+ },
8038
+ {
8039
+ onRetry: (attempt) => {
8040
+ onEvent({
8041
+ type: "status",
8042
+ message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
8043
+ });
7867
8044
  }
7868
- onEvent({ type: "thinking", text: event.text });
7869
- break;
7870
- case "thinking_complete": {
7871
- const startedAt = thinkingBlockStartTimes[thinkingCompleteCount] ?? event.ts;
7872
- contentBlocks.push({
7873
- type: "thinking",
7874
- thinking: event.thinking,
7875
- signature: event.signature,
7876
- startedAt,
7877
- completedAt: event.ts
7878
- });
7879
- thinkingCompleteCount++;
7880
- lastThinkingRelatedStartedAt = startedAt;
7881
- break;
7882
8045
  }
7883
- case "redacted_thinking_complete": {
7884
- const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
7885
- contentBlocks.push({
7886
- type: "redacted_thinking",
7887
- data: event.data,
7888
- startedAt,
7889
- completedAt: event.ts
7890
- });
7891
- lastThinkingRelatedStartedAt = startedAt;
8046
+ )) {
8047
+ if (signal?.aborted) {
7892
8048
  break;
7893
8049
  }
7894
- case "tool_input_delta": {
7895
- const acc = getOrCreateAccumulator2(event.id, event.name);
7896
- acc.json += event.delta;
7897
- try {
7898
- const partial = parsePartialJson(acc.json);
7899
- await handlePartialInput(acc, event.id, event.name, partial);
7900
- } catch {
8050
+ switch (event.type) {
8051
+ case "text": {
8052
+ const lastBlock = contentBlocks.at(-1);
8053
+ if (lastBlock?.type === "text" && textBlockOpen) {
8054
+ lastBlock.text += event.text;
8055
+ } else {
8056
+ contentBlocks.push({
8057
+ type: "text",
8058
+ text: event.text,
8059
+ startedAt: event.ts
8060
+ });
8061
+ }
8062
+ textBlockOpen = true;
8063
+ onEvent({ type: "text", text: event.text });
8064
+ break;
7901
8065
  }
7902
- break;
7903
- }
7904
- case "tool_input_args": {
7905
- const acc = getOrCreateAccumulator2(event.id, event.name);
7906
- await handlePartialInput(acc, event.id, event.name, event.args);
7907
- break;
7908
- }
7909
- case "tool_use": {
7910
- const tool = getToolByName(event.name);
7911
- contentBlocks.push({
7912
- type: "tool",
7913
- id: event.id,
7914
- name: event.name,
7915
- input: event.input,
7916
- startedAt: event.ts,
7917
- ...(event.input.background || tool?.backgroundOnly) && {
7918
- background: true
8066
+ case "thinking":
8067
+ if (event.text === "") {
8068
+ thinkingBlockStartTimes.push(event.ts);
8069
+ textBlockOpen = false;
7919
8070
  }
7920
- });
7921
- const acc = toolInputAccumulators.get(event.id);
7922
- const wasStreamed = acc?.started ?? false;
7923
- const isInputStreaming = !!tool?.streaming?.partialInput;
7924
- log14.info("Tool received", {
7925
- requestId,
7926
- toolCallId: event.id,
7927
- name: event.name
7928
- });
7929
- if (!wasStreamed || isInputStreaming) {
7930
- onEvent({
7931
- type: "tool_start",
8071
+ onEvent({ type: "thinking", text: event.text });
8072
+ break;
8073
+ case "thinking_complete": {
8074
+ const startedAt = thinkingBlockStartTimes[thinkingCompleteCount] ?? event.ts;
8075
+ contentBlocks.push({
8076
+ type: "thinking",
8077
+ thinking: event.thinking,
8078
+ signature: event.signature,
8079
+ startedAt,
8080
+ completedAt: event.ts
8081
+ });
8082
+ thinkingCompleteCount++;
8083
+ lastThinkingRelatedStartedAt = startedAt;
8084
+ break;
8085
+ }
8086
+ case "redacted_thinking_complete": {
8087
+ const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
8088
+ contentBlocks.push({
8089
+ type: "redacted_thinking",
8090
+ data: event.data,
8091
+ startedAt,
8092
+ completedAt: event.ts
8093
+ });
8094
+ lastThinkingRelatedStartedAt = startedAt;
8095
+ break;
8096
+ }
8097
+ case "tool_input_delta": {
8098
+ const acc = getOrCreateAccumulator2(event.id, event.name);
8099
+ acc.json += event.delta;
8100
+ try {
8101
+ const partial = parsePartialJson(acc.json);
8102
+ await handlePartialInput(acc, event.id, event.name, partial);
8103
+ } catch {
8104
+ }
8105
+ break;
8106
+ }
8107
+ case "tool_input_args": {
8108
+ const acc = getOrCreateAccumulator2(event.id, event.name);
8109
+ await handlePartialInput(acc, event.id, event.name, event.args);
8110
+ break;
8111
+ }
8112
+ case "tool_use": {
8113
+ const tool = getToolByName(event.name);
8114
+ contentBlocks.push({
8115
+ type: "tool",
7932
8116
  id: event.id,
7933
8117
  name: event.name,
7934
- input: event.input
8118
+ input: event.input,
8119
+ startedAt: event.ts,
8120
+ ...(event.input.background || tool?.backgroundOnly) && {
8121
+ background: true
8122
+ }
8123
+ });
8124
+ const acc = toolInputAccumulators.get(event.id);
8125
+ const wasStreamed = acc?.started ?? false;
8126
+ const isInputStreaming = !!tool?.streaming?.partialInput;
8127
+ log14.info("Tool received", {
8128
+ requestId,
8129
+ toolCallId: event.id,
8130
+ name: event.name
7935
8131
  });
8132
+ if (!wasStreamed || isInputStreaming) {
8133
+ onEvent({
8134
+ type: "tool_start",
8135
+ id: event.id,
8136
+ name: event.name,
8137
+ input: event.input,
8138
+ // Mirror the content-block stamp above so the live event
8139
+ // matches what a history reload shows — without it the
8140
+ // frontend renders background rows as already complete.
8141
+ ...isBackgroundCall({
8142
+ name: event.name,
8143
+ input: event.input
8144
+ }) && { background: true }
8145
+ });
8146
+ }
8147
+ break;
7936
8148
  }
7937
- break;
8149
+ case "done":
8150
+ stopReason = event.stopReason;
8151
+ turnProviderMetadata = event.providerMetadata;
8152
+ turnModelId = event.modelId;
8153
+ turnLlmCalls++;
8154
+ lastCallInputTokens = event.usage.inputTokens;
8155
+ lastCallCacheCreation = event.usage.cacheCreationTokens ?? 0;
8156
+ lastCallCacheRead = event.usage.cacheReadTokens ?? 0;
8157
+ turnInputTokens += lastCallInputTokens;
8158
+ turnOutputTokens += event.usage.outputTokens;
8159
+ turnCacheCreation += lastCallCacheCreation;
8160
+ turnCacheRead += lastCallCacheRead;
8161
+ recordUsage({
8162
+ ts: Date.now(),
8163
+ requestId,
8164
+ agentName: "parent",
8165
+ modelId: event.modelId,
8166
+ inputTokens: event.usage.inputTokens,
8167
+ outputTokens: event.usage.outputTokens,
8168
+ cacheCreationTokens: event.usage.cacheCreationTokens,
8169
+ cacheReadTokens: event.usage.cacheReadTokens,
8170
+ cost: nanoToDollars(event.cost),
8171
+ billingEvents: event.billingEvents,
8172
+ durationMs: Date.now() - iterStart,
8173
+ toolNames: contentBlocks.filter(
8174
+ (b) => b.type === "tool"
8175
+ ).map((b) => b.name)
8176
+ });
8177
+ break;
8178
+ case "error":
8179
+ statusWatcher.stop();
8180
+ onEvent({
8181
+ type: "error",
8182
+ error: friendlyError(event.error),
8183
+ ...event.code ? { code: event.code } : {}
8184
+ });
8185
+ return;
7938
8186
  }
7939
- case "done":
7940
- stopReason = event.stopReason;
7941
- turnProviderMetadata = event.providerMetadata;
7942
- turnModelId = event.modelId;
7943
- turnLlmCalls++;
7944
- lastCallInputTokens = event.usage.inputTokens;
7945
- lastCallCacheCreation = event.usage.cacheCreationTokens ?? 0;
7946
- lastCallCacheRead = event.usage.cacheReadTokens ?? 0;
7947
- turnInputTokens += lastCallInputTokens;
7948
- turnOutputTokens += event.usage.outputTokens;
7949
- turnCacheCreation += lastCallCacheCreation;
7950
- turnCacheRead += lastCallCacheRead;
7951
- recordUsage({
7952
- ts: Date.now(),
7953
- requestId,
7954
- agentName: "parent",
7955
- modelId: event.modelId,
7956
- inputTokens: event.usage.inputTokens,
7957
- outputTokens: event.usage.outputTokens,
7958
- cacheCreationTokens: event.usage.cacheCreationTokens,
7959
- cacheReadTokens: event.usage.cacheReadTokens,
7960
- cost: nanoToDollars(event.cost),
7961
- billingEvents: event.billingEvents,
7962
- durationMs: Date.now() - iterStart,
7963
- toolNames: contentBlocks.filter(
7964
- (b) => b.type === "tool"
7965
- ).map((b) => b.name)
7966
- });
7967
- break;
7968
- case "error":
7969
- onEvent({
7970
- type: "error",
7971
- error: friendlyError(event.error),
7972
- ...event.code ? { code: event.code } : {}
7973
- });
7974
- return;
8187
+ }
8188
+ } catch (err) {
8189
+ if (signal?.aborted) {
8190
+ } else {
8191
+ throw err;
7975
8192
  }
7976
8193
  }
7977
- } catch (err) {
7978
8194
  if (signal?.aborted) {
7979
- } else {
7980
- throw err;
8195
+ statusWatcher.stop();
8196
+ if (contentBlocks.length > 0) {
8197
+ contentBlocks.push({
8198
+ type: "text",
8199
+ text: "\n\n(cancelled)",
8200
+ startedAt: Date.now()
8201
+ });
8202
+ state.messages.push({
8203
+ role: "assistant",
8204
+ content: [...contentBlocks].sort(
8205
+ (a, b) => a.startedAt - b.startedAt
8206
+ ),
8207
+ usage: {
8208
+ inputTokens: turnInputTokens,
8209
+ outputTokens: turnOutputTokens,
8210
+ cacheCreationTokens: turnCacheCreation || void 0,
8211
+ cacheReadTokens: turnCacheRead || void 0,
8212
+ llmCalls: turnLlmCalls
8213
+ },
8214
+ ...turnProviderMetadata && {
8215
+ providerMetadata: turnProviderMetadata
8216
+ },
8217
+ model: turnModelId ?? parentModel,
8218
+ ...modelOverride && { modelOverride }
8219
+ });
8220
+ }
8221
+ onEvent({ type: "turn_cancelled" });
8222
+ saveSession(state);
8223
+ return;
7981
8224
  }
7982
- }
7983
- if (signal?.aborted) {
7984
- statusWatcher.stop();
7985
8225
  if (contentBlocks.length > 0) {
7986
- contentBlocks.push({
7987
- type: "text",
7988
- text: "\n\n(cancelled)",
7989
- startedAt: Date.now()
7990
- });
7991
8226
  state.messages.push({
7992
8227
  role: "assistant",
7993
8228
  content: [...contentBlocks].sort((a, b) => a.startedAt - b.startedAt),
@@ -8005,229 +8240,212 @@ async function runTurn(params) {
8005
8240
  ...modelOverride && { modelOverride }
8006
8241
  });
8007
8242
  }
8008
- onEvent({ type: "turn_cancelled" });
8009
- saveSession(state);
8010
- return;
8011
- }
8012
- if (contentBlocks.length > 0) {
8013
- state.messages.push({
8014
- role: "assistant",
8015
- content: [...contentBlocks].sort((a, b) => a.startedAt - b.startedAt),
8016
- usage: {
8017
- inputTokens: turnInputTokens,
8018
- outputTokens: turnOutputTokens,
8019
- cacheCreationTokens: turnCacheCreation || void 0,
8020
- cacheReadTokens: turnCacheRead || void 0,
8021
- llmCalls: turnLlmCalls
8022
- },
8023
- ...turnProviderMetadata && { providerMetadata: turnProviderMetadata },
8024
- model: turnModelId ?? parentModel,
8025
- ...modelOverride && { modelOverride }
8026
- });
8027
- }
8028
- const toolCalls = getToolCalls(contentBlocks);
8029
- if (stopReason !== "tool_use" || toolCalls.length === 0) {
8030
- statusWatcher.stop();
8031
- saveSession(state);
8032
- onEvent({
8033
- type: "turn_done",
8034
- stats: {
8035
- inputTokens: turnInputTokens,
8036
- outputTokens: turnOutputTokens,
8037
- cacheCreationTokens: turnCacheCreation || void 0,
8038
- cacheReadTokens: turnCacheRead || void 0,
8039
- llmCalls: turnLlmCalls,
8040
- lastCallInputTokens,
8041
- lastCallCacheCreation: lastCallCacheCreation || void 0,
8042
- lastCallCacheRead: lastCallCacheRead || void 0
8043
- }
8044
- });
8045
- return;
8046
- }
8047
- log14.info("Tools executing", {
8048
- requestId,
8049
- count: toolCalls.length,
8050
- tools: toolCalls.map((tc) => tc.name)
8051
- });
8052
- currentToolNames = toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ");
8053
- const wrappedOnEvent = (e) => {
8054
- if ("parentToolId" in e && e.parentToolId) {
8055
- if (e.type === "text") {
8056
- subAgentText = e.text;
8057
- } else if (e.type === "tool_start") {
8058
- subAgentText = `Using ${e.name}`;
8059
- }
8243
+ const toolCalls = getToolCalls(contentBlocks);
8244
+ if (stopReason !== "tool_use" || toolCalls.length === 0) {
8245
+ statusWatcher.stop();
8246
+ saveSession(state);
8247
+ onEvent({
8248
+ type: "turn_done",
8249
+ stats: {
8250
+ inputTokens: turnInputTokens,
8251
+ outputTokens: turnOutputTokens,
8252
+ cacheCreationTokens: turnCacheCreation || void 0,
8253
+ cacheReadTokens: turnCacheRead || void 0,
8254
+ llmCalls: turnLlmCalls,
8255
+ lastCallInputTokens,
8256
+ lastCallCacheCreation: lastCallCacheCreation || void 0,
8257
+ lastCallCacheRead: lastCallCacheRead || void 0
8258
+ }
8259
+ });
8260
+ return;
8060
8261
  }
8061
- onEvent(e);
8062
- };
8063
- const subAgentMessages = /* @__PURE__ */ new Map();
8064
- const results = await Promise.all(
8065
- toolCalls.map(async (tc) => {
8066
- if (signal?.aborted) {
8067
- return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
8262
+ log14.info("Tools executing", {
8263
+ requestId,
8264
+ count: toolCalls.length,
8265
+ tools: toolCalls.map((tc) => tc.name)
8266
+ });
8267
+ currentToolNames = toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ");
8268
+ const wrappedOnEvent = (e) => {
8269
+ if ("parentToolId" in e && e.parentToolId) {
8270
+ if (e.type === "text") {
8271
+ subAgentText = e.text;
8272
+ } else if (e.type === "tool_start") {
8273
+ subAgentText = `Using ${e.name}`;
8274
+ }
8068
8275
  }
8069
- const toolStart = Date.now();
8070
- let settle;
8071
- const resultPromise = new Promise((res) => {
8072
- settle = (result, isError) => res({ id: tc.id, result, isError });
8073
- });
8074
- let toolAbort = new AbortController();
8075
- let settled = false;
8076
- const safeSettle = (result, isError) => {
8077
- if (settled) {
8078
- return;
8276
+ onEvent(e);
8277
+ };
8278
+ const subAgentMessages = /* @__PURE__ */ new Map();
8279
+ const results = await Promise.all(
8280
+ toolCalls.map(async (tc) => {
8281
+ if (signal?.aborted) {
8282
+ return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
8079
8283
  }
8080
- settled = true;
8081
- signal?.removeEventListener("abort", cascadeAbort);
8082
- settle(result, isError);
8083
- };
8084
- const cascadeAbort = () => {
8085
- toolAbort.abort();
8086
- safeSettle(USER_CANCELLED_RESULT, true);
8087
- };
8088
- signal?.addEventListener("abort", cascadeAbort, { once: true });
8089
- const run = async (input) => {
8090
- try {
8091
- let result;
8092
- if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
8093
- saveSession(state);
8094
- log14.info("Waiting for external tool result", {
8095
- requestId,
8096
- toolCallId: tc.id,
8097
- name: tc.name
8098
- });
8099
- const blocksUser = USER_BLOCKING_EXTERNAL_TOOLS.has(tc.name);
8100
- if (blocksUser) {
8101
- statusWatcher.pause();
8102
- }
8103
- try {
8104
- result = await resolveExternalTool(tc.id, tc.name, input);
8105
- } finally {
8284
+ const toolStart = Date.now();
8285
+ let settle;
8286
+ const resultPromise = new Promise((res) => {
8287
+ settle = (result, isError) => res({ id: tc.id, result, isError });
8288
+ });
8289
+ let toolAbort = new AbortController();
8290
+ let settled = false;
8291
+ const safeSettle = (result, isError) => {
8292
+ if (settled) {
8293
+ return;
8294
+ }
8295
+ settled = true;
8296
+ signal?.removeEventListener("abort", cascadeAbort);
8297
+ settle(result, isError);
8298
+ };
8299
+ const cascadeAbort = () => {
8300
+ toolAbort.abort();
8301
+ safeSettle(USER_CANCELLED_RESULT, true);
8302
+ };
8303
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
8304
+ const run = async (input) => {
8305
+ try {
8306
+ let result;
8307
+ if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
8308
+ saveSession(state);
8309
+ log14.info("Waiting for external tool result", {
8310
+ requestId,
8311
+ toolCallId: tc.id,
8312
+ name: tc.name
8313
+ });
8314
+ const blocksUser = USER_BLOCKING_EXTERNAL_TOOLS.has(tc.name);
8106
8315
  if (blocksUser) {
8107
- statusWatcher.resume();
8316
+ statusWatcher.pause();
8108
8317
  }
8318
+ try {
8319
+ result = await resolveExternalTool(tc.id, tc.name, input);
8320
+ } finally {
8321
+ if (blocksUser) {
8322
+ statusWatcher.resume();
8323
+ }
8324
+ }
8325
+ } else {
8326
+ result = await executeTool(tc.name, input, {
8327
+ apiConfig,
8328
+ model,
8329
+ models: state.models,
8330
+ signal: toolAbort.signal,
8331
+ onEvent: wrappedOnEvent,
8332
+ resolveExternalTool,
8333
+ toolCallId: tc.id,
8334
+ requestId,
8335
+ onboardingState,
8336
+ subAgentMessages,
8337
+ conversationMessages: state.messages,
8338
+ toolRegistry,
8339
+ onBackgroundComplete,
8340
+ onLog: (line) => wrappedOnEvent({
8341
+ type: "tool_input_delta",
8342
+ id: tc.id,
8343
+ name: tc.name,
8344
+ result: line
8345
+ })
8346
+ });
8109
8347
  }
8110
- } else {
8111
- result = await executeTool(tc.name, input, {
8112
- apiConfig,
8113
- model,
8114
- models: state.models,
8115
- signal: toolAbort.signal,
8116
- onEvent: wrappedOnEvent,
8117
- resolveExternalTool,
8118
- toolCallId: tc.id,
8119
- requestId,
8120
- onboardingState,
8121
- subAgentMessages,
8122
- conversationMessages: state.messages,
8123
- toolRegistry,
8124
- onBackgroundComplete,
8125
- onLog: (line) => wrappedOnEvent({
8126
- type: "tool_input_delta",
8127
- id: tc.id,
8128
- name: tc.name,
8129
- result: line
8130
- })
8348
+ safeSettle(capToolResult(result), result.startsWith("Error"));
8349
+ } catch (err) {
8350
+ safeSettle(`Error: ${err.message}`, true);
8351
+ }
8352
+ };
8353
+ const entry = {
8354
+ id: tc.id,
8355
+ name: tc.name,
8356
+ input: tc.input,
8357
+ abortController: toolAbort,
8358
+ startedAt: toolStart,
8359
+ settle: safeSettle,
8360
+ rerun: (newInput) => {
8361
+ settled = false;
8362
+ toolAbort = new AbortController();
8363
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
8364
+ once: true
8131
8365
  });
8366
+ entry.abortController = toolAbort;
8367
+ entry.input = newInput;
8368
+ run(newInput);
8132
8369
  }
8133
- safeSettle(capToolResult(result), result.startsWith("Error"));
8134
- } catch (err) {
8135
- safeSettle(`Error: ${err.message}`, true);
8370
+ };
8371
+ toolRegistry?.register(entry);
8372
+ run(tc.input);
8373
+ const r = await resultPromise;
8374
+ if (!isBackgroundCall(tc)) {
8375
+ toolRegistry?.unregister(tc.id);
8136
8376
  }
8137
- };
8138
- const entry = {
8139
- id: tc.id,
8140
- name: tc.name,
8141
- input: tc.input,
8142
- abortController: toolAbort,
8143
- startedAt: toolStart,
8144
- settle: safeSettle,
8145
- rerun: (newInput) => {
8146
- settled = false;
8147
- toolAbort = new AbortController();
8148
- signal?.addEventListener("abort", () => toolAbort.abort(), {
8149
- once: true
8150
- });
8151
- entry.abortController = toolAbort;
8152
- entry.input = newInput;
8153
- run(newInput);
8377
+ log14.info("Tool completed", {
8378
+ requestId,
8379
+ toolCallId: tc.id,
8380
+ name: tc.name,
8381
+ durationMs: Date.now() - toolStart,
8382
+ isError: r.isError
8383
+ });
8384
+ onEvent({
8385
+ type: "tool_done",
8386
+ id: tc.id,
8387
+ name: tc.name,
8388
+ result: r.result,
8389
+ isError: r.isError
8390
+ });
8391
+ if (!r.isError && BRAND_TRIGGERING_TOOLS.has(tc.name)) {
8392
+ triggerBrandExtraction(
8393
+ apiConfig,
8394
+ resolveModel("brandExtractor", state.models, model)
8395
+ );
8154
8396
  }
8155
- };
8156
- toolRegistry?.register(entry);
8157
- run(tc.input);
8158
- const r = await resultPromise;
8159
- if (!isBackgroundCall(tc)) {
8160
- toolRegistry?.unregister(tc.id);
8161
- }
8162
- log14.info("Tool completed", {
8163
- requestId,
8164
- toolCallId: tc.id,
8165
- name: tc.name,
8166
- durationMs: Date.now() - toolStart,
8167
- isError: r.isError
8168
- });
8169
- onEvent({
8170
- type: "tool_done",
8171
- id: tc.id,
8172
- name: tc.name,
8173
- result: r.result,
8174
- isError: r.isError
8175
- });
8176
- if (!r.isError && BRAND_TRIGGERING_TOOLS.has(tc.name)) {
8177
- triggerBrandExtraction(
8178
- apiConfig,
8179
- resolveModel("brandExtractor", state.models, model)
8180
- );
8181
- }
8182
- return r;
8183
- })
8184
- );
8185
- statusWatcher.stop();
8186
- for (const r of results) {
8187
- const block = contentBlocks.find(
8188
- (b) => b.type === "tool" && b.id === r.id
8397
+ return r;
8398
+ })
8189
8399
  );
8190
- if (block?.type === "tool") {
8191
- block.result = r.result;
8192
- block.isError = r.isError;
8193
- block.completedAt = Date.now();
8194
- const msgs = subAgentMessages.get(r.id);
8195
- if (msgs) {
8196
- block.subAgentMessages = msgs;
8400
+ for (const r of results) {
8401
+ const block = contentBlocks.find(
8402
+ (b) => b.type === "tool" && b.id === r.id
8403
+ );
8404
+ if (block?.type === "tool") {
8405
+ block.result = r.result;
8406
+ block.isError = r.isError;
8407
+ block.completedAt = Date.now();
8408
+ const msgs = subAgentMessages.get(r.id);
8409
+ if (msgs) {
8410
+ block.subAgentMessages = msgs;
8411
+ }
8197
8412
  }
8198
8413
  }
8199
- }
8200
- const lastNonExcluded = toolCalls.filter(
8201
- (tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)
8202
- );
8203
- lastCompletedTools = lastNonExcluded.map((tc) => tc.name).join(", ");
8204
- lastCompletedInput = JSON.stringify(lastNonExcluded.at(-1)?.input ?? {});
8205
- lastCompletedResult = results.at(-1)?.result ?? "";
8206
- for (const r of results) {
8207
- state.messages.push({
8208
- role: "user",
8209
- content: r.result,
8210
- toolCallId: r.id,
8211
- isToolError: r.isError
8212
- });
8213
- }
8214
- if (takeSteering && !signal?.aborted) {
8215
- const injected = (await takeSteering()).filter(
8216
- (e) => e.text.trim().length > 0 || (e.attachments?.length ?? 0) > 0
8414
+ const lastNonExcluded = toolCalls.filter(
8415
+ (tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)
8217
8416
  );
8218
- if (injected.length > 0) {
8219
- for (const entry of injected) {
8220
- appendEntry(entry);
8221
- keptEntries.push(entry);
8417
+ lastCompletedTools = lastNonExcluded.map((tc) => tc.name).join(", ");
8418
+ lastCompletedInput = JSON.stringify(lastNonExcluded.at(-1)?.input ?? {});
8419
+ lastCompletedResult = results.at(-1)?.result ?? "";
8420
+ for (const r of results) {
8421
+ state.messages.push({
8422
+ role: "user",
8423
+ content: r.result,
8424
+ toolCallId: r.id,
8425
+ isToolError: r.isError
8426
+ });
8427
+ }
8428
+ if (takeSteering && !signal?.aborted) {
8429
+ const injected = (await takeSteering()).filter(
8430
+ (e) => e.text.trim().length > 0 || (e.attachments?.length ?? 0) > 0
8431
+ );
8432
+ if (injected.length > 0) {
8433
+ for (const entry of injected) {
8434
+ appendEntry(entry);
8435
+ keptEntries.push(entry);
8436
+ }
8437
+ saveSession(state);
8222
8438
  }
8439
+ }
8440
+ if (signal?.aborted) {
8441
+ statusWatcher.stop();
8442
+ onEvent({ type: "turn_cancelled" });
8223
8443
  saveSession(state);
8444
+ return;
8224
8445
  }
8225
8446
  }
8226
- if (signal?.aborted) {
8227
- onEvent({ type: "turn_cancelled" });
8228
- saveSession(state);
8229
- return;
8230
- }
8447
+ } finally {
8448
+ statusWatcher.stop();
8231
8449
  }
8232
8450
  }
8233
8451
 
@@ -8511,62 +8729,6 @@ var MessageQueue = class {
8511
8729
  }
8512
8730
  };
8513
8731
 
8514
- // src/automatedActions/resolve.ts
8515
- var NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
8516
- function resolveAction(text) {
8517
- const parsed = parseSentinel(text);
8518
- if (!parsed) {
8519
- return null;
8520
- }
8521
- const { name: triggerName, remainder } = parsed;
8522
- if (NON_ACTION_SENTINELS.has(triggerName)) {
8523
- return null;
8524
- }
8525
- let params = {};
8526
- if (remainder) {
8527
- try {
8528
- params = JSON.parse(remainder.split("\n")[0]);
8529
- } catch {
8530
- }
8531
- }
8532
- let body = readAsset("automatedActions", `${triggerName}.md`);
8533
- let next;
8534
- const fmMatch = body.match(/^---\s*\n([\s\S]*?)\n---/);
8535
- if (fmMatch) {
8536
- const nextMatch = fmMatch[1].match(/^\s*next:\s*(\w+)\s*$/m);
8537
- if (nextMatch) {
8538
- next = nextMatch[1];
8539
- }
8540
- }
8541
- body = body.replace(/^---[\s\S]*?---\s*/, "");
8542
- for (const [key, value] of Object.entries(params)) {
8543
- const str = typeof value === "string" ? value : JSON.stringify(value);
8544
- body = body.replaceAll(`{{${key}}}`, str);
8545
- }
8546
- return {
8547
- message: automatedMessage(triggerName, body),
8548
- next
8549
- };
8550
- }
8551
- function getActionChain(startName) {
8552
- const chain = [];
8553
- const seen = /* @__PURE__ */ new Set();
8554
- let name = startName;
8555
- while (name && !seen.has(name)) {
8556
- seen.add(name);
8557
- chain.push(name);
8558
- let body;
8559
- try {
8560
- body = readAsset("automatedActions", `${name}.md`);
8561
- } catch {
8562
- break;
8563
- }
8564
- const fm = body.match(/^---\s*\n([\s\S]*?)\n---/);
8565
- name = fm?.[1].match(/^\s*next:\s*(\w+)\s*$/m)?.[1];
8566
- }
8567
- return chain;
8568
- }
8569
-
8570
8732
  // src/headless/index.ts
8571
8733
  var log16 = createLogger("headless");
8572
8734
  var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
@@ -8622,6 +8784,13 @@ var HeadlessSession = class {
8622
8784
  earlyResults = /* @__PURE__ */ new Map();
8623
8785
  // Tool block updates from background completions (separate from the message queue)
8624
8786
  pendingBlockUpdates = [];
8787
+ /**
8788
+ * Id of the UI-only tool block synthesized for the in-flight user/gate
8789
+ * compaction (see the compaction listener). Model-invoked compactions have
8790
+ * a real block and never set this. Single slot — triggerCompaction is
8791
+ * single-flight.
8792
+ */
8793
+ syntheticCompactionId = null;
8625
8794
  // Tool lifecycle management — shared across all nesting depths
8626
8795
  toolRegistry = new ToolRegistry();
8627
8796
  // IO — accumulates stdin bytes between newline boundaries
@@ -8653,6 +8822,7 @@ var HeadlessSession = class {
8653
8822
  this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
8654
8823
  });
8655
8824
  this.passivePen = loadPassiveResults();
8825
+ this.persistStats();
8656
8826
  if (resumed) {
8657
8827
  this.emit("session_restored", {
8658
8828
  messageCount: this.state.messages.length,
@@ -8675,15 +8845,61 @@ var HeadlessSession = class {
8675
8845
  );
8676
8846
  this.sessionStats.compactionInProgress = true;
8677
8847
  this.persistStats();
8848
+ if (!event.toolCallId && (event.origin === "user" || event.origin === "gate")) {
8849
+ const id = `compact_${Date.now()}`;
8850
+ this.syntheticCompactionId = id;
8851
+ this.state.messages.push({
8852
+ role: "assistant",
8853
+ content: [
8854
+ {
8855
+ type: "tool",
8856
+ id,
8857
+ name: "compactConversation",
8858
+ input: {},
8859
+ startedAt: Date.now(),
8860
+ background: true,
8861
+ uiOnly: true,
8862
+ result: "Compaction started in the background."
8863
+ }
8864
+ ]
8865
+ });
8866
+ saveSession(this.state);
8867
+ this.onEvent({
8868
+ type: "tool_start",
8869
+ id,
8870
+ name: "compactConversation",
8871
+ input: {},
8872
+ background: true
8873
+ });
8874
+ this.onEvent({
8875
+ type: "tool_done",
8876
+ id,
8877
+ name: "compactConversation",
8878
+ result: "Compaction started in the background.",
8879
+ isError: false
8880
+ });
8881
+ }
8678
8882
  } else {
8679
8883
  const data = event.error ? { error: event.error } : {};
8680
8884
  this.emit("compaction_complete", data, event.requestId);
8885
+ if (this.syntheticCompactionId) {
8886
+ const id = this.syntheticCompactionId;
8887
+ this.syntheticCompactionId = null;
8888
+ this.onBackgroundComplete(
8889
+ id,
8890
+ "compactConversation",
8891
+ event.error ? `Error: ${event.error}` : formatSummariesResult(event.summaries ?? [])
8892
+ );
8893
+ }
8681
8894
  this.sessionStats.compactionInProgress = false;
8682
8895
  if (!event.error) {
8683
8896
  this.sessionStats.lastContextSize = 0;
8684
8897
  }
8685
8898
  this.sessionStats.messageCount = this.state.messages.length;
8686
8899
  this.persistStats();
8900
+ if (!this.running) {
8901
+ this.kickDrain();
8902
+ }
8687
8903
  }
8688
8904
  });
8689
8905
  process.stdin.setEncoding("utf-8");
@@ -8822,7 +9038,8 @@ var HeadlessSession = class {
8822
9038
  await triggerCompaction(this.state, this.config, {
8823
9039
  blocking: true,
8824
9040
  requestId,
8825
- model: this.opts.model
9041
+ model: this.opts.model,
9042
+ origin: "gate"
8826
9043
  });
8827
9044
  applyPendingSummaries(this.state);
8828
9045
  } catch {
@@ -8851,6 +9068,12 @@ var HeadlessSession = class {
8851
9068
  }
8852
9069
  return;
8853
9070
  }
9071
+ if (notify === "silent") {
9072
+ if (!this.running) {
9073
+ this.applyPendingBlockUpdates();
9074
+ }
9075
+ return;
9076
+ }
8854
9077
  this.queue.push({
8855
9078
  command: {
8856
9079
  action: "message",
@@ -9232,6 +9455,12 @@ var HeadlessSession = class {
9232
9455
  this.completedEmitted = false;
9233
9456
  this.lastCompleted = null;
9234
9457
  this.turnStart = Date.now();
9458
+ const inflight2 = getInflightCompaction();
9459
+ if (inflight2) {
9460
+ await inflight2.catch(() => {
9461
+ });
9462
+ }
9463
+ applyPendingSummaries(this.state);
9235
9464
  if (this.passivePen.length > 0) {
9236
9465
  const swept = this.passivePen.splice(0);
9237
9466
  this.persistStats();
@@ -9321,7 +9550,7 @@ var HeadlessSession = class {
9321
9550
  this.applyPendingBlockUpdates();
9322
9551
  }
9323
9552
  async handleMessage(parsed, requestId) {
9324
- if (this.running) {
9553
+ if (this.running || getInflightCompaction()) {
9325
9554
  const command = { ...parsed };
9326
9555
  if (requestId && command.requestId === void 0) {
9327
9556
  command.requestId = requestId;
@@ -9331,6 +9560,9 @@ var HeadlessSession = class {
9331
9560
  source: "user",
9332
9561
  enqueuedAt: Date.now()
9333
9562
  });
9563
+ if (!this.running && !getInflightCompaction()) {
9564
+ this.kickDrain();
9565
+ }
9334
9566
  return;
9335
9567
  }
9336
9568
  this.running = true;
@@ -9338,6 +9570,7 @@ var HeadlessSession = class {
9338
9570
  await this.runSingleTurn(parsed, requestId);
9339
9571
  await this.drainQueueLoop();
9340
9572
  } finally {
9573
+ this.currentAbort?.abort();
9341
9574
  this.currentAbort = null;
9342
9575
  this.currentRequestId = void 0;
9343
9576
  this.running = false;
@@ -9369,6 +9602,18 @@ var HeadlessSession = class {
9369
9602
  async drainQueueLoop() {
9370
9603
  while (this.queue.length > 0) {
9371
9604
  const head = this.queue.peek();
9605
+ if (head.command.action === "compact") {
9606
+ this.queue.shift();
9607
+ await triggerCompaction(this.state, this.config, {
9608
+ blocking: true,
9609
+ requestId: head.command.requestId,
9610
+ model: this.opts.model,
9611
+ origin: "user"
9612
+ }).catch(() => {
9613
+ });
9614
+ applyPendingSummaries(this.state);
9615
+ continue;
9616
+ }
9372
9617
  if (head.source === "chain") {
9373
9618
  const item = this.queue.shift();
9374
9619
  const rid = item.command.requestId ?? `chain-${Date.now()}`;
@@ -9413,6 +9658,7 @@ var HeadlessSession = class {
9413
9658
  try {
9414
9659
  await this.drainQueueLoop();
9415
9660
  } finally {
9661
+ this.currentAbort?.abort();
9416
9662
  this.currentAbort = null;
9417
9663
  this.currentRequestId = void 0;
9418
9664
  this.running = false;
@@ -9659,11 +9905,31 @@ var HeadlessSession = class {
9659
9905
  return;
9660
9906
  }
9661
9907
  if (action === "compact") {
9908
+ if (this.running) {
9909
+ const alreadyQueued = this.queue.snapshot().some((it) => it.command.action === "compact");
9910
+ if (!alreadyQueued) {
9911
+ this.queue.push({
9912
+ command: {
9913
+ action: "compact",
9914
+ // Display label for the queue card (matches the automated
9915
+ // action idiom); never resolved as an action — drainQueueLoop
9916
+ // routes on command.action before any sentinel handling.
9917
+ text: sentinel("compact"),
9918
+ requestId
9919
+ },
9920
+ source: "user",
9921
+ enqueuedAt: Date.now()
9922
+ });
9923
+ }
9924
+ this.emit("completed", { success: true, queued: true }, requestId);
9925
+ return;
9926
+ }
9662
9927
  try {
9663
9928
  await triggerCompaction(this.state, this.config, {
9664
9929
  blocking: false,
9665
9930
  requestId,
9666
- model: this.opts.model
9931
+ model: this.opts.model,
9932
+ origin: "user"
9667
9933
  });
9668
9934
  if (!this.running) {
9669
9935
  applyPendingSummaries(this.state);